Optimistic React Apollo ui lag with React Beautiful Drag and Drop

Posted

技术标签:

【中文标题】Optimistic React Apollo ui lag with React Beautiful Drag and Drop【英文标题】: 【发布时间】:2019-02-20 22:21:20 【问题描述】:

我正在尝试创建一个乐观的响应,其中 UI 立即更新(最小的延迟和更好的用户体验)拖放数据。然而,我遇到的问题是它仍然滞后。

所以发生的事情是,我希望从查询中获得区域和未分配区域的列表,unassignedZone 是一个对象,其中包含城市,而区域是其中包含城市的区域列表。在编写我的突变时,我在拖放后返回新的重新排序的区域列表。重新排序由名为“DisplayOrder”的区域对象上的字段完成。逻辑将数字设置正确。问题是当我尝试用乐观的 ui 和更新来模仿它时,会有一点延迟,就像它仍在等待网络一样。

我想要实现的大部分内容都发生在 onDragEnd = () => ... 函数中。

import React,  Component  from "react";
import  graphql, compose, withApollo  from "react-apollo";
import gql from "graphql-tag";
import  withState  from "recompose";
import  withStyles  from "@material-ui/core/styles";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Input from "@material-ui/core/Input";
import Grid from "@material-ui/core/Grid";
import InputLabel from "@material-ui/core/InputLabel";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import AppBar from "@material-ui/core/AppBar";
import _ from "lodash";
import FormControl from "@material-ui/core/FormControl";
import move from "lodash-move";
import  Zone  from "../../Components/Zone";

const style = 
  ddlRight: 
    left: "3px",
    position: "relative",
    paddingRight: "10px"
  ,
  ddlDrop: 
    marginBottom: "20px"
  ,
  dropdownInput: 
    minWidth: "190px"
  
;

class Zones extends Component 
  constructor(props) 
    super(props);
    this.state = 
      companyId: "",
      districtId: "",
      selectedTab: "Zones",
      autoFocusDataId: null,
      zones: [],
      unassignedZone: null
    ;
  

  handleChange = event => 
    const  client  = this.props;
    this.setState( [event.target.name]: event.target.value );
  ;

  handleTabChange = (event, selectedTab) => 
    this.setState( selectedTab );
  ;

  onDragStart = () => 
    this.setState(
      autoFocusDataId: null
    );
  ;

  calculateLatestDisplayOrder = () => 
    const  allZones  = this.state;
    if (allZones.length === 0) 
      return 10;
    
    return allZones[allZones.length - 1].displayOrder + 10;
  ;

  updateCitiesDisplayOrder = cities => 
    let displayOrder = 0;
    const reorderedCities = _.map(cities, aCity => 
      displayOrder += 10;
      const city =  ...aCity, ... displayOrder  ;
      if (city.ZonesCities) 
        city.ZonesCities.displayOrder = displayOrder;
      
      return city;
    );
    return reorderedCities;
  ;

  moveAndUpdateDisplayOrder = (allZones, result) => 
    const reorderedZones = _.cloneDeep(
      move(allZones, result.source.index, result.destination.index)
    );
    let displayOrder = 0;
    _.each(reorderedZones, (aZone, index) => 
      displayOrder += 10;
      aZone.displayOrder = displayOrder;
    );
    return reorderedZones;
  ;

  /**
   * droppable id board represents zones
   * @param result [holds our source and destination draggable content]
   * @return
   */

  onDragEnd = result => 
    console.log("Dragging");
    if (!result.destination) 
      return;
    
    const source = result.source;
    const destination = result.destination;
    if (
      source.droppableId === destination.droppableId &&
      source.index === destination.index
    ) 
      return;
    

    const 
      zonesByCompanyAndDistrict,
      unassignedZoneByCompanyAndDistrict
     = this.props.zones;
    // reordering column
    if (result.type === "COLUMN") 
      if (result.source.index < 0 || result.destination.index < 0) 
        return;
      

      const  reorderZones, companyId, districtId  = this.props;
      const sourceData = zonesByCompanyAndDistrict[result.source.index];
      const destinationData =
        zonesByCompanyAndDistrict[result.destination.index];
      const reorderedZones = this.moveAndUpdateDisplayOrder(
        zonesByCompanyAndDistrict,
        result
      );
      console.log(reorderedZones);
      console.log(unassignedZoneByCompanyAndDistrict);
      reorderZones(
        variables: 
          companyId,
          districtId,
          sourceDisplayOrder: sourceData.displayOrder,
          destinationDisplayOrder: destinationData.displayOrder,
          zoneId: sourceData.id
        ,
        optimisticResponse: 
          __typename: "Mutation",
          reorderZones: 
            zonesByCompanyAndDistrict: reorderedZones
          
        ,
        // refetchQueries: () => ["zones"],
        update: (store,  data:  reorderZones  ) => 
          const data = store.readQuery(
            query: unassignedAndZonesQuery,
            variables: 
              companyId,
              districtId
            
          );

          store.writeQuery(
            query: unassignedAndZonesQuery,
            data: data
          );
        
      );
      // this.setState( zones: reorderedZones );
      // Need to reorder zones api call here
      // TODO: Elixir endpoint to reorder zones
    
    return;
  ;

  render() 
    const  selectedTab  = this.state;
    const 
      classes,
      companies,
      districts,
      companyId,
      districtId,
      setCompanyId,
      setDistrictId,
      zones
     = this.props;
    const isDisabled = !companyId || !districtId;
    return (
      <Grid container spacing=16>
        <Grid container spacing=16 className=classes.ddlDrop>
          <Grid item xs=12 className=classes.ddlRight>
            <h2>Company Zones</h2>
          </Grid>
          <Grid item xs=2 className=classes.ddlRight>
            <FormControl>
              <InputLabel htmlFor="company-helper">Company</InputLabel>
              <Select
                value=companyId
                onChange=event => 
                  setCompanyId(event.target.value);
                
                input=
                  <Input
                    name="companyId"
                    id="company-helper"
                    className=classes.dropdownInput
                  />
                
              >
                _.map(companies.companies, aCompany => 
                  return (
                    <MenuItem
                      value=aCompany.id
                      key=`companyItem-$aCompany.id`
                    >
                      aCompany.name
                    </MenuItem>
                  );
                )
              </Select>
            </FormControl>
          </Grid>
          <Grid item xs=2 className=classes.ddlRight>
            <FormControl>
              <InputLabel htmlFor="district-helper">District</InputLabel>
              <Select
                value=districtId
                onChange=event => 
                  setDistrictId(event.target.value);
                
                input=
                  <Input
                    name="districtId"
                    id="district-helper"
                    className=classes.dropdownInput
                  />
                
              >
                _.map(districts.districts, aDistrict => 
                  return (
                    <MenuItem
                      value=aDistrict.id
                      key=`districtItem-$aDistrict.id`
                    >
                      aDistrict.name
                    </MenuItem>
                  );
                )
              </Select>
            </FormControl>
          </Grid>
        </Grid>
        <Grid container>
          <AppBar position="static" color="primary">
            <Tabs value=selectedTab onChange=this.handleTabChange>
              <Tab value="Zones" label="Zone" disabled=isDisabled />
              <Tab
                value="Pricing Structure"
                label="Pricing Structure"
                disabled=isDisabled
              />
              <Tab value="Pricing" label="Pricing" disabled=isDisabled />
              <Tab
                value="Student Pricing"
                label="Student Pricing"
                disabled=isDisabled
              />
            </Tabs>
          </AppBar>
          selectedTab === "Zones" &&
            zones &&
            zones.zonesByCompanyAndDistrict && (
              <Zone
                onDragStart=this.onDragStart
                onDragEnd=this.onDragEnd
                zones=_.sortBy(zones.zonesByCompanyAndDistrict, [
                  "displayOrder"
                ])
                unassignedZone=zones.unassignedZoneByCompanyAndDistrict
              />
            )
          selectedTab === "Pricing Structure" && <div>Pricing Structure</div>
          selectedTab === "Pricing" && <div>Pricing</div>
          selectedTab === "Student Pricing" && <div>Student Pricing</div>
        </Grid>
      </Grid>
    );
  


const companiesQuery = gql`
  query allCompanies 
    companies 
      id
      name
    
  
`;

const districtsQuery = gql`
  query allDistricts 
    districts 
      id
      name
    
  
`;

const unassignedAndZonesQuery = gql`
  query zones($companyId: String!, $districtId: String!) 
    unassignedZoneByCompanyAndDistrict(
      companyId: $companyId
      districtId: $districtId
    ) 
      name
      description
      displayOrder
      cities 
        id
        name
      
    

    zonesByCompanyAndDistrict(companyId: $companyId, districtId: $districtId) 
      id
      name
      description
      displayOrder
      basePrice
      zoneCities 
        displayOrder
        city 
          id
          name
        
      
    
  
`;

const reorderZones = gql`
  mutation(
    $companyId: String!
    $districtId: String!
    $sourceDisplayOrder: Int!
    $destinationDisplayOrder: Int!
    $zoneId: String!
  ) 
    reorderZones(
      companyId: $companyId
      districtId: $districtId
      sourceDisplayOrder: $sourceDisplayOrder
      destinationDisplayOrder: $destinationDisplayOrder
      zoneId: $zoneId
    ) 
      id
      __typename
      name
      description
      displayOrder
      basePrice
      zoneCities 
        displayOrder
        city 
          id
          name
        
      
    
  
`;

export default compose(
  withState("companyId", "setCompanyId", ""),
  withState("districtId", "setDistrictId", ""),
  graphql(unassignedAndZonesQuery, 
    name: "zones",
    skip: ( companyId, districtId ) => !(companyId && districtId),
    options: ( companyId, districtId ) => (
      variables:  companyId, districtId ,
      fetchPolicy: "cache-and-network"
    )
  ),
  graphql(companiesQuery, 
    name: "companies",
    options:  fetchPolicy: "cache-and-network" 
  ),
  graphql(districtsQuery, 
    name: "districts",
    options:  fetchPolicy: "cache-and-network" 
  ),
  graphql(reorderZones, 
    name: "reorderZones"
  ),
  withApollo,
  withStyles(style)
)(Zones);

https://drive.google.com/file/d/1ujxTOGr0YopeBxrGfKDGfd1Cl0HiMaK0/view?usp=sharing

【问题讨论】:

【参考方案1】:

对于遇到同样问题的任何人,主要问题是我的更新/乐观响应都不正确。这里要提的是这个块:

update: (store,  data:  reorderZones  ) => 
          const 
            zonesByCompanyAndDistrict,
            unassignedZoneByCompanyAndDistrict
           = store.readQuery(
            query: unassignedAndZonesQuery,
            variables: 
              companyId,
              districtId
            
          );
          const reorderedZones = this.moveAndUpdateDisplayOrder(
            zonesByCompanyAndDistrict,
            result
          );
          store.writeQuery(
            query: unassignedAndZonesQuery,
            variables:  companyId, districtId ,
            data: 
              unassignedZoneByCompanyAndDistrict,
              zonesByCompanyAndDistrict: reorderedZones
            
          );
        

如果您将它与我的原始代码进行比较,您会发现,当我 writeQuery 时,这次我有变量。查看 apollo devtools,我看到添加了一个条目,只是一个带有错误变量的条目。所以这很容易解决。乐观的响应是正确的(模仿从我们的突变返回的有效负载)。错误的另一个方面是,我获取所有这些数据的查询最初有一个缓存和网络的获取策略,这意味着当我们接收到数据时我们缓存它,并且我们要求最新的数据。所以这将始终获取最新数据。我不需要那个,因此会有一点滞后,我只需要乐观响应。默认情况下,apollo 会先缓存,它会在缓存中查找数据,如果不存在,我们会通过网络获取它。与缓存更新和慢速网络完美搭配。

【讨论】:

以上是关于Optimistic React Apollo ui lag with React Beautiful Drag and Drop的主要内容,如果未能解决你的问题,请参考以下文章

Reactjs/Apollo/AppSync Mutation Optimistic Response Resolved ID

React Apollo 显示“react-apollo 仅支持每个 HOC 的查询、订阅或突变”错误

Prisma Playground 中的 React + Apollo“入门”错误

Apollo:在订阅更新时更新 React 道具?

找不到模块:无法解析“apollo-link-context”-REACT,APOLLO-SERVER

使用 `react-apollo-hooks` 和 `useSubscription` 钩子