Первоначально опубликовано в моем личном блоге.
React Query (сейчас ребрендинг TanStack Query) — это библиотека React, используемая для упрощения получения и манипулирования данными на стороне сервера. Используя React Query, вы можете реализовать, наряду с выборкой данных, кэширование и синхронизацию ваших данных с сервером.
В этом руководстве вы создадите простой сервер Node.js, а затем узнаете, как взаимодействовать с ним на сайте React с помощью React Query.
Обратите внимание, что в этой версии используется v4 React Query, который теперь называется TanStack Query.
Код для этого руководства вы можете найти в этом репозитории GitHub.
- Предварительные условия
- Настройка сервера
- Создание проекта сервера
- Установка зависимостей
- Создать сервер
- Инициализация базы данных
- Добавьте конечные точки
- Тестовый сервер
- Настройка веб-сайта
- Создание проекта веб-сайта
- Установка зависимостей
- Настройка Tailwind CSS
- Использование QueryClientProvider
- Реализовать отображение заметок
- Тестирование отображения заметок
- Реализация функциональности добавления заметок
- Тест добавления заметки
- Реализация функциональности удаления заметки
- Тест удаления заметки
- Заключение
Предварительные условия
Прежде чем приступить к этому руководству, убедитесь, что у вас установлен Node.js. Вам нужна как минимум версия 14.
Настройка сервера
В этом разделе вы настроите простой сервер Node.js с базой данных SQLite. Сервер имеет 3 конечные точки для получения, добавления и удаления заметок.
Если у вас уже есть сервер, вы можете пропустить этот раздел и перейти к разделу «Настройка веб-сайта».
Создание проекта сервера
Создайте новый каталог server
, затем инициализируйте новый проект с помощью NPM:
mkdir server
cd server
npm init -y
Установка зависимостей
Затем установите пакеты, которые понадобятся для разработки сервера:
npm i express cors body-parser sqlite3 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 конечные точки:
Тестовый сервер
Выполните следующую команду для запуска сервера:
npm start
Это запустит сервер на порту 3001
. Вы можете протестировать его, отправив запрос на localhost:3001/notes
.
Настройка веб-сайта
В этом разделе вы создадите веб-сайт с помощью Create React App (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, для добавления стилей на сайт.
Настройка Tailwind 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;
Вот краткое описание того, что происходит в этом фрагменте кода:
- Вы используете
useQuery
для получения заметок. Первый параметр, который он принимает, — это уникальный ключ, используемый для кэширования. Второй параметр — это функция, используемая для получения данных. Вы передаете ей функциюfetchNotes
. - Функция
fetchNotes
должна возвращать обещание, которое либо разрешает данные, либо выбрасывает ошибку. В функции вы посылаете запросGET
наlocalhost:3001/notes
, чтобы получить заметки. Если данные получены успешно, они возвращаются в функции выполненияthen
. - В возвращаемом JSX, если
isLoading
является истиной, отображается иконка загрузки. ЕслиisError
является истиной, отображается сообщение об ошибке. Еслиdata
успешно извлечена и в ней есть какие-либо данные, то отображаются заметки. - Вы также показываете кнопку с иконкой плюса для добавления новых заметок. Вы реализуете это позже.
Тестирование отображения заметок
Чтобы проверить то, что вы уже реализовали, убедитесь, что ваш сервер все еще работает, затем запустите ваш сервер приложений 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>
)
}
Вот краткое объяснение этой формы
- Эта форма действует как всплывающее окно. Она принимает реквизиты
isOpen
иsetIsOpen
для определения момента открытия формы и ее закрытия. - Вы используете
useQueryClient
, чтобы получить доступ к Query Client. Это необходимо для выполнения мутации. - Чтобы обработать добавление заметки на сервере и синхронизировать все данные в клиенте запросов, необходимо использовать хук
useMutation
. - Хук
useMutation
принимает 2 параметра. Первый — это функция, которая будет обрабатывать мутацию, в данном случае этоinsertNote
. Второй параметр — это объект опций. Вы передаете ему одну опциюonSuccess
, которая является функцией, запускаемой при успешном выполнении мутации. Вы используете ее для сброса полейtitle
иcontent
формы. - В
insertNote
вы посылаете запросPOST
наlocalhost:3001/notes
и передаете в телеtitle
иcontent
создаваемой заметки. Если параметр телаsuccess
, возвращаемый с сервера, равенfalse
, будет выдана ошибка, сигнализирующая о том, что мутация не удалась. - Если заметка добавлена успешно, вы изменяете кэшированное значение ключа
notes
с помощью методаqueryClient.setQueriesData
. Этот метод принимает ключ в качестве первого параметра и новые данные, связанные с этим ключом, в качестве второго параметра. Таким образом, данные обновляются везде, где они используются на вашем сайте. - В этом компоненте вы отображаете форму с 2 полями:
title
иcontent
. В форме вы проверяете возникновение ошибки с помощьюmutation.isError
и получаете доступ к ошибке с помощьюmutation.error
. - Вы обрабатываете отправку формы в функции
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.