A short introduction to React Query

A short introduction to React Query

Intro

As you probably know React is a library for building UI's and not a framework. This means it's not very opinionated about how we fetch data and update the state to reflect the latest state.

A naive approach to this might be to use the browser fetch method and call it once the component mounts using useEffect and then subsequently manage the response using useState.
You've probably seen something like

  const [data, setData] = useState();
  const fetchData = () => {
    fetch("https://myApi.com/api")
      .then((response) => response.json())
      .then((json) => setData(json));
  };
  useEffect(() => {
    fetchData();
  }, []);

While this approach works it can quickly become difficult as your project scales and you need to consider caching, retrying failed requests etc. So is there a better way?

Introducing React Query 🚀

React Query is a React library made for fetching, synchronizing and updating data. It handles all the difficult logic for cache management, retrying and other requirements under the hood. While it's mostly used for simplifying network requests i'll show later in the article that this is just scratching the surface of what you can use React Query for.

Lets rewrite the example above to take advantage of React Query.

First lets create a function for fetching data. This can be placed anywhere you want

const fetchData = async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  const json = await response.json();
  return json;
};

Before we can start using React Query we'll need to instantiate a ReactQueryClient. Don't worry it's a one time thing. Instantiate the client by calling const client = new QueryClient();

(Optional) - You can customize the default behavior of your queries and mutations when instantiating the client. By default React Query refetches queries when returning to a tab. If this is undesirable behavior you could instantiate your client like

 const client = new QueryClient({
   defaultOptions: {
     queries: {
       refetchOnWindowFocus: false,
     },
   },
 })

theres a lot of options so i encourage you to take a look at the documentation

We then provide this client by wrapping your app (or component) in QueryClientProvider. If desirable you can provide the <ReactQueryDevtools /> component for debugging purposes.

Example:

    <QueryClientProvider client={client}>
      <App />
      <ReactQueryDevtools />
    </QueryClientProvider>

Then we can consume this function where a component needs it by using the useQuery hook. I encourage you to look at the documentation as there's lots of functionality to extract here.

If the query fails React Query will actually retry it three times before returning an error. While trying to resolve the query isLoading is set to true which lets us display a loading state and then gracefully update it either fails or succeeds. All this while React Query makes sure to cache responses in an efficient manner. A lot of functionality while keeping your code clean.

const MyComponent = () => {
  const { data, isLoading, isError, error } = useQuery("data", fetchData);

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error {error.message}</p>;

  return <div>{data.coolField}</div>;
};

React Query continued

Alright now what if we want to update some data on the server? It's very similar to how we fetch data but we'll use the useMutation hook instead. Just like the useQuery hook we can access several useful states like isLoading èrror`data and more. There's several useful side-effects we get access to from the useMutate effect as well. We'll use the onSuccess hook in this example to refetch a query. But as always take a look at the documentation if you're curious about the other.

Now to our component so that it can post data to the server as well:

const App = () => {
  const [body, setBody] = useState("");
  const [title, setTitle] = useState("");

  const { data, isLoading, isError, error } = useQuery("data", fetchData);
  const mutation = useMutation(postData, {
    onSuccess: () => {
      client.invalidateQueries("data");
    },
  });

  const {
    isLoading: isLoadingMutation,
    isError: isErrorMutation,
    error: errorMutation,
  } = mutation;

  const submitTodo = (e) => {
    e.preventDefault();
    mutation.mutate({ body: body, title: title });
  };

  if (isLoadingMutation) return <p>Posting new message...</p>;
  if (isErrorMutation) return <p>Error {errorMutation.message}</p>;

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Error {error.message}</p>;

  return (
    <>
      <form onSubmit={submitTodo}>
        <input onChange={(e) => setBody(e.target.value)} placeholder="title" />
        <input onChange={(e) => setTitle(e.target.value)} placeholder="body" />
        <button>submit</button>
      </form>
      {data.title}
    </>
  );
};

Notice how we're using refetching queries by calling client.invalidateQueries() and passing in the key "data" previously passed to useQuery. This will force React Query to fetch the new data and consequently update the state across the application.

Using it outside of network

React Query is in practice a async to sync converter and can be used to handle all sorts of state in your application. A great example of this is using it to create custom hooks for managing and accessing different properties in the browser. And as always you have full access to the full suite of nice to have features like loading state etc.

Example of two hooks where using React Query outside of network handling makes a lot of sense.

const useHasRearFacingCamera = () =>
  useQuery(
    "has-rear-camera",
    async () =>
      await navigator.mediaDevices.getUserMedia({
        video: { facingMode: { exact: "environment" } },
      })
  );

const usePrefersReducedMotion = () =>
  useQuery(
    "prefers-reduced-motion",
    () => window.matchMedia("(prefers-reduced-motion: reduce)").matches
  );

This is obviously just scratching the surface of what React Query is capable of doing. If utilised at its full potential it can successfully eliminate any need for a global state management tool like Redux. I strongly encourage you to play around with it. You'll most likely find that you have been managing lots of state you no longer need to.

Did you find this article valuable?

Support Birk Eidsvik by becoming a sponsor. Any amount is appreciated!