I'm not clear at all about how readQuery and writeQuery work in the context of the update function passed to a mutation function in react-apollo.
It seems that readQuery returns a reference to the cache, mutating the returned object directly triggers a rerender across my components with the relevant changes, why then do I need to call writeQuery?
Example directly from the docs (https://www.apollographql.com/docs/react/features/optimistic-ui.html) of an update function passed into mutate :
update: (proxy, { data: { submitComment } }) => {
// Read the data from our cache for this query.
const data = proxy.readQuery({ query: CommentAppQuery });
// Add our comment from the mutation to the end.
data.comments.push(submitComment);
// Write our data back to the cache.
proxy.writeQuery({ query: CommentAppQuery, data });
}
now if I comment out the last line :
update: (proxy, { data: { submitComment } }) => {
// Read the data from our cache for this query.
const data = proxy.readQuery({ query: CommentAppQuery });
// Add our comment from the mutation to the end.
data.comments.push(submitComment);
// Write our data back to the cache.
// proxy.writeQuery({ query: CommentAppQuery, data });
}
My component and UI would change as soon as the data returned from proxy.readQuery is mutated. What added benefit is there to calling proxy.writeQuery?
Initially I thought proxy.writeQuery would be necessary to notify React of the changes and trigger a re-render to my components, but evidently this is not the case.
Related
I have a question concerning Apollo Client's behaviour when dispatching an update mutation.
I have a little application that fetches data and allows you to modify it. After the modification, an update mutation is sent to graphQL. The changes can be seen instantly on the UI since the update of a single item triggers an automatic cache update by Apollo.
However, I noticed that when I refresh the page after an update, the order of the items I previously fetched is changed with the recently updated item going at the end of the list.
I was just wondering if this is the normal behaviour to expect and if there was a way to force the cache to keep the same order after an update?
Edit: Here's the code for my resolver, mutation and useMutation call.
Resolver:
async UpdateUser(parent, args, ctx, info) {
const { id, input } = args;
const updatedUser = await ctx.prisma.user.update({
where: {
id,
},
data: {
...input,
},
});
return updatedUser;
}
Mutation:
export const UPDATE_USER_MUTATION = gql`
mutation UpdateUser($id: String, $input: CreateUserInput) {
UpdateUser(id: $id, input: $input) {
id
name
email
}
}
`;
useMutation:
UpdateField({
variables: {
id: data.fieldID,
input: {
[data.fieldName]: value,
},
},
});
Edit 2: Here's a gif what's going on..
Thank you!
Mutation update option can update (or insert) properly list/array (query cached result) following BE sorting ...
... but it will fail on longer datasets, paginated results - on list query refetch record can be removed from current [page] list/array. It will be more confusing behavior.
IMHO fighting is not worth the effort, it's already acceptable behavior (mutation of indexed/ordering field).
Possible solutions:
don't refetch list query, only update the query list cache (mutation update - it will update the list view);
for small datasets, use FE sortable table component (BE order doesn't matter).
My database stores a list of items. I've written a query to randomly return one item from the list each time a button is clicked. I use useLazyQuery and call the returned function to execute the query and it works fine. I can call the refetch function on subsequent button presses and it returns another random item correctly.
The problem comes when i try to pass variables to the query. I want to provide different criteria to the query to tailor how the random choice is made. Whatever variables I pass to the first call are repeated on each refetch, even though they have changed. I can trace that they are different on the client but the resolver traces the previous variables.
// My query
const PICK = gql`
query Pick($options: PickOptionsInput) {
pick(options: $options) {
title
}
}
`;
// My lazy hook
const [pick, { data, refetch }] = useLazyQuery(PICK, {
fetchPolicy: "no-cache",
});
// My button
<MyButton
onPick={(options) =>
(refetch || pick)({ // Refetch unless this is the first click, then pick
variables: {
options // Options is a custom object of data used to control the pick
},
})
}
/>
Some things I've tried:
Various cache policies
Not using an object for the options and defining each possible option as a different variable
I'm really stumped. It seems like the docs say new variables are supposed to be used if they are provided on refetch. I don't think the cache policy is relevant...I'm getting fresh results on each call, it's just the input variables that are stale.
I guess only pick is called ...
... because for <MyBytton/> there is no props change, no onPick redefined - always the first handler definition used (from first render) and options state from the same time ...
Try to pass all (options, pick and refetch) as direct props to force rerendering and handler redefinition:
<MyButton
options={options}
pick={pick}
refetch={refetch}
onPick={(options) =>
(refetch || pick)({ // Refetch unless this is the first click, then pick
variables: {
options // Options is a custom object of data used to control the pick
},
})
}
/>
... or [better] define handler before passing it into <MyButton/>:
const pickHandler = useCallback(
(options) => {
if(refetch) {
console.log('refetch', options);
refetch( { variables: { options }});
} else {
console.log('pick', options);
pick( { variables: { options }});
}
},
[options, pick, refetch]
);
<MyButton onPick={pickHandler} />
I want to wait to apply state updates from the back-end if a certain animation is currently running. This animation could run multiple times depending on the game scenario. I'm using react-native with hooks and firestore.
My plan was to make an array that would store objects of the incoming snapshot and the function which would use that data to update the state. When the animation ended it would set that the animation was running to false and remove the first item of the array. I'd also write a useEffect, which would remove the first item from the array if the length of the array had changed.
I was going to implement this function by checking whether this animation is running or whether there's an item in the array of future updates when the latest snapshot arrives. If that condition was true I'd add the snapshot and the update function to my array, otherwise I'd apply the state update immediately. I need to access that piece of state in all 3 of my firestore listeners.
However, in onSnapshot if I try to access my state it'll give me the initial state from when the function rendered. The one exception is I can access the state if I use the function to set the state, in this case setPlayerIsBetting and access the previous state through the function passed in as a callback to setPlayerIsBetting.
I can think of a few possible solutions, but all of them feel hacky besides the first one, which I'm having trouble implementing.
Would I get the future state updates if I modify the useEffect for the snapshots to not just run when the component is mounted? I briefly tried this, but it seems to be breaking the snapshots. Would anyone know how to implement this?
access the state through calling setPlayerIsBetting in all 3 listeners and just set setPlayerIsBetting to the previous state 99% of the time when its not supposed to be updated. Would it even re-render if nothing is actually changed? Could this cause any other problems?
Throughout the component lifecycle add snapshots and the update functions to the queue instead of just when the animation is running. This might not be optimal for performance right? I wouldn't have needed to worry about it for my initial plan to make a few state updates after an animation runs since i needed to take time to wait for the animation anyway.
I could add the state I need everywhere on the back-end so it would come in with the snapshot.
Some sort of method that removes and then adds the listeners. This feels like a bad idea.
Could redux or some sort of state management tool solve this problem? It would be a lot of work to implement it for this one issue, but maybe my apps at the point where it'd be useful anyway?
Here's my relevant code:
const Game = ({ route }) => {
const [playerIsBetting, setPlayerIsBetting] = useState({
isBetting: false,
display: false,
step: Infinity,
minimumValue: -1000000,
maximumValue: -5000,
});
const [updatesAfterAnimations, setUpdatesAfterAnimations] = useState([]);
// updatesAfterAnimations is currently always empty because I can't access the updated playerIsBetting state easily
const chipsAnimationRunningOrItemsInQueue = (snapshot, updateFunction) => {
console.log(
"in chipsAnimationRunningOrItemsInQueue playerIsBetting is: ",
playerIsBetting
); // always logs the initial state since its called from the snapshots.
// So it doesn't know when runChipsAnimation is added to the state and becomes true.
// So playerIsBetting.runChipsAnimation is undefined
const addToQueue =
playerIsBetting.runChipsAnimation || updatesAfterAnimations.length;
if (addToQueue) {
setUpdatesAfterAnimations((prevState) => {
const nextState = cloneDeep(prevState);
nextState.push({ snapshot, updateFunction });
return nextState;
});
console.log("chipsAnimationRunningOrItemsInQueue returns true!");
return true;
}
console.log("chipsAnimationRunningOrItemsInQueue returns false!");
return false;
};
// listener 1
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
const unsubscribeFromPlayerCards = db
.collection("tables")
.doc(tableId)
.collection("players")
.doc(player.uniqueId)
.collection("playerCards")
.doc(player.uniqueId)
.onSnapshot(
function (cardsSnapshot) {
if (!chipsAnimationRunningOrItemsInQueue(cardsSnapshot, updatePlayerCards)) {
updatePlayerCards(cardsSnapshot);
}
},
function (err) {
// console.log('error is: ', err);
}
);
return unsubscribeFromPlayerCards;
}, []);
};
// listener 2
useEffect(() => {
const tableId = route.params.tableId;
const db = firebase.firestore();
const unsubscribeFromPlayers = db
.collection("tables")
.doc(tableId)
.collection("players")
.onSnapshot(
function (playersSnapshot) {
console.log("in playerSnapshot playerIsBetting is: ", playerIsBetting); // also logs the initial state
console.log("in playerSnapshot playerIsBetting.runChipsAnimation is: "playerIsBetting.runChipsAnimation); // logs undefined
if (!chipsAnimationRunningOrItemsInQueue(playersSnapshot, updatePlayers)) {
updatePlayers(playersSnapshot);
}
},
(err) => {
console.log("error is: ", err);
}
);
return unsubscribeFromPlayers;
}, []);
// listener 3
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
// console.log('tableId is: ', tableId);
const unsubscribeFromTable = db
.collection("tables")
.doc(tableId)
.onSnapshot(
(tableSnapshot) => {
if (!chipsAnimationRunningOrItemsInQueue(tableSnapshot, updateTable)) {
updateTable(tableSnapshot);
}
},
(err) => {
throw new err();
}
);
return unsubscribeFromTable;
}, []);
I ended up not going with any of the solutions I proposed.
I realized that I could access the up to date state by using a ref. How to do it is explained here: (https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559) And this is the relevant code sample from that post: (https://codesandbox.io/s/event-handler-use-ref-4hvxt?from-embed)
Solution #1 could've worked, but it would be difficult because I'd have to work around the cleanup function running when the animation state changes. (Why is the cleanup function from `useEffect` called on every render?)
I could work around this by having the cleanup function not call the function to unsubscribe from the listener and store the unsubscribe functions in state and put them all in a useEffect after the component mounts with a 2nd parameter that confirmed all 3 unsubscribe functions had been added to state.
But if a user went offline before those functions were in state I think there could be memory leaks.
I would go with solution #1: In the UseEffect() hooks you could put a boolean flag in so the snapshot listener is only set once per hook. Then put the animation state property in the useEffect dependency array so that each useEffect hook is triggered when the animation state changes and you can then run whatever logic you want from that condition.
Using useLazyQuery() hooks from #apollo/react-hooks I was able to execute a query on click of a button. But I cannot use it execute same query on consecutive clicks.
export default ({ queryVariables }) => {
const [sendQuery, { data, loading }] = useLazyQuery(GET_DIRECTION, {
variables: queryVariables
});
return <div onClick={sendQuery}>Click here</div>;
}
In the above the sendQuery executes only once.
useLazyQuery uses the default network policy that of cache-first So I supposed your onClick function actually executes but because the returned value is what was in the cache, React notices no change in data as such the state is not updated since the returned data is what it already has that way no re-render and thing seem not to have changed. I suggest you should pass in a different network policy something like
const [sendQuery, { data, loading }] = useLazyQuery(GET_DIRECTION, {
variables: queryVariables,
fetchPolicy: "network-only"
});
This will mean you want the most recent information from your api hence basically no caching.
You might also want to experiment on other option and see which one best suits you
like cache-and-network: you can find out a little more here understanding-apollo-fetch-policies
As per the docs, useLazyQuery accepts the parameter fetchPolicy.
const [lazyQuery, { data, loading }] = useLazyQuery(QUERY, {
fetchPolicy: 'no-cache'
});
With fetchPolicy set to no-cache, the query's result is not stored in the cache.
I'm passing some params from a page to the other to make an API call. What i've notice is when i hard code the value i'm suppose to get into the http call, i get the desired results but when i inject the value from the params into the call, if fails to work. so i thought the params wasn't able to pass till i did an alert in the render function and what i realized was the alert prompts twice, the first one is empty and the second prompt brings the value from the previous page, so then meaning in my componentDidMount, the call
state = {
modalVisible: false,
posts : [],
itemID: ''
}
componentDidMount = () => {
const { navigation } = this.props;
this.setState({itemID : navigation.getParam('itemID')})
api.get('/items/'+`${this.state.itemID}`+'/test_items_details/'+`${userID}`+'/posts').then((response) => {
let data = response.data.data
this.setState({posts: data})
console.log(JSON.stringify(this.state.posts))
})
}
As per the docs, it states
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Your code snippet is trying to access the state variable before it has in fact been updated.
You can do two things here:
Simply use navigation.getParam('itemID') so now your URL becomes /items/${navigation.getParam('itemID')}/test_item_details/${userID}/posts.
Use the setState callback, the snippet is added below.
Snippet:
this.setState({ itemID: navigation.getParam('itemID') }, () => {
// this.state.itemID is now set, you can make your API call.
});
If your only intention to use this.state.itemID is for the URL, I would opt for the first option.