Skip to main content

How we use React Query

Version information

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.

Ask in Slack

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.

src/hooks/queries.js
loading...

When writing a custom query, you can choose to spread the queryKeys to the query function.

src/hooks/queries.js
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.

src/hooks/queries.js
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.

src/hooks/queries.js
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

TDLR;

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

Note

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.

use-mutation-example.jsx
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.

second-callback-example.jsx
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}/>)
}
Note

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.

src/pages/Office/EditShipmentDetails/EditShipmentDetails.jsx
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.

src/components/Office/ShipmentForm/ShipmentForm.jsx
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

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