T
TanStack3y ago
compatible-crimson

Mutation in provider

I'd like to save some database bandwidth so I decided to debounced the save. I'm passing the localstorage version of my data to children of provider and mutate on debounced value after 3 seconds. The problem is after 3 seconds the mutation is triggered and cause a rerender of my provider and all the child components, which is an huge performance issue. Maybe I'm doing it the wrong way but I'm looking for a solution. PS : I don't need mutation result.
export const UserTrainingProvider: FC<PropsWithChildren> = ({ children }) => {
const [userTrainingLocal, setUserTrainingLocal] = useLocalStorage<UserTraining>(
"lms-training-user",
defaultUserTraining
);
const [isLoadingUserTraining, setIsLoadingUserTraining] = useState(true);
const { internalId } = useParams();
const {user: {roles, username}} = useAuth(); // prettier-ignore

if (!internalId || !username || !roles)
throw new LMSUnExpectedBehavior("Impossible d'avoir accès à un cours sans être authentifié", 400);

const productInternalId = Base64.decode(internalId);

const {
data: userTrainingFromAPI,
isLoading: isLoadingUserTrainingFromAPI,
isError,
} = trpcClient.user.getTraining.useQuery({ productInternalId, userIdentifier: username });

const { data: training, isLoading: isLoadingTraining } = trpcClient.training.findOneByProductInternalId.useQuery(
{ productInternalId },
{ enabled: (!isLoadingUserTrainingFromAPI && !userTrainingFromAPI) || isError }
);

const updateUserProgressInTreeMutation = trpcClient.user.saveUserProgressionOnTraining.useMutation({
onError(error: unknown) {
throw new LMSUnExpectedBehavior(
"Une erreur s'est produite durant le sauvegarde en base de données de la progression de l'utilisateur",
400,
{ error }
);
},
});

// Can use debounce hook from io-ts as well
useEffect(() => {
const timer = setTimeout(() => updateUserProgressInTreeMutation.mutate(userTrainingLocal), 3000);
return () => {
clearTimeout(timer);
};
}, [userTrainingLocal]);

const handleUpdateUserTraining = (userTraining: UserTraining) => {
const newUserTraining = { ...userTraining, updatedAt: now().toISOString() };
const userTrainingToSave = produce(userTraining, () => JSON.parse(JSON.stringify(newUserTraining)));
setUserTrainingLocal(userTrainingToSave);
};

const checkIfUserTrainingInLocalStorageIsMoreRecentThanDatabase = (
userTrainingLocalStorage: UserTraining,
userTrainingFromAPI: UserTraining
): boolean => {
return dayjsFr(userTrainingLocalStorage.updatedAt).isAfter(dayjsFr(userTrainingFromAPI.updatedAt));
};

const createEmptyUserTrainingMutation = trpcClient.user.buildEmptyUserTraining.useMutation({
onSuccess(userTraining: UserTraining) {
handleUpdateUserTraining(userTraining);
setIsLoadingUserTraining(false);
},
});

useEffect(() => {
if (userTrainingFromAPI) {
if (checkIfUserTrainingInLocalStorageIsMoreRecentThanDatabase(userTrainingLocal, userTrainingFromAPI)) {
handleUpdateUserTraining(userTrainingLocal);
}
setIsLoadingUserTraining(false);
}
}, [userTrainingFromAPI]);

useEffect(() => {
if (!isLoadingUserTrainingFromAPI && !userTrainingFromAPI && !isLoadingTraining && training) {
createEmptyUserTrainingMutation.mutate({ training, username });
}
}, [training]);

return !isLoadingUserTraining ? (
<UserTrainingContext.Provider
value={{ userTraining: userTrainingLocal, updateUserTraining: handleUpdateUserTraining }}
>
{children}
</UserTrainingContext.Provider>
) : (
<Loader />
);
};
export const UserTrainingProvider: FC<PropsWithChildren> = ({ children }) => {
const [userTrainingLocal, setUserTrainingLocal] = useLocalStorage<UserTraining>(
"lms-training-user",
defaultUserTraining
);
const [isLoadingUserTraining, setIsLoadingUserTraining] = useState(true);
const { internalId } = useParams();
const {user: {roles, username}} = useAuth(); // prettier-ignore

if (!internalId || !username || !roles)
throw new LMSUnExpectedBehavior("Impossible d'avoir accès à un cours sans être authentifié", 400);

const productInternalId = Base64.decode(internalId);

const {
data: userTrainingFromAPI,
isLoading: isLoadingUserTrainingFromAPI,
isError,
} = trpcClient.user.getTraining.useQuery({ productInternalId, userIdentifier: username });

const { data: training, isLoading: isLoadingTraining } = trpcClient.training.findOneByProductInternalId.useQuery(
{ productInternalId },
{ enabled: (!isLoadingUserTrainingFromAPI && !userTrainingFromAPI) || isError }
);

const updateUserProgressInTreeMutation = trpcClient.user.saveUserProgressionOnTraining.useMutation({
onError(error: unknown) {
throw new LMSUnExpectedBehavior(
"Une erreur s'est produite durant le sauvegarde en base de données de la progression de l'utilisateur",
400,
{ error }
);
},
});

// Can use debounce hook from io-ts as well
useEffect(() => {
const timer = setTimeout(() => updateUserProgressInTreeMutation.mutate(userTrainingLocal), 3000);
return () => {
clearTimeout(timer);
};
}, [userTrainingLocal]);

const handleUpdateUserTraining = (userTraining: UserTraining) => {
const newUserTraining = { ...userTraining, updatedAt: now().toISOString() };
const userTrainingToSave = produce(userTraining, () => JSON.parse(JSON.stringify(newUserTraining)));
setUserTrainingLocal(userTrainingToSave);
};

const checkIfUserTrainingInLocalStorageIsMoreRecentThanDatabase = (
userTrainingLocalStorage: UserTraining,
userTrainingFromAPI: UserTraining
): boolean => {
return dayjsFr(userTrainingLocalStorage.updatedAt).isAfter(dayjsFr(userTrainingFromAPI.updatedAt));
};

const createEmptyUserTrainingMutation = trpcClient.user.buildEmptyUserTraining.useMutation({
onSuccess(userTraining: UserTraining) {
handleUpdateUserTraining(userTraining);
setIsLoadingUserTraining(false);
},
});

useEffect(() => {
if (userTrainingFromAPI) {
if (checkIfUserTrainingInLocalStorageIsMoreRecentThanDatabase(userTrainingLocal, userTrainingFromAPI)) {
handleUpdateUserTraining(userTrainingLocal);
}
setIsLoadingUserTraining(false);
}
}, [userTrainingFromAPI]);

useEffect(() => {
if (!isLoadingUserTrainingFromAPI && !userTrainingFromAPI && !isLoadingTraining && training) {
createEmptyUserTrainingMutation.mutate({ training, username });
}
}, [training]);

return !isLoadingUserTraining ? (
<UserTrainingContext.Provider
value={{ userTraining: userTrainingLocal, updateUserTraining: handleUpdateUserTraining }}
>
{children}
</UserTrainingContext.Provider>
) : (
<Loader />
);
};
2 Replies
compatible-crimson
compatible-crimsonOP3y ago
up ?
flat-fuchsia
flat-fuchsia3y ago
I'd focus on trying to remove the useStates within this hook since they cause re-renders (shouldn't have to use extra state for loading indicators, react-query should manage that for you). Also look at what makes the value passed to the Context provider change: for example handleUpdateUserTraining has a new reference at each render, which causes all the context consumers to re-render every time. Finally, for debouncing, I would debounce the mutation function itself (using a useDebounce hook or similar) rather than relying on the userTrainingLocal state + useEffect, which is an antipattern: https://react.dev/learn/you-might-not-need-an-effect#sending-a-post-request. Using that guide, I would look into the other useEffects and see if they can be removed as well.

Did you find this page helpful?