Первоначально опубликовано в моем личном блоге.

React Query (теперь переименованный в TanStack Query) — это библиотека React, используемая для упрощения извлечения и обработки данных на стороне сервера. Используя React Query, вы можете реализовать, наряду с выборкой данных, кэширование и синхронизацию ваших данных с сервером.

В этом руководстве вы создадите простой сервер Node.js, а затем узнаете, как взаимодействовать с ним на веб-сайте React с помощью React Query.

Обратите внимание, что в этой версии используется v4 React Query, который теперь называется TanStack Query.

Вы можете найти код этого руководства в этом репозитории GitHub.

Предпосылки

Прежде чем начать с этого руководства, убедитесь, что у вас установлен Node.js. Вам нужна как минимум версия 14.

Настройка сервера

В этом разделе вы настроите простой сервер Node.js с базой данных SQLite. Сервер имеет 3 конечных точки для получения, добавления и удаления заметок.

Если у вас уже есть сервер, вы можете пропустить этот раздел и перейти к разделу «Настройка веб-сайта».

Создать серверный проект

Создайте новый каталог с именем server, затем инициализируйте новый проект с помощью NPM:

mkdir server
cd server
npm init -y

Установить зависимости

Затем установите пакеты, которые вам понадобятся для разработки сервера:

npm i express cors body-parser sqlite3 nodemon

Вот для чего предназначен каждый из пакетов:

  1. express создать сервер с помощью Экспресс.
  2. cors — это промежуточное ПО Express, используемое для обработки CORS на вашем сервере.
  3. body-parser — это промежуточное ПО Express, используемое для анализа тела запроса.
  4. sqlite3 — это адаптер базы данных SQLite для Node.js.
  5. nodemon — это библиотека, используемая для перезапуска сервера всякий раз, когда в файлах происходят новые изменения.

Создать сервер

Создайте файл index.js со следующим содержимым:

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}`);
});

Это инициализирует сервер с помощью Express на порту 3001. Он также использует промежуточное ПО cors и body-parser.

Затем в package.json добавьте новый скрипт start для запуска сервера:

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

Инициализировать базу данных

В index.js перед app.listen добавьте следующий код:

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)`);
  });
});

Это создает новую базу данных, если она не существует в файле data.db. Затем, если таблица notes не существует в базе данных, она также создается.

Добавить конечные точки

После кода базы данных добавьте следующий код, чтобы добавить конечные точки:

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' });
    });
  });
});

Вкратце, это создает 4 конечные точки:

  1. /notes конечная точка метода GET для получения всех заметок.
  2. /notes/:id конечная точка метода GET для получения заметки по идентификатору.
  3. /notes конечная точка метода POST для добавления примечания.
  4. /notes/:id конечная точка метода DELETE для удаления заметки.

Тестовый сервер

Выполните следующую команду, чтобы запустить сервер:

npm start

Это запускает сервер на порту 3001. Вы можете протестировать его, отправив запрос на localhost:3001/notes.

Настройка веб-сайта

В этом разделе вы создадите веб-сайт с помощью приложения Create React (CRA). Здесь вы будете использовать React Query.

Создать проект веб-сайта

Чтобы создать новое приложение React, выполните следующую команду в другом каталоге:

npx create-react-app website

Это создает новое приложение React в каталоге website.

Установить зависимости

Выполните следующую команду, чтобы перейти в каталог website и установить необходимые зависимости для веб-сайта:

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

Библиотека @tanstack/react-query — это библиотека React Query, которая теперь называется TanStack Query. Другие библиотеки относятся к Tailwind CSS и используются для добавления стилей к веб-сайту.

Настройка CSS попутного ветра

Этот раздел является необязательным и используется только для настройки Tailwind CSS.

Создайте файл postcss.config.js со следующим содержимым:

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

Также создайте файл tailwind.config.js со следующим содержимым:

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

Затем создайте файл src/index.css со следующим содержимым:

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

Наконец, в index.js импортируйте src/index.css в начало файла:

import './index.css';

Используйте QueryClientProvider

Чтобы использовать клиент React Query во всех своих компонентах, вы должны использовать его на высоком уровне в иерархии компонентов вашего веб-сайта. Лучшее место для его размещения — src/index.js, в котором находятся все компоненты вашего веб-сайта.

В src/index.js добавьте следующие импорты в начале файла:

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

Затем инициализируйте новый клиент Query:

const queryClient = new QueryClient()

Наконец, измените параметр, переданный в root.render:

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

Это оборачивает компонент App, который содержит остальные компоненты веб-сайта, с QueryClientProvider. Этот провайдер принимает реквизит client, который является экземпляром QueryClient.

Теперь все компоненты веб-сайта будут иметь доступ к Query Client, который используется для извлечения, кэширования и управления данными сервера.

Примечания к отображению агрегата

Получение данных с сервера — это акт выполнения запроса. Поэтому в этом разделе вы будете использовать useQuery.

Вы будете отображать заметки в компоненте App. Эти заметки извлекаются с сервера с использованием конечной точки /notes.

Замените содержимое app.js следующим содержимым:

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;

Вот кратко, что происходит в этом фрагменте кода:

  1. Вы используете useQuery для получения заметок. Первый параметр, который он принимает, — это уникальный ключ, используемый для кэширования. Второй параметр — это функция, используемая для получения данных. Вы передаете ему функцию fetchNotes.
  2. useQuery возвращает объект, который содержит много переменных. Здесь вы используете 4 из них: isLoading — логическое значение, определяющее, извлекаются ли данные в данный момент; isError — это логическое значение, которое определяет, произошла ли ошибка. data — это данные, получаемые с сервера; а error — это сообщение об ошибке, если isError верно.
  3. Функция fetchNotes должна возвращать обещание, которое либо разрешает данные, либо выдает ошибку. В этой функции вы отправляете GET запрос localhost:3001/notes для получения заметок. Если данные получены успешно, они возвращаются в функции выполнения then.
  4. В возвращенном JSX, если isLoading истинно, отображается значок загрузки. Если isError истинно, отображается сообщение об ошибке. Если data получено успешно и в нем есть какие-либо данные, заметки отображаются.
  5. Вы также показываете кнопку со значком плюса для добавления новых заметок. Вы реализуете это позже.

Проверка отображения примечаний

Чтобы проверить, что вы уже реализовали, убедитесь, что ваш сервер все еще работает, а затем запустите сервер приложений React с помощью следующей команды:

npm start

Это запускает ваше приложение React на localhost:3000 по умолчанию. Если вы откроете его в своем браузере, вы сначала увидите значок загрузки, а затем не увидите никаких заметок, поскольку вы еще ничего не добавили.

Реализовать функцию добавления заметок

Добавление примечания является актом изменения данных сервера. Поэтому в этом разделе вы будете использовать хук useMutation.

Вы создадите отдельный компонент, который показывает форму, используемую для добавления заметки.

Создайте файл src/form.js со следующим содержимым:

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>
  )
}

Вот краткое объяснение этой формы

  1. Эта форма действует как всплывающее окно. Он принимает реквизиты isOpen и setIsOpen для определения момента открытия формы и обработки ее закрытия.
  2. Вы используете useQueryClient, чтобы получить доступ к Query Client. Это необходимо для выполнения мутации.
  3. Чтобы обрабатывать добавление заметки на вашем сервере и синхронизировать все данные в вашем клиенте запросов, вы должны использовать хук useMutation.
  4. Хук useMutation принимает 2 параметра. Первая — это функция, которая будет обрабатывать мутацию, в данном случае это insertNote. Второй параметр — это объект опций. Вы передаете ему одну опцию onSuccess, которая представляет собой функцию, которая запускается, если мутация выполнена успешно. Вы используете это для сброса полей title и content формы.
  5. В insertNote вы отправляете POST запрос на localhost:3001/notes и передаете в теле title и content заметки, которую нужно создать. Если параметр тела success, возвращенный с сервера, равен false, выдается ошибка, указывающая на то, что мутация не удалась.
  6. Если заметка успешно добавлена, вы изменяете кэшированное значение ключа notes с помощью метода queryClient.setQueriesData. Этот метод принимает ключ в качестве первого параметра и новые данные, связанные с этим ключом, в качестве второго параметра. Это обновляет данные везде, где они используются на вашем веб-сайте.
  7. В этом компоненте вы отображаете форму с 2 полями: title и content. В форме вы проверяете, возникает ли ошибка, используя mutation.isError, и получаете доступ к ошибке, используя mutation.error.
  8. Вы обрабатываете отправку формы в функции handleSubmit. Здесь вы запускаете мутацию, используя mutation.mutate. Здесь срабатывает функция insertNote для добавления новой заметки.

Затем в src/app.js добавьте следующие импорты в начале файла:

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

Затем в начале компонента добавьте новую переменную состояния, чтобы управлять тем, открыта форма или нет:

const [isOpen, setIsOpen] = useState(false)

Затем добавьте новую функцию addNote, которая просто использует setIsOpen для открытия формы:

function addNote () {
    setIsOpen(true)
}

Наконец, в возвращенном JSX замените кнопку со значком плюса на следующее:

<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} />

Это устанавливает обработчик onClick кнопки на addNote. Он также добавляет компонент Form, который вы создали ранее, как дочерний компонент App.

Тестовое добавление заметки

Перезапустите сервер и приложение React, если они не работают. Затем снова откройте веб-сайт по адресу localhost:3000. Нажмите кнопку «плюс», и откроется всплывающее окно с формой для добавления новой заметки.

Введите случайное название и содержание, затем нажмите «Добавить». Всплывающая форма закроется, и вы увидите добавленную новую заметку.

Реализовать функцию удаления заметки

Последняя функция, которую вы добавите, — это удаление заметок. Удаление заметки — еще один акт мутации, поскольку он манипулирует данными сервера.

В начале компонента App в src/app.js добавьте следующий код:

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

Здесь вы получаете доступ к клиенту запросов, используя useQueryClient. Затем вы создаете новую мутацию, используя useMutation. Вы передаете ему функцию deleteNote (которую вы создадите дальше) в качестве первого параметра и объекта опций.

В параметр onSuccess вы передаете функцию, которая делает одну вещь. Он выполняет метод queryClient.invalidateQueries. Этот метод помечает кэшированные данные для определенного ключа как устаревшие, что приводит к повторному извлечению данных.

Таким образом, как только заметка будет удалена, созданный вами ранее запрос, который выполняет функцию fetchNotes, будет запущен, и заметки будут получены снова. Если вы создали на своем веб-сайте другие запросы, использующие тот же ключ notes, они также будут инициированы для обновления своих данных.

Далее добавляем функцию deleteNote в компонент App в том же файле:

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);
    })
  }

Эта функция получает note для удаления в качестве параметра. Он отправляет запрос DELETE на localhost:3001/notes/:id. Если параметр тела ответа success равен false, выдается ошибка. В противном случае отображается только предупреждение.

Затем в возвращенном JSX компонента App измените то, как значок загрузки и ошибка, показанные ранее, на следующее:

{(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>}

Это показывает значок загрузки или сообщение об ошибке как для запроса, который извлекает заметки, так и для мутации, которая обрабатывает удаление заметки.

Наконец, найдите кнопку удаления заметки и добавьте обработчик onClick:

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

При щелчке мутация, отвечающая за удаление заметки, запускается с использованием mutation.mutate. Вы передаете ему заметку для удаления, которая является текущей заметкой в ​​цикле map.

Тестовое удаление заметки

Перезапустите сервер и приложение React, если они не работают. Затем снова откройте веб-сайт по адресу localhost:3000. Щелкните ссылку Удалить для любой из ваших заметок. Если заметка успешно удалена, появится предупреждение.

После закрытия оповещения заметки будут снова загружены и отображены, если есть другие заметки.

Заключение

Используя React (TanStack) Query, вы можете легко обрабатывать выборку данных сервера и манипулировать ими на своем веб-сайте с помощью расширенных функций, таких как кэширование и синхронизация в вашем приложении React.

Обязательно ознакомьтесь с официальной документацией, чтобы узнать больше о том, что вы можете делать с React Query.