React Query is a popular library for fetching, caching and managing the state of your data in React applications. Next.js is a great framework for building server-rendered or statically exported React applications. Together, they can make building fast and efficient data-driven applications even easier. We also have an article covering using React Query in vanilla React apps here, but in this article, I’ll be talking about Next.js.

Why use React Query?

You might already be using API calls in your app using something like a combination of Axios and hooks, so you might be wondering why to use this library. React Query offers several benefits for managing data in React applications.

Firstly, it simplifies the process of fetching, caching and updating data.

React Query also has features like automatic re-fetching and error handling, making it a robust solution for managing data. Additionally, it integrates well with other React libraries and has a small footprint, making it a lightweight solution for managing data in React applications.

Set-up

First, let’s install React Query using:

npm install --save react-query

Then to add React query to our project, we just have to set up a QueryClientProvider, which uses Context under the hood. You’re likely to want to wrap this around your App component, in “_app.tsx”:

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

const queryClient = new QueryClient();

function App({ Component, pageProps }) {
  return (
    <QueryClientProvider client={queryClient}>
      <Component {...pageProps} />
    </QueryClientProvider>
  );
}

And that’s all you need! Now let’s get started trying to build something with React Query.

Example

React Query revolves around the useQuery hook. You can provide the hook with a regular asynchronous function. In our case we’ll be using Axios to fetch some data from the PokeAPI:

const fetchPokemon = async (pokemon: string) => {
    const res = await axios.get<SinglePokemonResponse>(
        'https://pokeapi.co/api/v2/pokemon/' + pokemon
    );
    return {
        name: res.data.name,
        types: res.data.types,
        sprite: res.data.sprites.front_default,
    };
};

The details of this don’t matter as much, but we get back a JSON object containing some data about the pokemon we provide. If you’d like to see another example of this, here’s a very similar article which uses the Axios library to perform API calls.

So let’s get started. Here’s the page we’ll be working with:

export default function ReactQuery() {
    return (
        <div className="flex h-screen w-full items-center justify-center bg-stone-900 text-white">
            <PokemonCard {...pokemon} />
        </div>
    );
}

Just a simple page styled with TailwindCSS. You might be wondering where our pokemon variable comes from, so let’s add in our useQuery hook:

export default function ReactQuery() {
    const pokemonName = 'bulbasaur';
    const { isLoading, error, data: pokemon } = useQuery(`fetch-${pokemonName}`, () =>
        fetchPokemon(pokemonName)
    );
    return (
        <div className="flex h-screen w-full items-center justify-center bg-stone-900 text-white">
            <PokemonCard {...pokemon} />
        </div>
    );
}

We provide the hook with two things. Firstly, a key to use for our query. React Query uses this for things like caching queries, to know if two queries are identical. Then we pass it the query function, in our case our fetchPokemon function from before.

useQuery returns a query object with a lot of useful properties, but for now, I’m interested in managing the state of my query, i.e. is it still in progress, has an error been thrown, and if not then getting our actual data.

Let’s add in some conditional rendering to display messages for these different states.

export default function ReactQuery() {
    const pokemonName = 'bulbasaur';
    const {
        isLoading,
        isSuccess,
        error,
        status,

        data: pokemon,
    } = useQuery(`fetch-${pokemonName}`, () => fetchPokemon(pokemonName));

    if (isLoading)
        return (
            <PageWrapper>
                <Loading />
            </PageWrapper>
        );
    if (error || !isSuccess)
        return (
            <PageWrapper>
                <Error />
            </PageWrapper>
        );

    return (
        <PageWrapper>
            <PokemonCard {...pokemon} />
        </PageWrapper>
    );
}

If you’re using TypeScript, you also get some great automatic type narrowing, e.g. by checking if “isSuccess” is true, TypeScript knows that data is not undefined.

Pagination

Now there’s a lot more pokemon than just Bulbasaur, so let’s use React Query to manage this.

Firstly we’ll modify our page to be able to display multiple pokemon:

export default function ReactQuery() {
...


    return (
        <PageWrapper>
            <div className="grid auto-rows-auto grid-cols-3 gap-12">
                <PokemonCard {...pokemon} />
                <PokemonCard {...pokemon} />
                <PokemonCard {...pokemon} />
            </div>
        </PageWrapper>
    );
}

I’ve just changed the cards to be displayed in a grid, and thrown in some of the same card just to test it.

Here’s our new page:

And we’ll also add in a button to load more:

            <button className="flex items-center justify-center rounded bg-white p-4 text-xl text-stone-800">
                <FaPlus />
            </button>

Now let’s fetch some other pokemon, so we have something else to display.

Here’s our updated component:

export default function ReactQuery() {
    const { fetchNextPage, data } = useInfiniteQuery(
        'fetch-pokemon',
        ({ pageParam = 1 }) => fetchPokemon(pageParam),
        {
            getNextPageParam: (lastPage) => lastPage.next,
        }
    );

    return (
        <PageWrapper>
            <div className="grid auto-rows-auto grid-cols-3 gap-12">
                {data?.pages?.map((p) => (
                    <PokemonCard key={p.name} {...p} />
                ))}
            </div>
            <button
                className="flex items-center justify-center rounded bg-white p-4 text-xl text-stone-800"
                onClick={() => fetchNextPage()}>
                <FaPlus />
            </button>
        </PageWrapper>
    );
}

Firstly, I’ve swapped out useQuery() for useInfiniteQuery(). The function we pass to this takes a page parameter, which gets passed to our fetch function so it knows how to get the next pokemon. Luckily in our case, the PokeAPI also just accepts a numerical id instead of a name, e.g. instead of using “Bulbasaur” we can just use “1”.

We get back our data in an array, as well as a function to call when we want another Pokemon.

Here’s our new page:

And that’s all we need to load more!

Conclusion

Thanks for reading this article on using React Query with Next.js. Hopefully, with these two examples, I’ve given you a taste of what React Query is capable of. If you liked this article, or if you’re having any issues, feel free to leave a comment!

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.