Eliminating Repetitive Loading State Handling in Mutations with @tanstack/react-query

Note that when I mention “react-query”, I’m referring to @tanstack/react-query

If you are working in mobile apps, you must be familiar with this UI Pattern.

A local image

As you can see, we present a full loading screen to the user to prevent unwanted actions such as going back, resubmitting a form, and canceling an action when we are submitting data or mutating data to our server.

Usually, we would do something like this:

const LoginFormScreen = () => {
	const [isLoading, setIsLoading] = useState(false)
	const onSubmit = () => {
		setIsLoading(true)
		promise.finally(() => {
			setIsLoading(false)
		})
	}
	return (
		<>
			<LoginForm onSubmit={() => } />
			<FullScreenLoading isLoading={isLoading} />
		</>
	)
}

The issue with the code above is that you need to repeat handling the state over and over again every time you have a mutation action. This could be very daunting if you have a lot of forms in your app.

We can cut some of the repeating stuff by using react-query

const LoginFormScreen = () => {
	const { mutate, isPending } = useMutation(() => {
		mutationFn: () => promise
	})
	const onSubmit = () => {
		mutate()
	}
	return (
		<>
			<LoginForm onSubmit={() => } />
			<FullScreenLoading isLoading={isPending} />
		</>
	)
}

Everything is so much cleaner, but still, every time we have a new form, we will repeat the same task over and over again.

What if we can do these one time and never need to worry about it anymore?

Fortunately, react-query tracks how many mutation is currently running. This means that we have access to the running mutation amount everywhere in our apps as long as it is inside the QueryClientProvider and we can access isMutating state globally with useIsMutating hook provided by react-query:

import { useIsMutating } from "@tanstack/react-query";

export const FullScreenLoading = () => {
  const isMutating = useIsMutating();

  return (
    <Modal visible={!!isMutating} transparent>
      <View style={styles.container}>
        <View style={styles.loadingContainer}>
          <ActivityIndicator />
        </View>
      </View>
    </Modal>
  );
};

We eliminate the need for FullScreenLoading to have props, and we just need to place FullScreenLoading component at the top level of our App and never think about handling this again:

const App = () => {
  return (
    <>
      <TheRestOfYourApp />
      <FullScreenLoading />
    </>
  );
};

export default App;

Additionally we can pass mutationKey to useIsMutating({ mutationKey }) in case we only want to show Full Loading Screen for specific mutation.

Make sure you also pass mutationKey in your useMutation call

useMutation({ mutationKey, mutationFn });

Shared Mutation Cache

In @tanstack/react-query v5 useMutationstate was introduced to solve this https://github.com/TanStack/query/issues/2304

Not that now we have access to the mutation loading state, we are now have access to data and error by filtering mutation by key or status

const variables = useMutationState({
  filters: { status: "pending" },
  select: (mutation) => mutation.state.variables,
});

const isMutating = variables.length > 0;

As you can see we can even achieve the same behavior as what useIsMutating have