Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optimistic React Apollo ui lag with React Beautiful Drag and Drop

I'm trying to create a optimistic response where the ui updates immmediately (minimal lag and better user experience) with drag and dropped data. The issue i'm having however is that it lags anyways.

So whats happening is that I expect a list of zones and unassigned zones from my query, unassignedZone is a object, with cities in them, and zones is a list of zones with cities within them. When writing my mutation, I return the new reordered list of zones after dragging and dropping them. The reordering is done by a field on a zone object called 'DisplayOrder' The logic is setting the numbers correct. The problem is that when I try to mimic it with optimistic ui and update, there is a slight lag like its still waiting for the network.

Most of the meat of what i'm trying to achieve is happening at the onDragEnd = () => { ... } function.

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 <- this is a video demonstrating it happening.

like image 825
Karan Avatar asked Sep 17 '18 02:09

Karan


1 Answers

For anyone who comes across this same issue, the main problem was that my update / optimisticResponse were both not correct. Something to mention here is this block:

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
            }
          });
        }

If you compare it to my original code up top, you see that when I writeQuery I have variables this time. Looking with the apollo devtools, I saw that there was a entry added, just one with the wrong variables. So that was a easy fix. The optimistic response was correct (mimics the payload returned from our mutation). The other aspect that was wrong, was that my query for fetching all this data initially had a fetch policy of cache and network, what this means is that when we receive data we cache it, and we ask for the latest data. So this will always fetch the latest data. I didn't need that, hence the little lag coming, I just needed optimisticResponse. By default apollo does cache-first, where it looks in the cache for data, if its not there we grab it via the network. Pairs well with cache updates and slow nets.

like image 145
Karan Avatar answered Nov 15 '22 03:11

Karan