Redux Query Dependent Mutations With Rollback Using createApi() - reactjs

I have two mutations that need to happen one after another if the first one succeeds. As bonus I would like to undo the first mutation if the second fails.
I have the first part working but it feels clumsy and I'm wondering if there is better way.
Here is how I bring in my two mutations:
const [updateSubscription, {
isLoading: isLoadingUpdateSubscription,
isSuccess: isSuccessUpdatedSubscription,
isError: isErrorUpdateSubscription,
error: errorUpdateSubscription,
reset: resetUpdateSubscription
}] = useUpdateSubscriptionMutation();
const [updateDeviceListing, {
isLoading: isLoadingUpdateDeviceListing,
isSuccess: isSuccessUpdatedDeviceListing,
isError: isErrorUpdateDeviceListing,
error: errorUpdateDeviceListing,
reset: resetUpdateDeviceListing
}] = useUpdateDeviceListingMutation();
As part of a button click I run the first mutation where a subscription is updated:
const handleListIt = () => {
if (deviceListing && subscriptions) {
const updatedAvailableSeats = subscriptions[0].availableSeats - deviceListing.count;
const updatedUsedSeats = subscriptions[0].usedSeats + deviceListing.count;
updateSubscription({...subscriptions[0], availableSeats: updatedAvailableSeats, usedSeats: updatedUsedSeats});
}
};
I then use useEffect() to check isSuccessUpdatedSubscription and run the second mutation:
useEffect(() => {
if (isSuccessUpdatedSubscription && deviceListing) {
updateDeviceListing({...deviceListing, status: 'open'})
}
if (isSuccessUpdatedDeviceListing) {
onClosed();
}
}, [isSuccessUpdatedDeviceListing, isSuccessUpdatedSubscription, deviceListing, onClosed, updateDeviceListing]);
The same useEffect() is also used to check if the second mutation worked, isSuccessUpdatedDeviceListing, at which point onClosed() is called and the user is shown some different UI.

You can just handle both in handleListIt, there is really no good reason for the re-render with the useEffect. And then you can also handle the rollback as you want.
const handleListIt = async () => {
if (deviceListing && subscriptions) {
const updatedAvailableSeats = subscriptions[0].availableSeats - deviceListing.count;
const updatedUsedSeats = subscriptions[0].usedSeats + deviceListing.count;
try {
await updateSubscription({...subscriptions[0], availableSeats: updatedAvailableSeats, usedSeats: updatedUsedSeats}).unwrap();
await updateDeviceListing({...deviceListing, status: 'open'}).unwrap()
} catch (error) {
// do your rollback
}
}
};

Related

uswSWRInfinite shows stale data when mutating with local data

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.

Stop useEffect if a condition is met

I have a useEffect set up how I thought would only run once on initial render but it continues to rerun.
This breaks a function that is supposed to set a piece of state to true if a condition is truthy and show appropriate UI.
This sort of works but then the useEffect runs again flicks back to false immediately. I am also using a use effect to check on first render if the condition is truthy and show appropriate UI if so.
Basically when setIsPatched is true I don't want the useEffect to rerun because it flicks it back to false and breaks the UI
Here is the function:
const [isPatched, setIsPatched] = useState<boolean>(false);
useEffect(() => {
getApplied(x);
}, []);
const getApplied = (x: any) => {
console.log(x);
if (x.Log) {
setIsPatched(true);
return;
} else {
setIsPatched(false);
}
};
I also pass getApplied() to child component which passes a updated data to the function for use in this parent component:
const updatePatch = async (id: string) => {
//check if patch already in db
const content = await services.data.getContent(id);
const infoToUpdate = content?.data[0] as CN;
if (!infoToUpdate.applyLog && infoToUpdate.type == "1") {
// add applyLog property to indicate it is patched and put in DB
infoToUpdate.applyLog = [
{ clID: clID ?? "", usID: usID, appliedAt: Date.now() },
];
if (content) {
await services.data
.updateX(id, content, usId)
.then(() => {
if (mainRef.current) {
setDisabled(true);
}
});
}
getApplied(infoToUpdate);
} else {
console.log("retrying");
setTimeout(() => updatePatch(id), 1000); // retries after 1 second delay
}
};
updatePatch(id);
}

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

fetchMore causes page to re-render unexpectedly

I have an infinite scroll list. I recently updated to the latest Apollo client and noticed infinite scroll no longer works.
Upon deeper investigation. I noticed when I call fetchmore with the incremented skip, it causes the entire page to re-render. Any ideas?
Query:
const {
data,
loading: queryLoading,
fetchMore,
error,
networkStatus
} = useQuery(SEARCH_PROFILES, {
variables: { ...searchParams, skip: skip.current },
fetchPolicy: "cache-first",
notifyOnNetworkStatusChange: true
});
FetchMore
const fetchData = () => {
ErrorHandler.setBreadcrumb("Fetch more profiles");
skip.current =
skip.current + parseInt(process.env.REACT_APP_SEARCHPROS_LIMIT);
const intLimit = parseInt(process.env.REACT_APP_SEARCHPROS_LIMIT);
if (hasMore.current) {
fetchMore({
variables: {
searchType,
long,
lat,
distance,
ageRange,
interestedIn,
skip: skip.current,
limit: intLimit,
isMobile: sessionStorage.getItem("isMobile")
},
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) {
hasMore.current = false;
return previousResult;
} else if (
fetchMoreResult.searchProfiles.profiles.length < intLimit
) {
hasMore.current = false;
}
const newData = produce(previousResult, (draftState) => {
if (draftState.searchProfiles.profiles) {
draftState.searchProfiles.profiles.push(
...fetchMoreResult.searchProfiles.profiles
);
} else {
draftState.searchProfiles.profiles =
fetchMoreResult.searchProfiles.profiles;
}
});
return newData;
}
});
}
};
Well, From your explanation, re-rendering is necessary since you're loading new content on while you scroll,
but for entire page not to be re-rendered is what we're concerned here... below tips might help.
extract the part of the page which needs to fetch data on scroll into a separate component, since it will be the only component which needs to be re-rendered
wrap your extracted component with React.memo() so it doesnt re-render when there is no change on data.
Make good use of life cycle hooks methods, they're the tools to manage on where or how to re-render

useEffect and redux a cache trial not working

I have a rather complex setup and am new to React with Hooks and Redux.
My setup:
I have a component, which when first mounted, should fetch data. Later this data should be updated at a given interval but not too often.
I added useRef to avoid a cascade of calls when one store changes. Without useEffect is called for every possible change of the stores linked in its array.
The data is a list and rather complex to fetch, as I first have to map a name to an ID and
then fetch its value.
To avoid doing this over and over again for a given "cache time" I tried to implement a cache using redux.
The whole thing is wrapped inside a useEffect function.
I use different "stores" and "reducer" for different pieces.
Problem:
The cache is written but during the useEffect cycle, changes are not readable. Even processing the same ISIN once again the cache returns no HIT as it is empty.
Really complex implementation. It dawns on me, something is really messed up in my setup.
Research so far:
I know there are libs for caching redux, I do want to understand the system before using one.
Thunk and Saga seem to be something but - same as above plus - I do not get the concept and would love to have fewer dependencies.
Any help would be appreciated!
Component - useEffect
const calculateRef = useRef(true);
useEffect(() => {
if (calculateRef.current) {
calculateRef.current = false;
const fetchData = async (
dispatch: AppDispatch,
stocks: IStockArray,
transactions: ITransactionArray,
cache: ICache
): Promise<IDashboard> => {
const dashboardStock = aggregate(stocks, transactions);
// Fetch notation
const stockNotation = await Promise.all(
dashboardStock.stocks.map(async (stock) => {
const notationId = await getXETRANotation(
stock.isin,
cache,
dispatch
);
return {
isin: stock.isin,
notationId,
};
})
);
// Fetch quote
const stockQuote = await Promise.all(
stockNotation.map(async (stock) => {
const price = await getQuote(stock.notationId, cache, dispatch);
return {
isin: stock.isin,
notationId: stock.notationId,
price,
};
})
);
for (const s of dashboardStock.stocks) {
for (const q of stockQuote) {
if (s.isin === q.isin) {
s.notationId = q.notationId;
s.price = q.price;
// Calculate current price for stock
if (s.quantity !== undefined && s.price !== undefined) {
dashboardStock.totalCurrent += s.quantity * s.price;
}
}
}
}
dispatch({
type: DASHBOARD_PUT,
payload: dashboardStock,
});
return dashboardStock;
};
fetchData(dispatch, stocks, transactions, cache);
}
}, [dispatch, stocks, transactions, cache]);
Action - async fetch action with cache:
export const getXETRANotation = async (
isin: string,
cache: ICache,
dispatch: AppDispatch
): Promise<number> => {
Logger.debug(`getXETRANotation: ${isin}`);
if (CACHE_ENABLE) {
const cacheTimeExceeding = new Date().getTime() + CACHE_NOTATION;
if (
isin in cache.notation &&
cache.notation[isin].created < cacheTimeExceeding
) {
Logger.debug(
`getXETRANotation - CACHE HIT: ${isin} (${cache.notation[isin].created} < ${cacheTimeExceeding} current)`
);
return cache.notation[isin].notationId;
}
}
// FETCH FANCY AXIOS RESULT
const axiosRESULT = ...
if (CACHE_ENABLE) {
Logger.debug(`getXETRANotation - CACHE STORE: ${isin}: ${axiosRESULT}`);
dispatch({
type: CACHE_NOTATION_PUT,
payload: {
isin,
notationId: axiosRESULT,
created: new Date().getTime(),
},
});
}
//FANCY AXIOS RESULT
return axiosRESULT;
}

Resources