Getting started with React-Query: A Beginner's Guide

A brief introduction of React-Query

React Query is a powerful library that revolutionizes the way we manage and fetch data in React applications. At its core, React Query simplifies state management and data fetching by providing a declarative API that seamlessly integrates with React components. One of the notable features of React Query is its provision of distinct states for loading, error, and success scenarios(one of my favourite aspects of react query). With React Query, developers can effortlessly handle asynchronous data fetching, caching, and invalidation with just a few lines of code. Another remarkable facet of React Query is its high degree of customizability, empowering developers to tailor data fetching and caching behaviors to suit their specific project requirements.

In this blog, I'll be presenting a straightforward React application focusing on two core APIs of React Query: useQuery and useMutation - useQuery for fetching data and useMutation for making changes in the data (the names itself are a giveaway).

Let's Dive into Coding

We will proceed systematically, dividing the blog into 8 clear steps.

You can refer to this github repo if you get stuck anywhere: here

Step 1: React project setup using Vite

So first of all we will create a folder and open that folder in VS Code. We will then open a terminal in VS Code and run the following command:

npm create vite@latest ./

We will proceed by selecting React and then Typescript.

I am using Tailwind CSS for this project.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

// we will use this later
@layer {
  .form_input {
    @apply rounded-[4px] outline-none text-lg;
  }
}

The App.tsx should look like this:

function App() {
  return (
    <div>
      <h1 className="text-xl text-red-500">Hello World</h1>
    </div>
  );
}

export default App;

Step 2: React-Query Setup

In this step we will install the react query package.

npm i @tanstack/react-query

Next we will setup queryClient in the root of the app i.e. in the main.tsx file.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

The queryClient in React Query is like a smart organizer for data in the app. It keeps track of fetched data, helps you fetch new data, and handles updating and refreshing it when needed. It's like having a helpful assistant that makes sure your app's data is always up-to-date and easily accessible.

Step 3: Making our first Request

For making requests we will need the axios package. So we need to install it:

npm i axios

We will now create the lib folder inside the src folder and then create 2 files within it: constants.ts and utils.ts.

The constants.ts file will have our server url. We'll utilize the JSONPlaceholder API, specifically focusing on the users endpoint.

export const SERVER_URL = "https://jsonplaceholder.typicode.com";

The utils.ts file will contain all the query functions.

import axios from "axios";
import { SERVER_URL } from "./constants";

export const getUsers = () => {
  return axios.get(`${SERVER_URL}/users`);
};

Here we have defined the first get request. We will use this function as the query function which we will see next. An important aspect to remember is that the query function must always return a promise.

Next use the useQuery hook of react query to actually make the get request using the query function.

// App.tsx
import { useQuery } from "@tanstack/react-query";
import { getUsers } from "./lib/utils";

function App() {
  const { isLoading, isError, data, error } = useQuery({
    queryKey: ["todos"],
    queryFn: getUsers,
  });

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    console.log(error);

    return <h2>Something went wrong.</h2>;
  }

  if (data) {
    console.log(data.data);
  }

  return (
    <div>
      <h1 className="text-xl text-red-500">Hello World</h1>
    </div>
  );
}
export default App;

As we can see the useQuery hook returns different states we can dynamically adjust the user interface to provide feedback or display relevant content.

The queryKey is not so important at this point in time but later we will see how to use that.

For now we are just logging the data but in the next step we will make a user card component and use the data there.

Step 4: Create UserCard Component

This step is straightforward, allowing you the flexibility to create the component according to your preferences and coding style.

Here is my implementation:

// src/components/UserCard.tsx
type UserCardProps = {
  name: string;
  street: string;
  city: string;
  company: string;
  email: string;
};

const UserCard = ({ name, street, city, company, email }: UserCardProps) => {
  return (
    <div className="w-72 rounded-[8px] overflow-hidden bg-slate-400 justify-self-center">
      <div className="w-full h-24 bg-slate-800 relative">
        <div className="w-20 h-20 absolute top-10 left-[50%] -translate-x-[50%] bg-white rounded-full">
          <img
            src="/icons/user.svg"
            alt="user_img"
            className="object-contain w-full"
          />
        </div>
      </div>
      <div className="flex flex-col items-center justify-center py-6 px-4">
        <h3 className="text-xl font-semibold">{name}</h3>
        <p>
          {street}, {city}
        </p>
        <p>Company: {company}</p>
        <p>email: {email}</p>
      </div>
    </div>
  );
};
export default UserCard;

After this we need to make some changes in the the App component to actually render the UserCard component.

import { useQuery } from "@tanstack/react-query";
import { getUsers } from "./lib/utils";
import UserCard from "./components/UserCard";

function App() {
  const { isLoading, isError, data, error } = useQuery({
    queryKey: ["users"],
    queryFn: getUsers,
  });
  if (isLoading) {
    return <h2>Loading...</h2>;
  }
  if (isError) {
    console.log(error);
    return <h2>Something went wrong.</h2>;
  }
  if (data) {
    console.log(data.data);
  }

  return (
    <div>
      <h1 className="text-xl text-red-500">Hello World</h1>
      <h1 className="text-3xl font-semibold text-center my-4">Users</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-y-5">
        {data &&
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          data.data.map((user: any) => (
            <UserCard
              key={user.id}
              name={user.name}
              city={user.address.city}
              street={user.address.street}
              company={user.company.name}
              email={user.email}
            />
          ))}
      </div>
    </div>
  );
}
export default App;

Step 5: Implementing Loading Screen

For the loading screen I have obtained an icon from the Google Material Icons and then created the Loading component.

// components/Loading.tsx
const Loading = () => {
  return (
    <div className="flex items-center justify-center h-screen">
      <div className="animate-spin w-10 h-10">
        <img
          src="/icons/loading.svg"
          alt="loading"
          className="object-contain w-full"
        />
      </div>
    </div>
  );
};
export default Loading;

Step 6: Making post request using useMutation

As we saw we needed a query function in case of useQuery, similarly we will need a mutation function for the useMutation hook which we will again define in the utis.tsx file:

import axios from "axios";
import { SERVER_URL } from "./constants";
import { User } from "./types";

export const getUsers = () => {
  return axios.get(`${SERVER_URL}/users`);
};

export const createUser = (user: User) => {
  return axios.post(`${SERVER_URL}/users`, { ...user });
};

We will create a types.ts in the lib folder to store the user type.

export type User = {
  name?: string;
  street?: string;
  city?: string;
  company?: string;
  email?: string;
  id?: number;
};

Now we will create the UserForm component.

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { createUser } from "../lib/utils";

const UserForm = () => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [city, setCity] = useState("");
  const [street, setStreet] = useState("");
  const [company, setCompany] = useState("");

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: createUser,
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    mutation.mutate({ name, city, company, email, street });
  };

  if (mutation.isSuccess) {
    console.log(mutation.data);
  }

  return (
    <div className="flex items-center justify-center my-10 px-4">
      <form
        onSubmit={handleSubmit}
        className="flex flex-col gap-y-3 w-full max-w-[500px] bg-slate-500 p-6 rounded-[8px]"
      >
        <label htmlFor="name">Name</label>
        <input
          type="text"
          value={name}
          id="name"
          onChange={(e) => setName(e.target.value)}
          className="form_input py-2 px-3"
        />
        <label htmlFor="street">Street</label>
        <input
          type="text"
          value={street}
          id="street"
          onChange={(e) => setStreet(e.target.value)}
          className="form_input py-2 px-3"
        />
        <label htmlFor="company">Company</label>
        <input
          type="text"
          value={company}
          id="company"
          onChange={(e) => setCompany(e.target.value)}
          className="form_input py-2 px-3"
        />
        <label htmlFor="email">Email</label>
        <input
          type="email"
          value={email}
          id="email"
          onChange={(e) => setEmail(e.target.value)}
          className="form_input py-2 px-3"
        />
        <label htmlFor="city">City</label>
        <input
          type="text"
          value={city}
          id="city"
          onChange={(e) => setCity(e.target.value)}
          className="form_input py-2 px-3"
        />
        <button
          type="submit"
          className="bg-slate-900 text-white px-4 py-2 w-fit rounded-[8px] mx-auto mt-3"
        >
          Submit
        </button>
      </form>
    </div>
  );
};
export default UserForm;

The useMutate hook in React Query provides a mutate function, accessed as mutation.mutate, allowing us to initiate a POST request conveniently, typically triggered upon form submission.

The queryClient is used here to run the invalidateQueries function which basically runs a query with which has the queryKey that we provide to refetch the data. Here the queryKey is used to uniquely identify a query. However here the invalidateQueries function would not do much because although we are making a post request and creating a user, JSONPLACEHOLDER does not actually change the data. So although we are making new request when the mutation is successfull but the data remains the same. We will tackle this problem in the next step.

// App.tsx we use the UserForm component
return (
    <div>
      <h1 className="text-3xl font-semibold text-center my-4">Users</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-y-5">
        {data &&
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          data.data.map((user: any) => (
            <UserCard
              key={user.id}
              name={user.name}
              city={user.address.city}
              street={user.address.street}
              company={user.company.name}
              email={user.email}
            />
          ))}
      </div>
      <div>
        <UserForm />
      </div>
    </div>
  );

We also make some changes in the UserCard component

import { User } from "../lib/types";

const UserCard = ({
  name = "xxx",
  street = "xxx",
  city = "xxx",
  company = "xxx",
  email = "xxx",
}: User) => {
  return (
    <div className="w-72 rounded-[8px] overflow-hidden bg-slate-400 justify-self-center">
      ...
    </div>
  );
};
export default UserCard;

Step 7: Using useState for state management of users

Since we are not getting the new user that we create in response from the server when we use the invalidateQueries function, we will just maintain a state for the users and add the new user in the state when using the response we get from the mutate function.

//App.tsx
import { useQuery } from "@tanstack/react-query";
import { getUsers } from "./lib/utils";
import UserCard from "./components/UserCard";
import Loading from "./components/Loading";
import UserForm from "./components/UserForm";
import { useEffect, useState } from "react";
import { User } from "./lib/types";

function App() {
  const [users, setUsers] = useState<User[] | null>(null);

  const { isLoading, isError, data, error } = useQuery({
    queryKey: ["users"],
    queryFn: getUsers,
  });

  useEffect(() => {
    if (data) {
      setUsers(data.data);
    }
  }, [data]);

  if (isLoading) {
    return <Loading />;
  }
  if (isError) {
    console.log(error);
    return <h2>Something went wrong.</h2>;
  }
  // if (data) {
  //   console.log(data.data);
  // }
  return (
    <div>
      <h1 className="text-3xl font-semibold text-center my-4">Users</h1>
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-y-5">
        {users &&
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          users.map((user: any) => (
            <UserCard
              key={user.id}
              name={user.name}
              city={user?.address?.city || user.city}
              street={user?.address?.street || user.street}
              company={user?.company?.name || user.company}
              email={user.email}
            />
          ))}
      </div>
      <div>
        <UserForm setUsers={setUsers} />
      </div>
    </div>
  );
}
export default App;

We are maintaining the state in the App component and passing the setUsers function to UserForm so that we can add the new user.

//UserForm.tsx

import { useMutation } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { createUser } from "../lib/utils";
import { User } from "../lib/types";

const UserForm = ({
  setUsers,
}: {
  setUsers: React.Dispatch<React.SetStateAction<User[] | null>>;
}) => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [city, setCity] = useState("");
  const [street, setStreet] = useState("");
  const [company, setCompany] = useState("");

  const mutation = useMutation({
    mutationFn: createUser,
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    mutation.mutate({ name, city, company, email, street });
  };

  useEffect(() => {
    if (mutation.isSuccess) {
      setUsers((prev) => [...prev!, mutation.data.data]);
    }
  }, [mutation.isSuccess, setUsers, mutation.data]);

  return (
    ...

In the UserForm component we have removed the queryClient and invalidateQueries function and in place of that we are now using the useEffect to add the new user if the mutation in successful.

Step 8: Rendering different states of user creation

We can use the isError, isPending and isSuccess which are returned by the useMutation hook to show different states.

// UserForm.tsx
...
return (
    <div className="flex flex-col gap-y-4 items-center justify-center my-10 px-4">
      <div>
        {mutation.isError && <p>{mutation.error.message}</p>}
        {mutation.isPending && <p>Adding User...</p>}
        {mutation.isSuccess && <p>User Added Successfully.</p>}
      </div>
      <form
...

That's it for this project.

Conclusion

As we conclude this journey into the world of React Query, we've explored its powerful capabilities in simplifying data management within React applications. From effortlessly fetching data with useQuery to seamlessly effecting changes with useMutation, React Query has demonstrated its versatility and efficiency. React Query's ease of use and flexibility make it an invaluable tool for building modern, data-driven web applications.We can further customize the behaviour by providing options to useQuery and useMutation as required. However in this project I have opted to stick with the default values which will work just fine for most of the cases.

Have a look at the react query documentation if you want to explore further: here

Happy coding!