Begrip: Optimistische update
Een optimistische update, is een update die ervan uit gaat dat een update, create of delete operatie succesvol weggeschreven zal worden naar de server en dat de wijzigingen dus in de UI getoond mogen worden. Hier kan dan eventueel nog een animatie of progressbar aan toegevoegd worden om aan te geven dat de data nog weggeschreven wordt.
Een optimistische update is voor een gebruiker aangenamer omdat er sneller iets te zien is op het scherm en de gebruiker dus verder kan werken. Het is echter ook mogelijk dat de mutatie mislukt en dat de UI teruggedraaid moet worden. In een goed gebouwde en geteste applicatie zou dit echter zo goed als nooit mogen voorvallen, maar je moet er natuurlijk wel rekening mee houden in je code.
Om een optimistische update te implementeren moeten volgende stappen doorlopen worden
In de onMutate functie van de useMutation hook
- Eventuele actieve queries moeten geannuleerd worden omdat het mogelijk is dat de optimistische update anders overschreven wordt door het resultaat van de active query. Hiervoor kunnen de onMutate property van de useMutation hook en de cancelQueries methode van de queryClient gebruikt worden.
- We maken vervolgens een kopie van de huidige data in de query cache via de getQueryData methode van de queryClient. We hebben deze nodig om de optimistische update terug te draaien als er iets misgaat.
- We overschrijven de huidige data in de query cache met de aangepast (optimistische) data via de setQueryData methode. Als het om een create operatie gaat, zal in de optimistische data natuurlijk geen ID aanwezig zijn. Hiermee moet dan rekening gehouden worden in de code.
In de onError functie van de useMutation hook
- De oude data gebruiken om de query cache aan te passen via de setQueryData methode.
- De query cache invalideren in het geval dat de error ontstaan is door oude (stale) data op de client die niet meer overeenkwam met data op de server.
In de onSuccess functies van de useMutation hook
- De aangepaste data, teruggegeven door de mutationFn gebruiken om de optimistische data te overschrijven, zo zijn zaken als het ID en andere dingen die door de server gegenereerd moeten worden in orde. Dit is natuurlijk enkel nodig indien de server bepaalde velden moet genereren (zoals een ID). Voor een delete-operatie is deze optie zo goed als altijd overbodig.
import {useQueryClient, useQuery, useMutation, UseMutationResult, UseQueryResult} from '@tanstack/react-query'
const useGetFoo = (): UseQueryResult<TData, Error> => {
return useQuery({
queryKey: ['foo'],
queryFn: getFoo
})
}
const useUpdateFoo = (): UseMutationResult<TData, Error, TVariables, TContext> => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: updateFoo,
onMutate: async (newData) => {
const queryKey = ['foo']
await queryClient.cancelQueries({queryKey})
// De generische parameter kan ook een gewoon object
// zijn in de plaats van een array.
const oldData = queryClient.getQueryData<TData[]>() ?? []
// In het geval dat de cache een enkel object bevat vervang je
// de data in de plaats van een functionele setter (analoog aan useState).
// Voor een update pas je de data in de old array aan.
queryClient.setQueriesData<TVariables>({queryKey}, old => [...old, newData])
return {oldData, queryKey}
},
onError: async (error, variables, context) => {
if (context) {
queryClient.setQueryData<TData[]>(context.queryKey, context.oldData)
await queryClient.invalidateQueries({queryKey: context.queryKey})
}
},
onSucces: (data, variables, context) => {
if (context) {
// Voor een update vervang je het item in de old array met
// de data parameter.
queryClient.setQueryData<TData>({queryKey: context.queryKey}, [...context.oldData, data])
}
}
})
}