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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With