uswSWRInfinite shows stale data when mutating with local data - reactjs

I use uswSWRInfinite to paginate data by a cursor (the id of the last loaded item). When I edit or delete items on page 2+ (for some reason, this doesn't happen on page 1), and I mutate by passing modified data, after revalidation, I see the old data again.
This does not happen if I either 1. mutate without local data (only revalidation) or 2. disable revalidation (only mutate with local data). Both together cause the bug.
Here's the relevant code:
useSWRInfinite setup:
const {
data: repliesPages,
size: repliesPagesSize,
setSize: setRepliesPagesSize,
isLoading: repliesLoading,
error: repliesLoadingError,
mutate: mutateRepliesPages,
} = useSWRInfinite(
getPageKey,
([commentId, lastReplyId]) => BlogApi.getRepliesForComment(commentId, lastReplyId));
The update/delete callbacks:
replies?.map(reply => (
<CommentBody
comment={reply}
onReplyCreated={addLocalReply}
key={reply._id}
onCommentUpdated={(updatedReply) => {
const updatedRepliesPages = repliesPages?.map(page => {
const updatedReplies = page.comments.map(existingReply => existingReply._id === updatedReply._id ? updatedReply : existingReply);
const updatedPage: GetCommentsResponse = { ...page, comments: updatedReplies };
return updatedPage;
});
mutateRepliesPages(updatedRepliesPages); // this works properly if I don't pass data or set revalidate : false
}}
onCommentDeleted={() => {
const updatedRepliesPages = repliesPages?.map(page => {
const updatedReplies = page.comments.filter(existingReply => existingReply._id !== reply._id);
const updatedPage: GetCommentsResponse = { ...page, comments: updatedReplies };
return updatedPage;
});
mutateRepliesPages(updatedRepliesPages); // this works properly if I don't pass data or set revalidate : false
}}
/>
));
The callbacks are triggered after we got the updated item back from the server:
async function onSubmit({ text }: { text: string }) {
if (!text) return;
try {
const updatedComment = await BlogApi.updateComment(comment._id, text);
onCommentUpdated(updatedComment);
} catch (error) {
console.error(error);
alert(error);
}
}
async function deleteComment() {
try {
setDeleteInProgress(true);
await BlogApi.deleteComment(comment._id);
onCommentDeleted();
} catch (error) {
console.error(error);
alert(error);
} finally {
setDeleteInProgress(false);
}
}
Here's a recording of the problem happening:
The behavior I expect, is that SWR shows the updated data after revalidation.

You need to fire a request to update the data on the server (BlogApi).
mutate() will update the data on the client side, but not on the server. You're updating the data locally, then the data revalidates (refetches), which replaces the local data with the server data, undoing your updates.
Add the appropriate request (probably POST) to your code. You can add it immediately before mutateRepliesPage(), or you can include it as part of the function passed to mutate's second argument like in this example.

Related

useState not updating rendered values after getting document snapshots

I am having a problem assigning data to useState by fetching data using reference type value from firebase.
const [preOil, setPreOil] = useState([]);
const [oilChange, setOilChange] = useState([]);
useEffect(() => {
getDocs(query(collection(db, "oil"), orderBy("timestamp"))).then(
(snapshot) => {
setPreOil(
snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}))
);
}
);
}, []);
useEffect(() => {
let current = preOil.length > 0 ? [...preOil] : [];
current.map((_oil, i) => {
getDoc(_oil.oilReference).then((oilRef) => {
current[i].oilReference = oilRef.data();
});
});
setOilChange(current);
}, [preOil]);
In the first useEffect, the data is fetched successfully in this form,
preOil = {
id:"RxbAOAIs8d3kGOHNskJ4",
oilReference: Ta {converter: null, _key: ut, type: 'document', firestore: Na},
customerName: "USAMA",
}
In the second useEffect based on [preOil], I need to reassign the oilReference with the data fetched from firestorm through its reference(oilReference), The data is successfully fetched from the database and assigned to current but The main problem is when I set to state setOilChange(current) it updates my oilChange state when I inspect in inspect tools in chrome but in JSX the changes don't reflect
I am updating the state in useEffect
I am having desired data assigned in a local variable and assign that variable to the state
Then What am I missing?
In your second useEffect(), more specifically, in
current.map((_oil, i) => {
getDoc(_oil.oilReference).then((oilRef) => {
current[i].oilReference = oilRef.data();
});
});
setOilChange(current);
You are mutating the content of the current variable. This mutation, because it is async, will happen after the setOilChange call. Such mutation will thus not trigger a re-render.
What you need is to instead first wait for all the docs to be loaded and only after that set the state. Example:
const _docs = current.map((_oil, i) => {
return getDoc(_oil.oilReference).then((oilRef) => { // changed here
return { // changed here
...current[i], // changed here
oilReference: oilRef.data() // changed here
} // changed here
}); // changed here
});
Promise.all(_docs).then(() => {
setOilChange(_docs);
});
Also notice I didn't mutate current, rather I returned a new object. This is not mandatory but just a best practice overall.

store data in firestore when Browser Tab is closed or the route is changed (react JS)

const handleDraftContracts = async () => {
console.log('/bruhhhhhhandleDraftContract');
const paragraphRef: string | any = document.getElementById('contract');
const contractDetails = {
contractName: 'House Rental',
states: {
amount: amount,
},
content: paragraphRef?.textContent,
};
await makeDraftContract(contractDetails);
};
useEffect(() => {
console.log('///////I am hreeeee');
window.addEventListener('onbeforeunload', (env) => {
handleDraftContracts();
});
return () => {
console.log('///////removing');
window.removeEventListener('onbeforeunload', handleDraftContracts);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
firestore.js
// make Draft Contracts
export async function makeDraftContract(contractDetails: object | any) {
try {
console.log("making a draft contract", contractDetails);
const draftContractRef: any = collection(db,"makeDraftContracts");
let contract = await addDoc(draftContractRef, contractDetails);
console.log("./////////makeDraftContract", contract);
} catch (error) {
console.log('////errror in contract Hanlder', error);
}
}
I want to call my handleDraftContracts method whenever user closes the tab or changes the route. I am using onbeforeunload event. The handleDraftContracts is getting called but the tab unloads before Firestore could update the collection. How can I get around this that as the user closes the tab or move to a new route, my firestore method get executed first then the tab gets unloaded ?
Try with Beacon api
https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API
as 'onbeforeunload' cannot make sure you request to server has been made and requests can slow down the browser
componentWillUnmount is like that one, cannot to make long running script.

Saving an ID value from an API to a User with GraphQL

I'm working on a video game website where a user can save a game to a list. How this is supposed to work is when the user clicks "Complete Game", the ID of the game is saved to a state that holds the value. The value is then passed into the mutation, then the mutation runs, saving the ID of the game to the users list of completed games. However, all I'm seeing in the console is this:
"GraphQLError: Variable \"$addGame\" got invalid value { gameId: 740, name: \"Halo: Combat Evolved\",
The above error continues, listing the entirety of the API response, instead of just the gameId.
I was able to successfully add the game to the list in the explorer with the following mutation:
mutation completeGame($addGame: AddNewGame!) {
completeGame(addGame: $addGame) {
_id
completedGameCount
completedGames {
gameId
}
}
}
with the following variable:
{
"addGame": {"gameId": 740}
}
How can I trim down what is being passed into the mutation to just be the gameId?
Below is the entirety of the page, except the return statement at the bottom.
const [selectedGame, setSelectedGame] = useState([]);
const [savedGameIds, setSavedGameIds] = useState(getSavedGameIds());
const [completeGame, { error }] = useMutation(COMPLETE_GAME);
const { id: gameId } = useParams();
useEffect(() => {
return () => saveGameIds(savedGameIds);
});
useEffect(() => {
async function getGameId(gameId) {
const response = await getSpecificGame(gameId);
if (!response.ok) {
throw new Error('Something went wrong...');
}
const result = await response.json();
const gameData = result.map((game) => ({
gameId: game.id,
name: game.name,
cover: game.cover,
summary: game.summary,
platforms: game.platforms,
platformId: game.platforms,
genres: game.genres,
genreId: game.genres,
}));
setSelectedGame(gameData);
}
getGameId(gameId);
}, [])
const handleCompleteGame = async (gameId) => {
const gameToComplete = selectedGame.find((game) => game.gameId === gameId);
const token = Auth.loggedIn() ? Auth.getToken() : null;
if (!token) {
return false;
}
try {
const { data } = await completeGame({
variables: { addGame: { ...gameToComplete } },
});
console.log(data);
setSavedGameIds([...savedGameIds, gameToComplete]);
} catch (err) {
console.error(err);
}
};
With the mutation working in the explorer when I'm able to explicitly define the variable, I am led to believe that the issue is not with the resolver or the typedef, so I'm going to omit those from this post because I don't want it to get too long.
However, I'd be happy to attach any extra code (resolver, typeDef, getSavedGameIds function, etc) if it would allow anyone to assist. The issue (I think) lies in getting my response to match the syntax I used in the explorer, which means trimming down everything except the gameId.
I specifically am extremely suspicious of this line
const gameToComplete = selectedGame.find((game) => game.gameId === gameId)
but I have fiddled around with that for awhile to no avail.
Thank you to anyone who is able to help!
It sounds like you're trying to pass more into your mutation then your schema is defined to allow. In this part:
const { data } = await completeGame({
variables: { addGame: { ...gameToComplete } },
});
You're spreading gameToComplete here which means everything in the gameToComplete object is going to be sent as a variable. If your schema is setup to just expect gameId to be passed in, but your error message is showing that name is also being passed in, you just need to adjust your variables to exclude everything you can't accept. Try:
const { data } = await completeGame({
variables: { addGame: { gameId } },
});

What is the correct way to call updateCachedData on a click event in a component that uses the RTKQ query?

I can only think of storing a reference to updateCachedData somewhere globally and use it in that click event but I am not sure this is the React way of doing this.
I have a notifications feed built with a Socket.IO server.
By clicking on a notification it should get deleted from the list. (The list should show only unread notifications.)
But when deleting from the list I create a new array as state in the notifications pane.
When I receive a new notification, all the deleted notifications return back - this is not what I intended.
How can I change the cache entry, more precisely remove items from it without remaking the request for all the notifications?
There are no error messages.
Code
getNotifications: build.query<
IDbNotification[],
IGetNotificationsQueryParams
>({
query: (params: IGetNotificationsQueryParams) => ({
url: `notifications?authToken=${params.authToken || ""}&limit=${
params.limit
}&userId=${params.userId || ""}${
params.justUnread ? "&justUnread" : ""
}`,
method: "GET"
}),
keepUnusedDataFor: 0,
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const { myIo, connectHandler } = getWebSocketConnection(
"notifications",
clone({
subscribtions: arg.userId
? getFollowedUserIds().concat({
uid: arg.userId,
justUnread: arg.justUnread
})
: getFollowedUserIds()
})
);
const listener = (eventData: IDbNotification) => {
if (
(eventData as any).subscriber === arg.userId &&
(!arg.justUnread || typeof eventData.readDateTime === "undefined")
) {
updateCachedData(draft => {
draft.unshift(eventData);
if (draft.length > arg.limit) {
draft.pop();
}
});
}
};
try {
await cacheDataLoaded;
myIo.on("notifications", listener);
} catch {}
await cacheEntryRemoved;
myIo.off("notifications", listener);
myIo.off("connect", connectHandler);
}
})
You can use updateQueryData - updateCachedData is just a shortcut for the current cache entry for convenience.
dispatch(
api.util.updateQueryData('getNotifications', arg, (draft) => {
// change it here
})
)
See this for more context: https://redux-toolkit.js.org/rtk-query/usage/optimistic-updates

component mounting before fetch is complete, resulting in failed propTypes

I have a tour website. The basic structure here is that I have the tour page, and on the tour page is a component that handles booking times. When mounted, TourPage fetches the available start times from the db and ships that data down as props.
I am able to receive this data and send it down, but the deeper component gives back a propTypes error, saying the data is undefined. When I console log out (from parent) the value of this data, it logs 3 times: first, it is an empty array. The second and third time, the data is there.
Odd thing is that the propType error appears chronologically AFTER my console.log showing the actual data. I have also added logic to the TourPage to return a Spinner if the availableStartTimes array is empty. I fully expect that no children components will be mounted until all data has arrived. This does not seem to be the case.
Here is some relevant code (edited for brevity):
const ExperienceDetailContainer = () => {
//a function that cleans the fetched data. returns an array
const availableStartTimes = getTimesForCurrentDate(experienceDate, detail.availabilities)
//fetch request to get available start times
const getExperienceAvailabilities = () => {
api.post('/GetExperienceAvailabilities', { experienceid: experienceId })
.then(result => {
const availabilities = result.d
updateExperience(availabilities)
})
.catch(err => {
console.error(err)
})
}
useEffect(() => {
getExperienceAvailabilities()
}, [])
if (availableStartTimes.length < 1) {
return <Spinner />
}
return <ExperienceDetail {...{ availableStartTimes, experienceDate }} />
const mapStateToProps = state => ({
detail: fromResource.getDetail(state, 'experience'),
experienceDate: state.userSelections.experienceDate,
})
const mapDispatchToProps = (dispatch) => ({
readDetail: (apiRequestParams) => dispatch(resourceDetailReadRequest('experience', { apiRequestParams })),
updateExperience: (availabilities) => dispatch(updateExperience('experience', availabilities))
})
here is getTimesForCurrentDate helper function:
export const getTimesForCurrentDate = (date, availabilities) => {
if (!availabilities) {
return []
}
const rawTimes = availabilities[formattedDateWithoutTime(date)]
const properTimes = rawTimes.map(rawTime => {
return convertTimeFromRawToProper(rawTime)
})
return properTimes
}
here is the propTypes for BookCard component
BookCard.propTypes = {
availableStartTimes: T.arrayOf(T.string).isRequired,
}
and the reducer for updateExperience
case UPDATE_EXPERIENCE:
return {
...state,
experience: {
...getResourceState(state, resource),
detail: {
...getDetail(state, resource),
availabilities: payload
}
}
}
here is a screenshot of my console to demonstrate the 3 outputs of availableStartTimes, as well as a console.log(availableStartTimes.toString()) placed after the Spinner guard if statement

Resources