React (TanStack) Query Tutorial for Beginners
Tags: react, javascript, webdev, programming
Published: 2022-07-24 16:16:53

---
title: React (TanStack) Query Tutorial for Beginners
published: true
date: 2022-07-24 15:50:35 UTC
tags: react, javascript, webdev, programming
canonical_url: https://blog.shahednasser.com/react-query-tutorial-for-beginners/
cover_image: https://blog.shahednasser.com/static/49810f1cc6a34556c1ce36e3061364ce/React--TanStack--Query-Tutorial-for-Beginners.jpg
---

_Originally published on [my personal blog](https://blog.shahednasser.com/react-query-tutorial-for-beginners/)._

React Query (now rebranded to TanStack Query) is a React library used to make fetching and manipulating server-side data easier. Using React Query, you can implement, along with data fetching, caching, and synchronization of your data with the server.

In this tutorial, you'll build a simple Node.js server and then learn how to interact with it on a React website using React Query.

Please note that this version uses v4 of React Query which is now named TanStack Query.

You can find the code for this tutorial in [this GitHub repository](https://github.com/shahednasser/react-query-tutorial).

## Prerequisites

Before starting with this tutorial make sure you have [Node.js installed](https://nodejs.org/en/). You need at least version 14.

## Server Setup

In this section, you'll set up a simple Node.js server with an SQLite database. The server has 3 endpoints to fetch, add, and delete notes.

If you already have a server you can skip this section and go to the Website Setup section.

### Create Server Project

Create a new directory called `server` then initialize a new project using NPM:

```
mkdir server
cd server
npm init -y
```

### Install Dependencies

Then, install the packages you'll need for the development of the server:

```
npm i express cors body-parser sqlite3 nodemon
```

Here's what each of the packages is for:

1. `express` to create a server using [Express](https://expressjs.com/).
2. `cors` is an Express middleware used to handle [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) on your server.
3. `body-parser` is an Express middleware used to parse the body of a request.
4. `sqlite3` is an SQLite database adapter for Node.js.
5. `nodemon` is a library used to restart the server whenever new changes occur to the files.

### Create Server

Create the file `index.js` with the following content:

```
const express = require('express');

const app = express();
const port = 3001;
const cors = require('cors');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');

app.use(bodyParser.json());
app.use(cors());

app.listen(port, () => {
  console.log(`Notes app listening on port ${port}`);
});
```

This initializes the server using Express on port `3001`. It also uses the `cors` and `body-parser` middleware.

Then, in `package.json` add a new script `start` to run the server:

```
  "scripts": {
    "start": "nodemon index.js"
  },
```

### Initialize the Database

In `index.js` before `app.listen` add the following code:

```
const db = new sqlite3.Database('data.db', (err) => {
  if (err) {
    throw err;
  }

  // create tables if they don't exist
  db.serialize(() => {
    db.run(`CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, 
      created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP)`);
  });
});
```

This creates a new database if it doesn't exist in the file `data.db`. Then, if the `notes` table doesn't exist on the database it creates it as well.

### Add Endpoints

Following the database code, add the following code to add the endpoints:

```
app.get('/notes', (req, res) => {
  db.all('SELECT * FROM notes', (err, rows) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    return res.json({ success: true, data: rows });
  });
});

app.get('/notes/:id', (req, res) => {
  db.get('SELECT * FROM notes WHERE id = ?', req.params.id, (err, row) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    if (!row) {
      return res.status(404).json({ success: false, message: 'Note does not exist' });
    }

    return res.json({ success: true, data: row });
  });
});

app.post('/notes', (req, res) => {
  const { title, content } = req.body;

  if (!title || !content) {
    return res.status(400).json({ success: false, message: 'title and content are required' });
  }

  db.run('INSERT INTO notes (title, content) VALUES (?, ?)', [title, content], function (err) {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    return res.json({
      success: true,
      data: {
        id: this.lastID,
        title,
        content,
      },
    });
  });
});

app.delete('/notes/:id', (req, res) => {
  const { id } = req.params;

  db.get('SELECT * FROM notes WHERE id = ?', [id], (err, row) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
    }

    if (!row) {
      return res.status(404).json({ success: false, message: 'Note does not exist' });
    }

    db.run('DELETE FROM notes WHERE id = ?', [id], (error) => {
      if (error) {
        console.error(error);
        return res.status(500).json({ success: false, message: 'An error occurred, please try again later' });
      }

      return res.json({ success: true, message: 'Note deleted successfully' });
    });
  });
});
```

Briefly, this creates 4 endpoints:

1. `/notes` endpoint of the method `GET` to fetch all notes.
2. `/notes/:id` endpoint of the method `GET` to fetch a note by an ID.
3. `/notes` endpoint of the method `POST` to add a note.
4. `/notes/:id` endpoint of the method `DELETE` to delete a note.

### Test Server

Run the following command to start the server:

```
npm start
```

This starts the server on port `3001`. You can test it out by sending a request to `localhost:3001/notes`.

## Website Setup

In this section, you'll create the website with Create React App (CRA). This is where you'll make use of React Query.

### Create Website Project

To create a new React app, run the following command in a different directory:

```
npx create-react-app website
```

This creates a new React app in the directory `website`.

### Install Dependencies

Run the following command to change to the `website` directory and install the necessary dependencies for the website:

```
cd website
npm i @tanstack/react-query tailwindcss postcss autoprefixer @tailwindcss/typography @heroicons/react @windmill/react-ui
```

The `@tanstack/react-query` library is the React Query library which is now named TanStack Query. The other libraries are [Tailwind CSS](https://tailwindcss.com/) related libraries to add styling to the website.

### Tailwind CSS Setup

This section is optional and is only used to set up Tailwind CSS.

Create the file `postcss.config.js` with the following content:

```
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

Also, create the file `tailwind.config.js` with the following content:

```
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/typography')
  ],
}

```

Then, create the file `src/index.css` with the following content:

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

Finally, in `index.js` import `src/index.css` at the beginning of the file:

```
import './index.css';
```

### Use QueryClientProvider

To use the React Query client in all of your components, you must use it at a high level in your website's components hierarchy. The best place to put it is in `src/index.js` which wraps up your entire website's components.

In `src/index.js` add the following imports at the beginning of the file:

```
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
```

Then, initialize a new Query client:

```
const queryClient = new QueryClient()
```

Finally, change the parameter passed to `root.render`:

```
root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);
```

This wraps the `App` component which holds the rest of the website's components with `QueryClientProvider`. This provider accepts the prop `client` which is an instance of `QueryClient`.

Now, all components within the website will have access to the Query Client which is used to fetch, cache, and manipulate the server data.

### Implement Display Notes

Fetching data from the server is an act of performing a query. Therefore, you'll use `useQuery` in this section.

You'll display notes in the `App` component. These notes are fetched from the server using the `/notes` endpoint.

Replace the content of `app.js` with the following content:

```
import { PlusIcon, RefreshIcon } from '@heroicons/react/solid'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'

function App() {
  const { isLoading, isError, data, error } = useQuery(['notes'], fetchNotes)

  function fetchNotes () {
    return fetch('http://localhost:3001/notes')
    .then((response) => response.json())
    .then(({ success, data }) => {
      if (!success) {
        throw new Error ('An error occurred while fetching notes');
      }
      return data;
    })
  }

  return (
    <div className="w-screen h-screen overflow-x-hidden bg-red-400 flex flex-col justify-center items-center">
      <div className='bg-white w-full md:w-1/2 p-5 text-center rounded shadow-md text-gray-800 prose'>
        <h1>Notes</h1>
        {isLoading && <RefreshIcon className="w-10 h-10 animate-spin mx-auto"></RefreshIcon>}
        {isError && <span className='text-red'>{error.message ? error.message : error}</span>}
        {!isLoading && !isError && data && !data.length && <span className='text-red-400'>You have no notes</span>}
        {data && data.length > 0 && data.map((note, index) => (
          <div key={note.id} className={`text-left ${index !== data.length - 1 ? 'border-b pb-2' : ''}`}>
            <h2>{note.title}</h2>
            <p>{note.content}</p>
            <span>
              <button className='link text-gray-400'>Delete</button>
            </span>
          </div>
        ))}
      </div>
      <button className="mt-2 bg-gray-700 hover:bg-gray-600 rounded-full text-white p-3">
        <PlusIcon className='w-5 h-5'></PlusIcon>
      </button>
    </div>
  );
}

export default App;
```

Here's briefly what's going on in this code snippet:

1. You use `useQuery` to fetch the notes. The first parameter it accepts is a unique key used for caching. The second parameter is the function used to fetch the data. You pass it the `fetchNotes` function.
2. `useQuery` returns an object that holds [many variables](https://tanstack.com/query/v4/docs/reference/useQuery). Here, you use 4 of them: `isLoading` is a boolean value that determines whether the data is currently being fetched; `isError` is a boolean value that determines if an error occurred. `data` is the data that is fetched from the server; and `error` is the error message if `isError` is true.
3. The `fetchNotes` function must return a promise that either resolves data or throws an error. In the function, you send a `GET` request to `localhost:3001/notes` to fetch the notes. If the data is fetched successfully it is returned in the `then` fulfillment function. 
4. In the returned JSX, if `isLoading` is true, a loading icon is shown. If `isError` is true, an error message is shown. If `data` is fetched successfully and has any data in it, the notes are rendered.
5. You also show a button with a plus icon to add new notes. You'll implement this later.

### Test Displaying Notes

To test out what you've implemented so far, make sure your server is still running, then start your React app server with the following command:

```
npm start
```

This runs your React app on `localhost:3000` by default. If you open it in your browser, you'll see a loading icon at first then you'll see no notes as you haven't added any yet.

![React (TanStack) Query Tutorial for Beginners](https://backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-06-28-at-9.10.04-PM.png)

### Implement Add Notes Functionality

Adding a note is an act of mutation on the server data. Therefore, you'll be using the `useMutation` hook in this section.

You'll create a separate component that shows the form used to add a note.

Create the file `src/form.js` with the following content:

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

import { useState } from 'react'

export default function Form ({ isOpen, setIsOpen }) {
  const [title, setTitle] = useState("")
  const [content, setContent] = useState("")
  const queryClient = useQueryClient()

  const mutation = useMutation(insertNote, {
    onSuccess: () => {
      setTitle("")
      setContent("")
    }
  })

  function closeForm (e) {
    e.preventDefault()
    setIsOpen(false)
  }

  function insertNote () {
    return fetch(`http://localhost:3001/notes`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        title,
        content
      })
    })
    .then((response) => response.json())
    .then(({ success, data }) => {
      if (!success) {
        throw new Error("An error occured")
      }
      
      setIsOpen(false)
      queryClient.setQueriesData('notes', (old) => [...old, data])
    })
  }

  function handleSubmit (e) {
    e.preventDefault()
    mutation.mutate()
  }

  return (
    <div className={`absolute w-full h-full top-0 left-0 z-50 flex justify-center items-center ${!isOpen ? 'hidden' : ''}`}>
      <div className='bg-black opacity-50 absolute w-full h-full top-0 left-0'></div>
      <form className='bg-white w-full md:w-1/2 p-5 rounded shadow-md text-gray-800 prose relative' 
        onSubmit={handleSubmit}>
        <h2 className='text-center'>Add Note</h2>
        {mutation.isError && <span className='block mb-2 text-red-400'>{mutation.error.message ? mutation.error.message : mutation.error}</span>}
        <input type="text" placeholder='Title' className='rounded-sm w-full border px-2' 
          value={title} onChange={(e) => setTitle(e.target.value)} />
        <textarea onChange={(e) => setContent(e.target.value)} 
          className="rounded-sm w-full border px-2 mt-2" placeholder='Content' value={content}></textarea>
        <div>
          <button type="submit" className='mt-2 bg-red-400 hover:bg-red-600 text-white p-3 rounded mr-2 disabled:pointer-events-none' 
            disabled={mutation.isLoading}>
            Add</button>
          <button className='mt-2 bg-gray-700 hover:bg-gray-600 text-white p-3 rounded'
            onClick={closeForm}>Cancel</button>
        </div>
      </form>
    </div>
  )
}
```

Here's a brief explanation of this form

1. This form acts as a pop-up. It accepts `isOpen` and `setIsOpen` props to determine when the form is opened and handle closing it.
2. You use `useQueryClient` to get access to the Query Client. This is necessary to perform a mutation.
3. To handle adding a note on your server and keep all data in your query client synced, you must the `useMutation` hook. 
4. The `useMutation` hook accepts 2 parameters. Thie first one is the function that will handle the mutation, which in this case is `insertNote`. The second parameter is an object of [options](https://tanstack.com/query/v4/docs/reference/useMutation). You pass it one option `onSuccess` which is a function that runs if the mutation is performed successfully. You use this to reset the `title` and `content` fields of the form.
5. In `insertNote`, you send a `POST` request to `localhost:3001/notes` and pass in the body the `title` and `content` of the note to be created. If the `success` body parameter returned from the server is `false`, an error is thrown to signal that the mutation failed. 
6. If the note is added successfully, you change the cached value of the `notes` key using the `queryClient.setQueriesData` method. This method accepts the key as a first parameter and the new data associated with that key as a second parameter. This updates the data everywhere it's used on your website.
7. In this component you display a form with 2 fields: `title` and `content`. In the form, you check if an error occurs using `mutation.isError` and get access to the error using `mutation.error`.
8. You handle form submission in the `handleSubmit` function. Here, you trigger the mutation using `mutation.mutate`. This is where the `insertNote` function is triggered to add a new note.

Then, in `src/app.js` add the following imports at the beginning of the file:

```
import Form from './form'
import { useState } from 'react'
```

Then, at the beginning of the component add a new state variable to manage wheter the form is opened or not:

```
const [isOpen, setIsOpen] = useState(false)
```

Next, add a new function `addNote` that just uses `setIsOpen` to open the form:

```
function addNote () {
    setIsOpen(true)
}
```

Finally, in the returned JSX, replace the button with the plus icon with the following:

```
<button className="mt-2 bg-gray-700 hover:bg-gray-600 rounded-full text-white p-3" onClick={addNote}>
    <PlusIcon className='w-5 h-5'></PlusIcon>
</button>
<Form isOpen={isOpen} setIsOpen={setIsOpen} />
```

This sets the `onClick` handler of the button to `addNote`. It also adds the `Form` component you created earlier as a child component of `App`.

### Test Adding a Note

Rerun your server and React app if they're not running. Then, open the website again at `localhost:3000`. Click on the plus button and a pop up will open with the form to add a new note.

![React (TanStack) Query Tutorial for Beginners](https://backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-07-24-at-6.31.41-PM.png)

Enter a random title and content then click Add. The pop up form will then close and you can see the new note added.

![React (TanStack) Query Tutorial for Beginners](https://backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-07-24-at-6.32.53-PM.png)

### Implement Delete Note Functionality

The last functionality you'll add is deleting notes. Deleting a note is another act of mutation as it manipulates the server's data.

At the beginning of the `App` component in `src/app.js` add the following code:

```
const queryClient = useQueryClient()
const mutation = useMutation(deleteNote, {
    onSuccess: () => queryClient.invalidateQueries('notes')
})
```

Here, you get access to the query client using `useQueryClient`. Then, you create a new mutation using `useMutation`. You pass it the function `deleteNote` (which you'll create next) as a first parameter and an object of options.

To the `onSuccess` option you pass a function that does one thing. It executes the method `queryClient.invalidateQueries`. This method marks the cached data for a specific key as outdated, which triggers retrieving the data again.

So, once a note is deleted, the query you created earlier that executes the function `fetchNotes` will be triggered and the notes will be fetched again. If you had created other queries on your website that use the same key `notes`, they'll also be triggered to update their data.

Next, add the function `deleteNote` in the `App` component in the same file:

```
function deleteNote (note) {
    return fetch(`http://localhost:3001/notes/${note.id}`, {
      method: 'DELETE'
    })
    .then((response) => response.json())
    .then(({ success, message }) => {
      if (!success) {
        throw new Error(message);
      }

      alert(message);
    })
  }
```

This function receives the `note` to be deleted as a parameter. It sends a `DELETE` request to `localhost:3001/notes/:id`. If the `success` body parameter of the response is `false`, an error is thrown. Otherwise, only an alert is shown.

Then, in the returned JSX of the `App` component, change how the loading icon and error where shown previously to the following:

```
{(isLoading || mutation.isLoading) && <RefreshIcon className="w-10 h-10 animate-spin mx-auto"></RefreshIcon>}
{(isError || mutation.isError) && <span className='text-red'>{error ? (error.message ? error.message : error) : mutation.error.message}</span>}
```

This shows the loading icon or the error message for both the query that fetches the notes and the mutation that handles deleting a note.

Finally, find the delete button of a note and add an `onClick` handler:

```
<button className='link text-gray-400' onClick={() => mutation.mutate(note)}>Delete</button>
```

On click, the mutation responsible for deleting the note is triggered using `mutation.mutate`. You pass it the note to delete which is the current note in a `map` loop.

### Test Deleting a Note

Rerun your server and React app if they're not running. Then, open the website again at `localhost:3000`. Click the Delete link for any of your notes. If the note is deleted successfully, an alert will be shown.

![React (TanStack) Query Tutorial for Beginners](https://backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-07-01-at-2.48.53-PM.png)

After closing the alert, the notes will be fetched again and displayed, if there are any other notes.

![React (TanStack) Query Tutorial for Beginners](https://backend.shahednasser.com/content/images/2022/07/Screen-Shot-2022-06-28-at-9.10.04-PM-1.png)

## Conclusion

Using React (TanStack) Query, you can easily handle server data fetching and manipulation on your website with advanced features such as caching and synchronization across your React app.

Make sure to check out the [official documentation](https://tanstack.com/query/v4) to learn more about what you can do with React Query.