How we use React Query
The MilMove project uses TanStack Query v4 for the Frontend applications. Within TanStack Query, we are using the React framework, a combination colloquially known as React Query.
If you have any questions, ask in #prac-frontend / #prac-engineering for help.
Overview
Read more about React Query in the official Overview
documentation. This guide is a brief primer and discusses
best practices for the MilMove project. Instead of being comprehensive, this
documentation strives to be a starting point for the official documentation with
references to how to find the particular React Query functions within the
MilMove codebase using the official GitHub search functionality. This search
functionality can also be achieved locally using command-line tools such as
grep or ripgrep.
useQuery Basics
On the MilMove project we setup all of our custom React Query hooks in the same file, with the exceptions of Mutations. For Mutations we create these on a per-page or per-Component basis.
loading...
When writing a custom query, you can choose to spread the queryKeys to the query function.
export const useNewCustomQueries = (moveCode) => {
const { data: move, ...moveQuery } = useQuery({
queryKey: [KEY, moveCode],
queryFn: ({ queryKey }) => getMove(...queryKey)});
const { isLoading, isError, isSuccess } = getQueriesStatus([movesQuery]);
return {move,orders,isLoading, isError,isSuccess,};
};
Or you can pass only the queryKey need for the api call.
export const useNewCustomQueries = (moveCode) => {
const { data: move, ...moveQuery } = useQuery({
queryKey: [KEY, moveCode],
queryFn: ({ queryKey }) => getMove(queryKey[1]),
});
const { isLoading, isError, isSuccess } = getQueriesStatus([movesQuery, ordersQuery]);
return {move,orders,isLoading, isError,isSuccess,};
};
Multiple queries can be in the same custom Query. If the one query has a dependency based on another query's data, setting the enabled key with the needed value will paused the query while that value is undefined.
loading...
Mutations (Updates)
On the MilMove project, we create Mutations for specific Components or Pages. The reason for that is because these Mutations have callback functions that are specific to user actions. This allows us to do specific Component or Page actions on Success or Error related to the data being shown on that Component or Page.
Writing Mutations
There are some common "gotchas" that can cause mutations to not behave as expected. To avoid those issues, the examples below give examples of best practices to follow in order for mutations to work consistently. These preferences were influenced by methods recommended by the maintainers of React Query.
Mutate functions
Preference is given to using the mutate function because errors are handled by React Query.
There are two mutation functions, mutate or mutateAsync. The mutate function does not return anything and utilizes React Query's built in error handling. However there still access to the mutated data via React Query's callbacks.
import { useMutation } from '@tanstack/react-query'
const {mutate: myMutation } = useMutation({mutateFn: functionToBeCalled});
const onSubmit = () => {
myMutation(variables, {
onSuccess: (result) => {
console.log(result)
history.push(path)
}
})
}
The mutateAsync function returns a promise but requires manual error handling.
import { useMutation } from '@tanstack/react-query'
const {mutateAsync: myAsyncMutation } = useMutation({mutateFn: functionToBeCalled});
const onSubmit = async () => {
try{
const result = await myAsyncMutation();
console.log(result);
history.push(path);
}
catch(error) {
console.log(`Handle this ${error}`)
}
}
Mutation callbacks
In this codebase, the mutation is often created in a different component that where mutate function is called.
Callbacks maybe not fire as expected. To avoid that issue, logic should be handled in the useMutation callback which is called first.
import {useQueryClient} from '@tanstack/react-query'
const queryClient = useQueryClient();
const { mutate: myNewMutation } = useMutation(
{
mutationFn: functionToBeCalled ,
// First callback
onSuccess: () => {
console.log('Where logic should be handled')
queryClient.invalidateQueries({queryKey: [myKey]})
},
onError: () => {
console.log('Error handling');
}
});
UI changes should happen in the mutate callback which is called second after the useMutation callback. This is handled second so the mutation can complete.
const ComponentForm = ({myNewMutation}) => {
const onSubmit = () => {
myNewMutation(
variables,
// Second callback
{
onSuccess: () => { console.log('Where UI changes should happen') },
onError: () => console.log('Error handling UI'),
});
}
return (<Formik onSubmit={onSubmit}/>)
}
If UI changes, such navigating to a new page, happen on the useMutation callback, the mutation will prematurely end. In this codebase, the mutation is often created in a different component that where mutate function is called.
Example of useMutation in the codebase
The useMutation is called on the EditShipmentDetails component. This creates the mutateMTOShipment mutation. It has it's onSuccess and onError handlers that will be called. In these callbacks is where invalidation or any business logic is done.
loading...
Example of using the mutate function in the codebase
mutateMTOShipment is passed down to the shipment form as the value for submitHandler. When submitHandler is called, it has additional onSuccess and onError callbacks. The callbacks on submitHandler are called after the callbacks on the useMutation function above. The callbacks on the submitHandler, where UI changes are done.
loading...
Query invalidation
We use query invalidation to fetch new data from
the APIs that useQuery fetches from. Only use this when you want to fetch new
data related to the Entity that you're updating. Make sure you also fetch new
data for related Entities as well.
import {useQueryClient} from '@tanstack/react-query'
const queryClient = useQueryClient()
const {mutate: myMutation } = useMutation({
mutateFn: functionToBeCalled,
onSuccess: () => {
queryClient.invalidateQueries({queryKey: [KEY_1, KEY_2]})
}
});
Query cache
Currently, we access the Query cache via the QueryClient using the useQueryClient hook. This is mostly used to invalidate and refetch queries. In the future, we have the ability to do Query Cache for the React Query when
we get real-user data.
import {useQueryClient} from '@tanstack/react-query'
const queryClient = useQueryClient()
const {mutate: myMutation } = useMutation({
mutateFn: functionToBeCalled,
onSuccess: () => {
queryClient.invalidateQueries({queryKey: [KEY_1, KEY_2]})
}
});
Testing
In order to test component using React Query, it needs to have a wrapper with an instance of the query client running. ReactQueryWrapper is available in testing utils to accomplish that. The wrapper has been added to MockProviders also in the testing utils.
it('has a React Query wrapper directly', async () => {
render(<ShipmentForm />, {wrapper: ReactQueryWrapper});
});
it('has a React Query wrapper via Mock Providers', async () => {
render(
<MockProviders>
<ShipmentForm />
</MockProviders>,
);
});