Учебник по запросам в React (TanStack) для начинающих

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

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
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

Создайте файл 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;
Вход в полноэкранный режим Выйти из полноэкранного режима

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

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

Тестирование отображения заметок

Чтобы проверить то, что вы уже реализовали, убедитесь, что ваш сервер все еще работает, затем запустите ваш сервер приложений 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)
Enter fullscreen mode Выйти из полноэкранного режима

Затем добавьте новую функцию 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.

Оцените статью
devanswers.ru
Добавить комментарий