Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for React (TanStack) Query Tutorial for Beginners
Shahed Nasser
Shahed Nasser

Posted on • Originally published atblog.shahednasser.com on

     

React (TanStack) Query Tutorial for Beginners

Originally published onmy personal blog.

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 inthis GitHub repository.

Prerequisites

Before starting with this tutorial make sure you haveNode.js installed. 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 calledserver then initialize a new project using NPM:

mkdir servercd servernpm init -y
Enter fullscreen modeExit fullscreen mode

Install Dependencies

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

npm i express cors body-parser sqlite3 nodemon
Enter fullscreen modeExit fullscreen mode

Here's what each of the packages is for:

  1. express to create a server usingExpress.
  2. cors is an Express middleware used to handleCORS 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 fileindex.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}`);});
Enter fullscreen modeExit fullscreen mode

This initializes the server using Express on port3001. It also uses thecors andbody-parser middleware.

Then, inpackage.json add a new scriptstart to run the server:

  "scripts": {    "start": "nodemon index.js"  },
Enter fullscreen modeExit fullscreen mode

Initialize the Database

Inindex.js beforeapp.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)`);  });});
Enter fullscreen modeExit fullscreen mode

This creates a new database if it doesn't exist in the filedata.db. Then, if thenotes 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' });    });  });});
Enter fullscreen modeExit fullscreen mode

Briefly, this creates 4 endpoints:

  1. /notes endpoint of the methodGET to fetch all notes.
  2. /notes/:id endpoint of the methodGET to fetch a note by an ID.
  3. /notes endpoint of the methodPOST to add a note.
  4. /notes/:id endpoint of the methodDELETE to delete a note.

Test Server

Run the following command to start the server:

npm start
Enter fullscreen modeExit fullscreen mode

This starts the server on port3001. You can test it out by sending a request tolocalhost: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
Enter fullscreen modeExit fullscreen mode

This creates a new React app in the directorywebsite.

Install Dependencies

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

cd websitenpm i @tanstack/react-query tailwindcss postcss autoprefixer @tailwindcss/typography @heroicons/react @windmill/react-ui
Enter fullscreen modeExit fullscreen mode

The@tanstack/react-query library is the React Query library which is now named TanStack Query. The other libraries areTailwind CSS 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 filepostcss.config.js with the following content:

module.exports = {  plugins: {    tailwindcss: {},    autoprefixer: {},  },}
Enter fullscreen modeExit fullscreen mode

Also, create the filetailwind.config.js with the following content:

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

Then, create the filesrc/index.css with the following content:

@tailwind base;@tailwind components;@tailwind utilities;
Enter fullscreen modeExit fullscreen mode

Finally, inindex.js importsrc/index.css at the beginning of the file:

import './index.css';
Enter fullscreen modeExit fullscreen mode

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 insrc/index.js which wraps up your entire website's components.

Insrc/index.js add the following imports at the beginning of the file:

import {  QueryClient,  QueryClientProvider,} from '@tanstack/react-query'
Enter fullscreen modeExit fullscreen mode

Then, initialize a new Query client:

const queryClient = new QueryClient()
Enter fullscreen modeExit fullscreen mode

Finally, change the parameter passed toroot.render:

root.render(  <QueryClientProvider client={queryClient}>    <App />  </QueryClientProvider>);
Enter fullscreen modeExit fullscreen mode

This wraps theApp component which holds the rest of the website's components withQueryClientProvider. This provider accepts the propclient which is an instance ofQueryClient.

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 useuseQuery in this section.

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

Replace the content ofapp.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;
Enter fullscreen modeExit fullscreen mode

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

  1. You useuseQuery 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 thefetchNotes function.
  2. useQuery returns an object that holdsmany variables. 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; anderror is the error message ifisError is true.
  3. ThefetchNotes function must return a promise that either resolves data or throws an error. In the function, you send aGET request tolocalhost:3001/notes to fetch the notes. If the data is fetched successfully it is returned in thethen fulfillment function.
  4. In the returned JSX, ifisLoading is true, a loading icon is shown. IfisError is true, an error message is shown. Ifdata 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
Enter fullscreen modeExit fullscreen mode

This runs your React app onlocalhost: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

Implement Add Notes Functionality

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

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

Create the filesrc/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>  )}
Enter fullscreen modeExit fullscreen mode

Here's a brief explanation of this form

  1. This form acts as a pop-up. It acceptsisOpen andsetIsOpen props to determine when the form is opened and handle closing it.
  2. You useuseQueryClient 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 theuseMutation hook.
  4. TheuseMutation hook accepts 2 parameters. Thie first one is the function that will handle the mutation, which in this case isinsertNote. The second parameter is an object ofoptions. You pass it one optiononSuccess which is a function that runs if the mutation is performed successfully. You use this to reset thetitle andcontent fields of the form.
  5. IninsertNote, you send aPOST request tolocalhost:3001/notes and pass in the body thetitle andcontent of the note to be created. If thesuccess body parameter returned from the server isfalse, an error is thrown to signal that the mutation failed.
  6. If the note is added successfully, you change the cached value of thenotes key using thequeryClient.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 andcontent. In the form, you check if an error occurs usingmutation.isError and get access to the error usingmutation.error.
  8. You handle form submission in thehandleSubmit function. Here, you trigger the mutation usingmutation.mutate. This is where theinsertNote function is triggered to add a new note.

Then, insrc/app.js add the following imports at the beginning of the file:

import Form from './form'import { useState } from 'react'
Enter fullscreen modeExit fullscreen mode

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)
Enter fullscreen modeExit fullscreen mode

Next, add a new functionaddNote that just usessetIsOpen to open the form:

function addNote () {    setIsOpen(true)}
Enter fullscreen modeExit fullscreen mode

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} />
Enter fullscreen modeExit fullscreen mode

This sets theonClick handler of the button toaddNote. It also adds theForm component you created earlier as a child component ofApp.

Test Adding a Note

Rerun your server and React app if they're not running. Then, open the website again atlocalhost: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

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

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 theApp component insrc/app.js add the following code:

const queryClient = useQueryClient()const mutation = useMutation(deleteNote, {    onSuccess: () => queryClient.invalidateQueries('notes')})
Enter fullscreen modeExit fullscreen mode

Here, you get access to the query client usinguseQueryClient. Then, you create a new mutation usinguseMutation. You pass it the functiondeleteNote (which you'll create next) as a first parameter and an object of options.

To theonSuccess option you pass a function that does one thing. It executes the methodqueryClient.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 functionfetchNotes will be triggered and the notes will be fetched again. If you had created other queries on your website that use the same keynotes, they'll also be triggered to update their data.

Next, add the functiondeleteNote in theApp 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);    })  }
Enter fullscreen modeExit fullscreen mode

This function receives thenote to be deleted as a parameter. It sends aDELETE request tolocalhost:3001/notes/:id. If thesuccess body parameter of the response isfalse, an error is thrown. Otherwise, only an alert is shown.

Then, in the returned JSX of theApp 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>}
Enter fullscreen modeExit fullscreen mode

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 anonClick handler:

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

On click, the mutation responsible for deleting the note is triggered usingmutation.mutate. You pass it the note to delete which is the current note in amap loop.

Test Deleting a Note

Rerun your server and React app if they're not running. Then, open the website again atlocalhost: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

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

React (TanStack) Query Tutorial for Beginners

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 theofficial documentation to learn more about what you can do with React Query.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Technical writer and full-stack developer
  • Location
    Lebanon
  • Work
    Technical Writer @ Medusa
  • Joined

More fromShahed Nasser

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp