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