Первоначально опубликовано в моем личном блоге.
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
Вот для чего предназначен каждый из пакетов:
express
создать сервер с помощью Экспресс.cors
— это промежуточное ПО Express, используемое для обработки CORS на вашем сервере.body-parser
— это промежуточное ПО Express, используемое для анализа тела запроса.sqlite3
— это адаптер базы данных SQLite для Node.js.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 конечные точки:
/notes
конечная точка методаGET
для получения всех заметок./notes/:id
конечная точка методаGET
для получения заметки по идентификатору./notes
конечная точка методаPOST
для добавления примечания./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;
Вот кратко, что происходит в этом фрагменте кода:
- Вы используете
useQuery
для получения заметок. Первый параметр, который он принимает, — это уникальный ключ, используемый для кэширования. Второй параметр — это функция, используемая для получения данных. Вы передаете ему функциюfetchNotes
. useQuery
возвращает объект, который содержит много переменных. Здесь вы используете 4 из них:isLoading
— логическое значение, определяющее, извлекаются ли данные в данный момент;isError
— это логическое значение, которое определяет, произошла ли ошибка.data
— это данные, получаемые с сервера; аerror
— это сообщение об ошибке, еслиisError
верно.- Функция
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.