Tanstack Query / React Query

Jeff P
10 min readFeb 16, 2024

React Query (now Known as Tanstack Query) is a library for managing, caching, and synchronizing server state in React applications. It provides a simple and efficient solution for fetching and caching data from APIs or other data sources, handling loading and error states, and synchronizing data across components.

Key features of React Query include:

  1. Data Fetching: React Query simplifies data fetching by providing hooks like useQuery and useMutation that handle fetching data from APIs. It abstracts away the complexities of managing asynchronous data fetching, allowing developers to focus on building user interfaces.
  2. Caching: React Query includes a powerful caching mechanism that stores fetched data in memory and automatically updates it when needed. This helps to minimize unnecessary network requests and improve application performance.
  3. Background Data Fetching: React Query supports background data fetching, allowing developers to pre-fetch and cache data before it’s needed, such as when navigating to a new page or component. This helps to reduce loading times and provide a smoother user experience.
  4. Optimistic Updates: React Query enables optimistic updates by providing built-in support for optimistic UI rendering. This allows developers to update the UI optimistically before the server response is received, providing a more responsive and interactive user experience.
  5. Pagination and Infinite Loading: React Query includes built-in support for pagination and infinite loading, making it easy to implement these common UI patterns when working with paginated data.
  6. Server-Side Rendering (SSR) Support: React Query is designed to work seamlessly with server-side rendering (SSR) frameworks like Next.js, enabling developers to fetch and cache data on the server before rendering the initial HTML.

Overall, React Query simplifies data management and state synchronization in React applications, making it easier for developers to build fast, responsive, and scalable user interfaces.

Installing and using

First we install the package:

>npm i @tanstack/react-query@4

It’s also advised to installed the associated devtools for React Query

>npm i @tanstack/react-query-devtools@4

We then import the QueryClient and the QueryClientProvider from react query:

import { QueryClient, QueryClientProvider } from 'react-query';

We then can configure a QueryClient “instance” with some default options. QueryClient is a central part of React Query, responsible for managing and caching data fetched by queries.

For example:

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});

We would then wrap our application (or a specific part of it) with the QueryClientProvider component to provide the query client to all components in your app.

function App() {
return (
<QueryClientProvider client={queryClient}>
<GlobalStyles />
<BrowserRouter>
<Routes>
<Route element={<AppLayout />}>
<Route index element={<Navigate replace to="dashboard" />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="bookings" element={<Bookings />} />
<Route path="cabins" element={<Cabins />} />
<Route path="users" element={<Users />} />
<Route path="settings" element={<Settings />} />
<Route path="account" element={<Account />} />
</Route>

<Route path="login" element={<Login />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
);
}

Let’s also import and use the ReactQueryDevtools…

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

And place this special self-closing component immediately after our opening tag for QueryClientProvider in our JSX

function App() {
return (
<QueryClientProvider client={queryClient}>
// special self-closing component
<ReactQueryDevtools initialIsOpen={false} />
<GlobalStyles />
<BrowserRouter>
<Routes>
<Route element={<AppLayout />}>
<Route index element={<Navigate replace to="dashboard" />} />
<Route path="dashboard" element={<Dashboard />} />

Now when we load our page, we’ll see the React-Query logo in the bottom-left corner, which you can click on to open the special dev tools…

Fetching data from Supabase

To fetch data we can use the special useQuery() hook.

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

We could then de-structure the hook that is returned, to extract three properties from the object returned by useQuery:

  • isLoading: A boolean indicating whether the query is currently loading data.
  • data: The actual data fetched by the query. In this case, it's named cabins.
  • error: An error object, if any, encountered during the query.
  const {
isLoading,
data: cabins,
error,
} = useQuery({
queryKey: ["cabin"],
queryFn: getCabins,
});

The hook itself gets passed an options object as its argument…

useQuery({
queryKey: ["cabin"],
queryFn: getCabins,
})

queryKey: An array specifying the query key. The query key is used to identify and manage the cached data associated with the query. In this case, the query key is [“cabin”].

queryFn: A function that performs the actual data fetching. This function is responsible for fetching data from the server or other data sources. Here, the getCabins function is passed as the queryFn. This function will be invoked by React Query to fetch the data.

When the component containing the useQuery hook is rendered, React Query executes the query specified by the queryFn. It then manages the lifecycle of the query, including caching the data, handling loading and error states, and triggering background re-fetches based on the specified options.

Based on the defaultOptions we provided with a “staleTime”of 60 seconds, this means the data retrieved will stay cached for 60 seconds before being deemed “stale”, which can be seen in the React Query devTools. It starts as “fresh” when the component first mounts, until eventually turning stale.

Now with a staleTime of 60 seconds, it means that data on the page won’t necessarily update immediately if any details on the page are changed (for example the price of a cabin changes) until the staleTime expires, so based on this you might want the staleTime to be set to 0 which means that the page will update immediately if something changes.

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 0,
},
},
});

Deleting records with useMutation

IF you want to delete records from a remote database (e..g. Supabase) then you would use the useMutation hook.

You would set it up like this:

  const queryClient = useQueryClient();

const { isLoading: isDeleting, mutate } = useMutation({
mutationFn: (id) => deleteCabin(id),
onSuccess: () => {
queryClient.invalidateQueries(["cabins"]);
},
});

Firstly we use the useQueryClient hook from the @tanstack/react-query library to obtain the queryClient object. The queryClient object is a central piece of React Query that provides various methods to interact with the query cache and perform operations like fetching data, invalidating queries, and more.

isLoading: This property is used to track whether the mutation operation is currently in progress. To make it more clear what it’s doing, it’s renamed to isDeleting, as it will ultimately be used to indicate whether a deletion operation is in progress, to disable a button whilst this stage is in progress.

mutate: This property is a function provided by the useMutation hook, which triggers the mutation operation.

mutationFn: This property specifies the function that will be called when the mutation is invoked. In this case, it's an arrow function (id) => deleteCabin(id). It refers to a function deleteCabin imported from ../../services/apiCabins, which handles the deletion of a cabin with the given id.

onSuccess: This property specifies a function to be called when the mutation succeeds. In this case, it calls queryClient.invalidateQueries(["cabins"]).

Here, queryClient.invalidateQueries is a method provided by React Query to invalidate one or more queries in the cache. When a mutation succeeds (i.e., the deletion of a cabin is successful), it invalidates the query with the key ["cabins"], which is the query responsible for fetching the list of cabins. Basically this is used to immediately re-fetch the data, essentially ensuring the row that we’ve deleted disappears off the screen immediately.

For completeness, here is the deleteCabin function which is a function provided by Supabase:

export async function deleteCabin(id) {
const { data, error } = await supabase.from("cabins").delete().eq("id", id);

if (error) {
console.error(error);
throw new Error("Cabin could not be deleted");
}

return data;
}

Here is the button that calls the function to delete the row….

      <button disabled={isDeleting} onClick={() => mutate(cabinId)}>
Delete
</button>

So you can see that when we click on the button, the mutate() function (useMutation()) is called with the current cabinId, which of course itself is calling the deleteCabin() supabase-relatred function.

This also changes the button to disabled whilst “isDeleting” is true.

It’s important to also note that Suapabase RLS policies also need to be updated to allow table rows to be deleted!

using onError

onError: This property specifies a function to be called when the mutation encounters an error.

  const { isLoading: isDeleting, mutate } = useMutation({
mutationFn: (id) => deleteCabin(id),
onSuccess: () => {
alert("Cabin deleted");

queryClient.invalidateQueries({
queryKey: ["cabins"],
});
},
onError: (error) => {
alert(error.message);
},
});

When an error occurs during the mutation operation (e.g., if the deletion of a cabin fails due to network issues or server errors), this function will be invoked with the error object as its argument. Inside the function, it displays an alert with the error message to notify the user about the error.
Including onError in the useMutation hook allows you to handle errors gracefully and provide feedback to the user when something goes wrong during the mutation operation. This can be helpful for debugging and providing a better user experience by informing users about any issues encountered during data manipulation.

React Hot Toast

React Hot Toast is a library for creating toasts (notifications) in React applications. Toast notifications are non-intrusive messages that typically appear near the bottom of the screen, providing feedback or alerts to users without interrupting their workflow.

Here’s an overview of React Hot Toast’s features and functionality:

  1. Simple API: React Hot Toast offers a simple and intuitive API for creating toast notifications in React applications. Developers can easily create, customize, and display toast notifications with just a few lines of code.
  2. Customizable: The library provides various options for customizing the appearance and behavior of toast notifications. Developers can customize the toast content, position, duration, animation, and more to suit the needs of their application.
  3. Responsive Design: React Hot Toast is designed to work well across different screen sizes and devices. Toast notifications are responsive and adapt to the available screen space, ensuring a consistent user experience on desktop and mobile devices.
  4. Easy Integration: The library can be easily integrated into existing React applications. Developers can install React Hot Toast via npm or yarn and start using it right away to add toast notifications to their projects.

We install React Hot toast as follows:

npm i react-hot-toast

We then import it as Toaster into App.js, and set it up with some options:

        </Routes>
</BrowserRouter>

<Toaster
position="top-center"
gutter={12}
containerStyle={{ margin: "8px" }}
toastOptions={{
success: {
duration: 3000,
},
error: {
duration: 5000,
},
style: {
fontSize: "16px",
maxWidth: "500px",
padding: "1.6rem 2.4rem",
backgroundColor: "var(--color-grey-0)",
color: "var(--color-grey-700)",
},
}}
/>

</QueryClientProvider>

Now we can use it to replace our alerts in our components…

For example….. this….

  const { isLoading: isDeleting, mutate } = useMutation({
mutationFn: (id) => deleteCabin(id),
onSuccess: () => {
alert("Cabin deleted");

queryClient.invalidateQueries({
queryKey: ["cabins"],
});
},
onError: (error) => {
alert(error.message);
},
});

can be changed to this….

  const { isLoading: isDeleting, mutate } = useMutation({
mutationFn: (id) => deleteCabin(id),
onSuccess: () => {
toast.success("Cabin deleted");

queryClient.invalidateQueries({
queryKey: ["cabins"],
});
},
onError: (error) => {
toast.error(error.message);
},
});

In terms of the visual difference…. with standard alerts, when a row is deleted, we’d see this….

but with toasts, we’d see this :-)

React Hook Form

React Hook Form is a library for managing form state and validation in React applications using React Hooks. It provides a simple and intuitive API for building forms that are efficient, performant, and easy to maintain. Here’s an overview of React Hook Form and how to use it:

Features of React Hook Form:

  1. Built on React Hooks: React Hook Form leverages React Hooks for managing form state and validation, allowing developers to build forms using functional components and Hooks.
  2. Minimal API: The library offers a minimal API with a small footprint, making it easy to learn and use. Developers can quickly set up forms with minimal code and configuration.
  3. Performance: React Hook Form is designed for performance, with optimizations such as reduced re-renders and minimal memory usage. This ensures that forms remain responsive and performant, even with complex validation logic or large datasets.
  4. Uncontrolled Components: React Hook Form uses an uncontrolled components approach, where form inputs are managed by the library rather than React state. This simplifies the code and eliminates the need for two-way data binding.
  5. Validation: The library provides built-in support for form validation, allowing developers to define validation rules and error messages for form fields easily.
  6. Custom Hooks: React Hook Form encourages the use of custom Hooks to encapsulate form logic and share it across components. This promotes code reuse and helps keep forms organized and maintainable.

Installation:

npm install react-hook-form

Next we import the hook from the library:

import { useForm } from 'react-hook-form';

We then initialize the form:

function CreateCabinForm() {
const { register, handleSubmit, reset } = useForm();

We then spread the register function to bind form inputs to the form state:

      <FormRow>
<Label htmlFor="name">Cabin name</Label>
<Input type="text" id="name" {...register("name")} />
</FormRow>

<FormRow>
<Label htmlFor="maxCapacity">Maximum capacity</Label>
<Input type="number" id="maxCapacity" {...register("maxCapacity")} />
</FormRow>

<FormRow>
<Label htmlFor="regularPrice">Regular price</Label>
<Input type="number" id="regularPrice" {...register("regularPrice")} />
</FormRow>

<FormRow>
<Label htmlFor="discount">Discount</Label>
<Input
type="number"
id="discount"
defaultValue={0}
{...register("discount")}
/>
</FormRow>

Next we define a submit handler function using the handleSubmit function provided by React Hook Form:

  function formSubmit(data) {
mutate(data);
}

We then apply the onSubmit to the form itself, which calls the react form handleSubmit function, which itself is passed our function that we created.

  return (
<Form onSubmit={handleSubmit(formSubmit)}>
<FormRow>
<Label htmlFor="name">Cabin name</Label>
<Input type="text" id="name" {...register("name")} />
</FormRow>

So what we’re saying here, is that we we submit the form, the react-form handleSubmit function will be called, and this will call our formSubmit function. Our formSubmit function calls the mutate(data) function, which will add a row to our table….

  const { mutate, isLoading: isCreating } = useMutation({
mutationFn: createCabin,
onSuccess: () => {
toast.success("New Cabin created");
queryClient.invalidateQueries({
queryKey: ["cabins"],
});
reset();
},

--

--

Jeff P

I tend to write about anything I find interesting. There’s not much more to it than that really :-)