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.
Related
I'm working on a React component that uses gql and useQuery from GraphQL to fetch data from an API.
My problem is that the data resulting is undefined because probably the state is not handled correctly and the component didn't load the data requested to the API.
I'm new to React and cannot resolve this situation probably a way to wait until the data is loaded and become defined but I have no idea how to do it.
I was trying even to add a useEffect not present in this version but didn't give any differences :( maybe I just don't know how to use it
The component as follows
import { gql, useQuery } from '#apollo/client';
import { useConfiguration } from '#lib/hooks/useConfiguration';
import { useSetConfiguration } from '#lib/hooks/useSetConfiguration';
import { useUnSetConfiguration } from '#lib/hooks/useUnSetConfiguration';
import { FormControlLabel, FormGroup, Switch, Typography } from '#mui/material';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
const QUERY = gql`
query WEB_useConfiguration($name: String!, $scope: StudyConfigurationScope) {
studyConfiguration(name: $name, filter: $scope) {
configurationOverrideChain {
value
scope
}
}
}
`;
const beforeQuery = ({ studyId }) => ({
variables: { name: 'messaging.email.sender.address', scope: { studyId } },
fetchPolicy: 'network-only',
});
const afterQuery = data => ({
studyConfigurationOverrides:
data?.studyConfiguration?.configurationOverrideChain ?? {},
});
const StudyConfiguration = ({ studyId }) => {
const intl = useIntl();
const [studyConf, setStudyConf] = useState({});
const { data } = useQuery(QUERY, beforeQuery({ studyId }));
console.log('data: ', data); // undefined
const { studyConfigurationOverrides } = afterQuery(data);
// !TODO: make one smart useConfiguration hook instead of 3
// Getting the study config
const smsEnabled = useConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
defaultValue: false,
});
const emailSender = useConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [valueEmailReplay, setValueEmailReplay] = useState(emailSender);
const [valueSmsConf, setValueSmsConf] = useState(smsEnabled);
useEffect(() => {
if (valueSmsConf !== smsEnabled) {
setValueSmsConf(smsEnabled);
}
}, [smsEnabled]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (valueEmailReplay !== emailSender) {
setValueEmailReplay(emailSender);
}
}, [emailSender]); // eslint-disable-line react-hooks/exhaustive-deps
let suffix;
const globalValue = studyConfigurationOverrides.find(
e => e.scope === 'GLOBAL',
);
const defaultValue = studyConfigurationOverrides.find(
e => e.scope === 'DEfAULT',
);
if (globalValue) {
suffix = globalValue.value;
}
if (defaultValue) {
suffix = defaultValue.value;
}
const emailDomain = suffix?.substring(suffix.indexOf('#'));
const noReplyEmail = `noreply${emailDomain}`;
// Set study config
const [setNoReplyStudyEmail] = useSetConfiguration({
name: 'messaging.email.sender.address',
value: noReplyEmail,
scope: { studyId },
});
const [setSmsMessagingDisable] = useSetConfiguration({
name: 'messaging.recruitment.sms.enable',
value: 'false',
scope: { studyId },
});
// unSet study config
const [setDefaultStudyEmail] = useUnSetConfiguration({
name: 'messaging.email.sender.address',
scope: { studyId },
});
const [setSmsMessagingDefault] = useUnSetConfiguration({
name: 'messaging.recruitment.sms.enable',
scope: { studyId },
});
const handleReplayEmailChange = async event => {
setValueEmailReplay(event.target.checked ? suffix : noReplyEmail);
event.target.checked
? await setDefaultStudyEmail()
: await setNoReplyStudyEmail();
};
const handleSmsConf = async event => {
setValueSmsConf(event.target.checked);
event.target.checked
? await setSmsMessagingDefault()
: await setSmsMessagingDisable();
};
const isEmailEnabled = emailSender === suffix;
return (
<FormGroup>
<FormControlLabel
control={
<Switch checked={isEmailEnabled} onChange={handleReplayEmailChange} />
}
label={
<Typography color="textPrimary">
{intl.formatMessage(
{
defaultMessage:
'Allow candidates to reply to emails (send from {replyEmailTxt} instead of {noReplyTxt})',
},
{ replyEmailTxt: suffix, noReplyTxt: 'noReplyEmail' },
)}
</Typography>
}
/>
<FormControlLabel
control={<Switch checked={valueSmsConf} onChange={handleSmsConf} />}
label={
<Typography color="textPrimary">
{intl.formatMessage({
defaultMessage: `SMS messaging`,
})}
</Typography>
}
/>
</FormGroup>
);
};
export default StudyConfiguration;
Using react 17 and relay-modern 11 I want to build a list, with which, when one reaches the end, they can click a button that says load more and it adds more entries to the list. Here is what I have so far. The rows are name and cursor
see I should click load more and it should add an extra 5 rows of data. This is what happens when I click load more
See it did fetch 5 new nodes. I can tell because the cursors are unique. However it did not append it to the list like I wanted.
How can I get this list to continue to build all the time as I keep clicking load more, until there is no more data?
Here is my code:
the root of the component:
// index.js
import React, { useState } from "react";
import { Text, View } from "react-native";
import { QueryRenderer, useRelayEnvironment } from "react-relay";
import PaginatedProfilesListContainer from "./PaginatedProfilesListContainer";
const ProfilesList = () => {
const environment = useRelayEnvironment();
const [count, setCount] = useState(5);
const [name, setName] = useState("");
const query = graphql`
query ProfilesListQuery(
$count: Int!
$cursor: String
$filter: ProfileFilter
) {
...PaginatedProfilesListContainer_list
}
`;
const filter = {
name,
};
const variables = {
count: 5,
cursor: null,
filter,
};
const render = ({ error, props, retry }) => {
if (error) {
return (
<View>
<Text>{error.message}</Text>
</View>
);
} else if (props) {
return (
<View>
<PaginatedProfilesListContainer
pagination={props}
count={count}
setCount={setCount}
name={name}
setName={setName}
filter={filter}
/>
</View>
);
} else {
return (
<View>
<Text>loading profiles list...</Text>
</View>
);
}
};
console.log("vriable", variables)
return (
<QueryRenderer
environment={environment}
query={query}
variables={variables}
render={render}
/>
);
};
export default ProfilesList;
here is the component that should be listing the object
// PaginatedProfilesListContainer.js
import React from "react";
import { Text, View } from "react-native";
import { Button } from "react-native-paper";
import { createPaginationContainer } from "react-relay";
import { FadoTextInput } from "../forms/fadoTextInput";
const PaginatedProfilesListContainer = (props) => {
console.log(props);
console.log("createPaginationContainer", createPaginationContainer)
// console.log(pagination)
const { pagination, count, setCount, name, setName, relay, filter } = props;
const { hasMore, loadMore, refetchConnection } = relay;
console.log(loadMore)
const { profiles } = pagination;
const { edges, pageInfo } = profiles;
return (
<View>
<View>
<FadoTextInput
dense={true}
isNumeric={true}
graphqlErrors={[]}
label="count"
errorKey="count"
// savedValue={price.amount}
value={count}
onChangeText={setCount}
/>
<FadoTextInput
dense={true}
isNumeric={false}
graphqlErrors={[]}
label="name"
errorKey="name"
// savedValue={price.amount}
value={name}
onChangeText={setName}
/>
</View>
{edges.map(({ cursor, node: { name } }) => (
<View key={cursor} style={{ display: "flex", flexDirection: "row"}}>
<Text>{name}</Text>
<Text>{cursor}</Text>
</View>
))}
<Button disabled={!hasMore()} onPress={() => loadMore(count, (error) => { error && console.log("error", error); })}>
Load More
</Button>
</View>
);
};
export default createPaginationContainer(
PaginatedProfilesListContainer,
{
pagination: graphql`
fragment PaginatedProfilesListContainer_list on RootQueryType {
profiles(first: $count, after: $cursor, filter: $filter)
#connection(key: "PaginatedProfilesListContainer_profiles") {
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
edges {
cursor
node {
name
}
}
}
}
`,
},
{
direction: 'forward',
query: graphql`
query PaginatedProfilesListContainerQuery(
$count: Int!
$cursor: String
$filter: ProfileFilter
) {
...PaginatedProfilesListContainer_list
}
`,
getConnectionFromProps(props) {
console.log(props)
return props.pagination?.profiles
},
getFragmentVariables(prevVars, totalCount) {
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
return {
count,
cursor,
filter: {},
};
},
}
);
I got the inspiration for this approach from https://github.com/facebook/relay/issues/1705
Note: I tried to use the #stream_connection, but the Elixir Absinthe on the backend doesn't seem tpo support it.
I know this is long so please any help appreciated :)
Index.js
import React, { Fragment, useContext, useState } from 'react';
import { useQuery } from '#apollo/client';
import { getIssuesPaginationQuery } from '#modules/CaseManager/CaseManagerDashboard/graphql';
import { Checkbox, Col, Icon, Row, Table } from 'antd';
import { removeFieldsFromObject } from '#components/helpers';
import FormattedDate from '#components/FormattedDate';
import { Link } from 'react-router-dom';
import { PermissionContext } from '#context/PermissionContext';
import { getCaseLink } from '#modules/CaseManager/CaseManagerDashboard/helpers';
import SearchInput from '#components/SearchInput';
import {
MY_CASES_FILTER_KEY,
SEARCH_FILTER_KEY,
UNASSIGNED_FILTER_KEY,
} from '#modules/CaseManager/CaseManagerDashboard/constants';
import LocalizedText from '#components/i18n/LocalizedText';
const { Column } = Table;
function CaseManagerDashboard({ match: { params: { snr } = {} } = {} }) {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [sorting, setSorting] = useState({});
const [searchValue, setSearchValue] = useState(
snr || localStorage.getItem(SEARCH_FILTER_KEY) || ''
);
const [myCasesFilter, setMyCasesFilter] = useState(
eval(localStorage.getItem(MY_CASES_FILTER_KEY))
);
const [unassignedCasesFilter, setUnassignedCasesFilter] = useState(
eval(localStorage.getItem(UNASSIGNED_FILTER_KEY))
);
const { hasAccess } = useContext(PermissionContext);
const { data, loading } = useQuery(getIssuesPaginationQuery, {
variables: {
options: {
offset: (currentPage - 1) * pageSize || 0,
limit: pageSize || 20,
searchValue,
sortBy: sorting && sorting.field,
sortOrder: sorting && sorting.order === 'ascend' ? 1 : -1,
myCases: myCasesFilter,
unassignedCases: unassignedCasesFilter,
},
},
fetchPolicy: 'no-cache',
});
const {
pagination: { items = [], totalCount = 0 } = {},
} = removeFieldsFromObject(data);
return (
<Fragment>
<Row>
<Col span={7}>
<h2>
<LocalizedText textKey="search.input.searchaccident" />
</h2>
</Col>
</Row>
<Row>
<Col span={6}>
<SearchInput
onChange={(value) => {
localStorage.setItem(SEARCH_FILTER_KEY, value);
setSearchValue(value);
}}
initialValue={searchValue}
/>
</Col>
</Row>
<Row style={{ margin: '10px 0' }}>
<Col span={24}>
<Checkbox
checked={myCasesFilter}
onChange={(e) => {
localStorage.setItem(MY_CASES_FILTER_KEY, e.target.checked);
setMyCasesFilter(e.target.checked);
}}
>
{' '}
<LocalizedText textKey="search.checkbox.mycases" />
</Checkbox>
(...)
graphql.js
port { gql } from '#apollo/client';
export const getIssuesPaginationQuery = gql`
query CaseManagerDashboard($options: CasePaginationArgsInput!) {
pagination: getIssuesPagination(options: $options) {
items {
_id
CaseNumber
CaseNumberNormalized
CreationDate
UpdateDate
AccidentDate
SinglePerson {
_id
Firstname
Lastname
Birthday
InvoiceCheck {
CreationDate
}
}
Clerks {
_id
Firstname
Lastname
}
}
totalCount
}
}
`;
constant.js
export const SEARCH_FILTER_KEY = 'CaseManagerDashboard.searchValue';
export const MY_CASES_FILTER_KEY = 'CaseManagerDashboard.myCases';
export const UNASSIGNED_FILTER_KEY = 'CaseManagerDashboard.unassignedCases';
PaginationArgs.ts
import { ArgsType, Field, InputType } from 'type-graphql'
#ArgsType()
#InputType('PaginationArgsInput')
export class PaginationArgs {
#Field({ nullable: true })
offset?: number
#Field({ nullable: true })
limit?: number
#Field({ nullable: true })
searchValue?: string
#Field({ nullable: true })
sortBy?: string
#Field({ nullable: true })
sortOrder?: number
}
CaseArgs.ts
#ArgsType()
#InputType('AssignToClerkInput')
export class AssignToClerkArgs {
#Field()
snr: string = ''
#Field(() => CopyAndAddCaseClerkInput)
clerk: CopyAndAddCaseClerkInput = new CopyAndAddCaseClerkInput()
}
#ArgsType()
#InputType('CasePaginationArgsInput')
export class CasePaginationArgs extends PaginationArgs{
#Field({ nullable: true })
myCases?: boolean
#Field({ nullable: true })
unassignedCases?: boolean
}
schema.graphql
(...)
input CasePaginationArgsInput {
limit: Float
myCases: Boolean
offset: Float
searchValue: String
sortBy: String
sortOrder: Float
unassignedCases: Boolean
}
(...)
I have a searchbar inside my index.js, which fetches results from the const getIssuesPaginationQuery.
The query contains option arguments for a case and pagination (Pagination/CaseArgs.ts) with an appropriate schema (like searchValue). I address the wanted property through keys written in a constant.js that leads to the the query named CaseManagerDashboard, in order to allow input options as explained above (input arguments).
searchValue uses the user's input (as String) in order to search within fetched data.
Patient full name works (first name & last name), same as case number, also checkboxes to filter specific data.
But when I enter a Clerk's name, I get no result, altough I received everything neccessary.
I'll attach a Screenshot of the network tab too.
Thanks in advance
Description
I have a container that has nested pagination on a file system. I recently added a caching layer to my relay environment to reduce the number of requests we are making. Opening a folder triggers a queryRender, that query fetches 8 edges at a time until the folder is fully loaded. Opening the folder on first fetch loads all the edges as expected. Closing the folder and opening it only loads the first 8 edges and fails on the second request.
I get this error: index.js:1 Warning: RelayConnectionHandler: Unexpected after cursor 'MTo3', edges must be fetched from the end of the list ('Mjow').
Adding { force: true } resolves the issue, but it causes a refetch of data that has already been requested and stored.
Alternative is to track the file count loaded and when a QueryRenderer is fired after the first load, i can pass in the loaded file count.
I'm just wondering if there is a cleaner way to achieve what I want? or does relay not support our schema the way I think it should?
Images
successful first load
First load gets 9 edges
failed second load
2nd load fails to paginate from the cache
Code
FileBrowser.js
Root query for the file browser
export default createPaginationContainer(
FileBrowser,
{
repository: graphql`
fragment FileBrowser_repository on Project {
files(
first: $first,
after: $cursor
) #connection(
key: "FileBrowser_files",
filters: [$cursor]
) #skip (if: $fileSkip) {
edges {
node {
... on ProjectFile {
id
namespace
repositoryName
key
modifiedAt
sizeBytes
downloadURL
}
... on ProjectDirectory {
id
key
}
}
}
pageInfo{
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
__typename
}`,
},
{
direction: 'forward',
getConnectionFromProps(props) {
return props.repository && props.repository.files;
},
getFragmentVariables(prevVars, first) {
return {
...prevVars,
first,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
const { namespace, repositoryName } = props;
return {
...fragmentVariables,
first: count,
cursor,
repositoryName,
namespace,
filter: [],
};
},
query: graphql`
query FileBrowserQuery(
$namespace: String!,
$repositoryName: String!,
$first: Int!,enter code here
$cursor: String,
$fileSkip: Boolean!
) {
repository(
namespace: $namespace,
repositoryName: $repositoryName
) {
...FileBrowser_repository
}
}`,
},
);
FolderQuery.js
Pagination container for folder
// #flow
// vendor
import React, { Component } from 'react';
import {
createPaginationContainer,
graphql,
} from 'react-relay';
// components
import Folder from './Folder';
// assets
import './Folder.scss';
const FolderPaginationContainer = createPaginationContainer(
Folder,
{
contents: graphql`
fragment Folder_contents on ProjectDirectory #relay(mask: false) {
id
key
contents(
first: $first,
after: $cursor,
) #connection(
key: "Folder_contents",
filters: []
) {
edges {
node {
... on ProjectFile {
id
namespace
repositoryName
key
modifiedAt
sizeBytes
}
... on ProjectDirectory {
id
key
}
}
cursor
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}`,
},
{
direction: 'forward',
getConnectionFromProps(props) {
return props.node && props.node.contents;
},
getFragmentVariables(prevVars, first) {
return {
...prevVars,
first,
};
},
getVariables(props, { count, cursor }, fragmentVariables) {
const { id } = props.node;
console.log(props, fragmentVariables);
return {
...fragmentVariables,
id,
first: count,
cursor,
filter: [],
};
},
query: graphql`
query FolderQuery(
$id: ID!,
$first: Int!,
$cursor: String,
) {
node(
id: $id,
) {
... on ProjectDirectory {
id
key
namespace
repositoryName
...Folder_contents
}
}
}`,
},
);
export default FolderPaginationContainer;
FolderSection.js
// #flow
// vendor
import React, { Component, Fragment } from 'react';
// components
import File from './File';
import RefetchFolder from './RefetchFolder';
type Props = {
namespace: string,
repositoryName: string,
sort: string,
reverse: bool,
index: number,
files: {
edges: Array<Object>,
},
query: Object,
__typename: string,
skipSort: boolean,
linkedDatasets: Object,
}
class FolderSection extends Component<Props> {
render() {
const {
namespace,
repositoryName,
sort,
reverse,
index,
files,
__typename,
query,
skipSort,
linkedDatasets,
} = this.props;
return (
<Fragment>
{ files.map((edge) => {
const {
key,
} = edge.node;
const isDir = (edge.node.__typename === `${__typename}Directory`);
const isFile = (edge.node.__typename === `${__typename}File`);
if (isDir) {
return (
<RefetchFolder
FolderSection={FolderSection}
name={key}
key={key}
edge={edge}
setState={this._setState}
rowStyle={{}}
sort={sort}
reverse={reverse}
rootFolder
index={index}
node={edge.node}
query={query}
__typename={__typename}
linkedDatasets={linkedDatasets}
namespace={namespace}
repositoryName={repositoryName}
/>
);
}
if (isFile) {
return (
<File
name={key}
key={key}
edge={edge}
isExpanded
index={index}
__typename={__typename}
/>
);
}
return (
<div>
Loading
</div>
);
})}
</Fragment>
);
}
}
export default FolderSection;
RefetchFolder.js
Contents of each folder is requested in a QueryRenderer
// #flow
// vendor
import React, { Component } from 'react';
import uuidv4 from 'uuid/v4';
import {
QueryRenderer,
graphql,
} from 'react-relay';
// environment
import environment from 'JS/createRelayEnvironment';
// component
import Folder from './FolderProjectWrapper';
type Props = {
node: {
id: String
},
relay: {
refetch: Function,
},
edge: {
node: Object
},
FolderSection: React.Node,
index: Number,
__typename: string,
sort: string,
linkedDatasets: Object,
reverse: boolean,
}
const RefetchFolderQuery = graphql`query RefetchFolderQuery(
$id: ID!,
$first: Int!,
$cursor: String,
) {
node(
id: $id,
) {
...Folder_contents #relay(mask: false)
}
}`;
class RefetchFolder extends Component<Props> {
state = {
isExpanded: false,
id: null,
progress: 0,
currentProgress: 0,
step: 0.01,
}
static getDerivedStateFromProps(props, state) {
const childNode = (props.node && props.node.contents)
? props.node
: props.edge.node;
return {
...state,
node: childNode,
};
}
/**
* #param {Object} evt
* sets item to expanded
* #return {}
*/
_expandFolder = () => {
this.setState((state) => {
const isExpanded = !state.isExpanded;
const id = (state.id === null)
? uuidv4()
: state.id;
return {
isExpanded,
id,
};
});
}
render() {
const {
FolderSection,
index,
sort,
reverse,
linkedDatasets,
} = this.props;
const { isExpanded, node, progress } = this.state;
const { __typename } = this.props;
if (!isExpanded) {
return (
<Folder
{...this.props}
FolderSection={FolderSection}
expandFolder={this._expandFolder}
node={node}
id={node.id}
index={index}
isExpanded={isExpanded}
__typename={__typename}
/>
);
}
return (
<QueryRenderer
query={RefetchFolderQuery}
environment={environment}
fetchPolicy="store-and-network"
variables={{
id: node.id,
first: 8,
cursor: null,
}}
render={({ props, error }) => {
if (props) {
const { hasNextPage } = props && props.node
&& props.node.contents && props.node.contents.pageInfo;
if (!hasNextPage) {
clearInterval(this.timeout);
}
return (
<Folder
FolderSection={FolderSection}
expandFolder={this._expandFolder}
node={props.node}
index={index}
isExpanded={isExpanded}
sort={sort}
reverse={reverse}
{...props}
__typename={__typename}
isLoading={hasNextPage}
progress={progress}
linkedDatasets={linkedDatasets}
/>
);
}
if (error) {
return (
<div>Error</div>
);
}
return (
<Folder
FolderSection={FolderSection}
expandFolder={this._expandFolder}
node={node}
index={index}
isExpanded={isExpanded}
{...this.props}
__typename={__typename}
isLoading
progress={progress}
linkedDatasets={linkedDatasets}
/>
);
}}
/>
);
}
}
export default RefetchFolder;
Folder.js
// vendor
import React, { Component } from 'react';
import classNames from 'classnames';
// ga
import ga from 'JS/ga';
// components
import LinkedDataset from './LinkedDataset';
// assets
import './Folder.scss';
type Props = {
namespace: string,
repositoryName: string,
index: number,
node: Object,
relay: {
refetchConnection: Function,
environment: Object,
loadMore: Function,
isLoading: Function,
},
expandFolder: Function,
isExpanded: Boolean,
FolderSection: React.node,
__typename: string,
}
class Folder extends Component<Props> {
componentDidMount() {
const { node } = this.props;
if (node.contents && node.contents.pageInfo && node.contents.pageInfo.hasNextPage) {
this._loadMore();
}
}
componentDidUpdate() {
const { node } = this.props;
if (node.contents && node.contents.pageInfo && node.contents.pageInfo.hasNextPage) {
this._loadMore();
}
}
/**
* #param {string} key
* #param {string || boolean} value - updates key value in state
* update state of component for a given key value pair
* #return {}
*/
_setState = (key, value) => {
this.setState({ [key]: value });
}
/**
* #param {}
* refetches component looking for new edges to insert at the top of the activity feed
* #return {}
*/
_loadMore = () => {
const { loadMore, isLoading } = this.props.relay;
if (!isLoading()) {
loadMore(
8,
() => {},
// { force: true } commented out because it causes the cache to be ignored, but
// fixes the issue
);
}
}
/**
* #param {Object} evt
* sets item to expanded
* #return {}
*/
_refetch = () => {
const { refetchConnection } = this.props.relay;
this.setState((state) => {
const isExpanded = !state.isExpanded;
return {
isExpanded,
};
});
refetchConnection(
8,
() => {
},
{
id: this.props.node.id,
},
);
}
render() {
const {
namespace,
repositoryName,
index,
node,
expandFolder,
isExpanded,
FolderSection,
__typename,
} = this.props;
const newIndex = index + 2;
const { key } = node;
const folderName = getName(key);
// declare css here
const folderRowCSS = classNames({
Folder__row: true,
});
const folderChildCSS = classNames({
Folder__child: true,
hidden: !isExpanded,
});
const folderNameCSS = classNames({
'Folder__cell Folder__cell--name': true,
'Folder__cell--open': isExpanded,
});
return (
<div className="Folder relative">
<div
className={folderRowCSS}
onClick={evt => expandFolder(evt)}
role="presentation"
>
<div className={folderNameCSS}>
<div className="Folder__icon" />
<div className="Folder__name">
{folderName}
</div>
</div>
<div className="Folder__cell Folder__cell--size" />
<div className="Folder__cell Folder__cell--date" />
</div>
<div className={folderChildCSS}>
<FolderSection
namespace={namespace}
repositoryName={repositoryName}
files={node.contents}
node={node}
index={newIndex}
linkedDatasets={linkedDatasets}
__typename={__typename}
/>
</div>
</div>
);
}
}
export default Folder;
On the frontend, I am using ReactJS and trying to build-in a filtering option to a list view. The list view correctly getting data from graphql endpoint by issuing this graphql query:
query getVideos($filterByBook: ID, $limit: Int!, $after: ID) {
videosQuery(filterByBook: $filterByBook, limit: $limit, after: $after) {
totalCount
edges {
cursor
node {
id
title
ytDefaultThumbnail
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
On the initial load $filterByBook variable is set to null, so the query correctly returns all pages for all nodes (query returns a paginated result). Then, by clicking on the filter (filter by book) another graphql query is issuing, but it always returns the same data. Here is a code snippet for filtering component
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
And, here is complete code without imports for the list view component
class VideoList extends React.Component {
constructor(props) {
super(props);
this.subscription = null;
}
componentWillUnmount() {
if (this.subscription) {
// unsubscribe
this.subscription();
}
}
renderVideos() {
const { videosQuery } = this.props;
return videosQuery.edges.map(({ node: { id, title, ytDefaultThumbnail } }) => {
return (
<Col sm="4" key={id}>
<Card>
<CardImg top width="100%" src={ytDefaultThumbnail} alt="video image" />
<CardBlock>
<CardTitle>
<Link
className="post-link"
to={`/video/${id}`}>
{title}
</Link>
</CardTitle>
</CardBlock>
</Card>
</Col>
);
});
}
renderLoadMore() {
const { videosQuery, loadMoreRows } = this.props;
if (videosQuery.pageInfo.hasNextPage) {
return (
<Button id="load-more" color="primary" onClick={loadMoreRows}>
Load more ...
</Button>
);
}
}
renderFilters() {
const { listOfBooksWithChapters, refetch } = this.props;
return (
<Row>
<FilterBooks
onBookTitleClickParam={(onBookTitleClickParam) => {
return refetch({
variables: {
limit: 3,
after: 0,
filterByBook: onBookTitleClickParam
}
})
}}
listOfBooksWithChapters={listOfBooksWithChapters}
/>
</Row>
)
}
render() {
const { loading, videosQuery } = this.props;
if (loading && !videosQuery) {
return (
<div>{ /* loading... */}</div>
);
} else {
return (
<div>
<Helmet
title="Videos list"
meta={[{
name: 'description',
content: 'List of all videos'
}]} />
<h2>Videos</h2>
{this.renderFilters()}
<Row>
{this.renderVideos()}
</Row>
<div>
<small>({videosQuery.edges.length} / {videosQuery.totalCount})</small>
</div>
{this.renderLoadMore()}
</div>
);
}
}
}
export default compose(
graphql(VIDEOS_QUERY, {
options: () => {
return {
variables: {
limit: 3,
after: 0,
filterByBook: null
},
};
},
props: ({ data }) => {
const { loading, videosQuery, fetchMore, subscribeToMore, refetch } = data;
const loadMoreRows = () => {
return fetchMore({
variables: {
after: videosQuery.pageInfo.endCursor,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const totalCount = fetchMoreResult.videosQuery.totalCount;
const newEdges = fetchMoreResult.videosQuery.edges;
const pageInfo = fetchMoreResult.videosQuery.pageInfo;
return {
videosQuery: {
totalCount,
edges: [...previousResult.videosQuery.edges, ...newEdges],
pageInfo,
__typename: "VideosQuery"
}
};
}
});
};
return { loading, videosQuery, subscribeToMore, loadMoreRows, refetch };
}
}),
graphql(LIST_BOOKS_QUERY, {
props: ({ data }) => {
const { listOfBooksWithChapters } = data;
return { listOfBooksWithChapters };
}
}),
)(VideoList);
Question:
Why refetch function returns data without taking into account new variable filterByBook? How to check which variables object I supplied to the refetch function? Do I need to remap data that I receive from refetch function back to the component props?
EDIT:
I found the way to find what variable object I supplied to the query and found that variable object on filtering event returns this data
variables:Object
after:0
limit:3
variables:Object
after:0
filterByBook:"2"
limit:3
you can do it by adding refetch in useQuery
const {loading, data, error,refetch} = useQuery(GET_ALL_PROJECTS,
{
variables: {id: JSON.parse(localStorage.getItem("user")).id}
});
then call function refetch()
const saveChange = input => {
refetch();
};
OR
const saveChange = input => {
setIsOpen(false);
addProject({
variables: {
createBacklogInput: {
backlogTitle: backlogInput,
project:id
}
}
}).then(refetch);
It seem that refetch function is not meant to refetch data with different variables set (see this discussion).
I finally and successfully solved my issue with the help from this article