Have you grown tired of handling data fetching natively in your React applications? There's just way too much setup required for something that has already been solved. Managing loading states, caching, and error handling can quickly become overwhelming.

Let me introduce you to React Query, a powerful library that simplifies data fetching, caching, and synchronization. It eliminates boilerplate code and helps you manage server state efficiently in React applications.

In this tutorial, we’ll integrate React Query in a Next.js project, learn how to create queries, use mutations, create custom hooks, and even explore folder structure and trade-offs compared to other methods.


Folder Structure

Before we dive into the setup, here's a typical folder structure for a Next.js project using React Query. This helps you understand where to place files and maintain organization:

/project-root
├── /src
│   ├── /app
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── /components
│   │       ├── Todos.tsx
│   │       └── AddTodo.tsx
│   ├── /hooks
│   │   └── useTodos.tsx
│   └── /api
│       └── todos.ts
├── /src/components
│   └── query-provider.tsx
├── package.json
└── README.md

Key Folders:

  • src/app/: Contains Next.js pages and layout components.
  • src/hooks/: Contains custom hooks like useTodos.tsx.
  • src/api/: Includes server-side API logic (for Next.js API routes).
  • src/components/: Contains reusable React components, such as query-provider.tsx.

Note: Modifications include using the QueryProvider component since client-side things cannot be used directly in layout.tsx. The /api has been created for handling data fetching, and specific changes have been made in AddTodo.tsx, Todos.tsx, layout.tsx, and page.tsx to reflect the improved structure and logic.


Installation

Start by creating a Next.js project:

npx create-next-app@latest

Install React Query and its optional ESLint plugin:

npm i @tanstack/react-query
npm i -D @tanstack/eslint-plugin-query

React Query is compatible with React v18+ and works seamlessly with both React DOM and React Native.

Setting Up QueryClientProvider

To enable React Query, wrap your application with the QueryProvider component. This setup ensures global access to React Query throughout your app.

It is important that we create it as a component, because React does not like it when we put client side code in layout.tsx.

query-provider.tsx:

// src/components/query-provider.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import { useState } from "react";

export default function QueryProvider({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({}));

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

Configure layout.tsx:

// src/app/layout.tsx
import QueryProvider from '@/components/query-provider';
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Next.js App',
  description: 'Generated by create next app',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <QueryProvider>
          {children}
        </QueryProvider>
      </body>
    </html>
  );
}

Creating an API for Todos

To implement the GET and POST operations for our todos, we'll use the following API route in Next.js.

// src/app/api/todos/route.ts

import { NextResponse } from 'next/server';

let todos = [
  { id: 1, title: 'Learn React' },
  { id: 2, title: 'Build a Next.js App' },
  { id: 3, title: 'Understand React Query' },
];

export async function GET() {
  return NextResponse.json(todos);
}

export async function POST(request: Request) {
  const newTodo = await request.json();

  if (!newTodo?.title) {
    return NextResponse.json({ error: "Title is required" }, { status: 400 });
  }

  // Generate a new id for the todo
  const newTodoWithId = { id: todos.length + 1, title: newTodo.title };

  todos.push(newTodoWithId);

  return NextResponse.json(newTodoWithId, { status: 201 });
}

Explanation:

  1. GET Handler: Returns the list of todos in JSON format.
  2. POST Handler: Responds with the newly created todo.

This API route serves as the backend for fetching and adding todos. It integrates seamlessly with the React Query setup we discussed in the "Creating Queries" and "Using Mutations" sections.

Creating Queries

Queries in React Query simplify data fetching by handling caching, background updates, and error states automatically.

Example: Fetching Todos

// src/components/Todos.tsx
'use client';

import useTodos from '@/hooks/useTodos';

export default function Todos() {
  const { data, error, isLoading } = useTodos();

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

  return (
    <ul>
      {data.map((todo: { id: number; title: string }) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

Using Mutations

Mutations are used to create, update, or delete server data.

Example: Adding a Todo

// src/components/AddTodo.tsx
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';

export default function AddTodo() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newTodo: { title: string }) => {
      const res = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTodo),
      });
      if (!res.ok) throw new Error('Failed to add todo');
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <div>
      {mutation.isPending ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError && <div>An error occurred: {mutation.error.message}</div>}
          {mutation.isSuccess && <div>Todo added!</div>}
          <button onClick={() => mutation.mutate({ title: 'Do Laundry' })}>
            Add Todo
          </button>
        </>
      )}
    </div>
  );
}

Creating Custom Hooks

Custom hooks make your React Query code cleaner and easier to reuse. Use them whenever possible to keep your components concise and your logic organized.

Hook for Fetching Todos

// src/hooks/useTodos.tsx
import { useQuery } from '@tanstack/react-query';

export default function useTodos() {
  return useQuery({
    queryKey: ['todos'],
    queryFn: async () => {
      const res = await fetch('/api/todos');
      if (!res.ok) throw new Error('Failed to fetch');
      return res.json();
    },
  });
}

Trade-offs and Comparisons

Why React Query?

  • Advantages:
    • Automatic caching and background updates.
    • Handles loading and error states effortlessly.
    • Easy to scale for larger applications.

Alternatives: Axios or Native Fetch

  • Native Fetch:

    • Pros: Lightweight and built-in.
    • Cons: Requires manual state management (loading, error handling, etc.).
  • Axios:

    • Pros: Simplifies HTTP requests and offers interceptors.
    • Cons: Still requires manual state management.

React Query abstracts away these complexities, saving you time and effort.


Conclusion

React Query is a game-changer for managing server data in React applications. It simplifies data fetching, caching, and state management, making your code cleaner and more efficient.

In this tutorial, you learned:

  1. How to set up React Query in a Next.js project.
  2. How to fetch data with queries.
  3. How to modify server data with mutations.
  4. How to create reusable custom hooks.
  5. Trade-offs between React Query and other approaches.

Start integrating React Query today, and enjoy effortless data management in your Next.js applications!


GitHub Repository

The full source code for this tutorial is available on GitHub. You can access it here: Github Repo