React Query 3: Руководство по получению и управлению данными

React Query 3 Изучение

Создание интерфейсных приложений CRUD часто начинается легко, а затем становится сложным по мере того, как вы продолжаете добавлять функции. Для каждой конечной точки API вам нужно будет иметь дело с управлением состоянием, синхронизацией, кэшированием и обработкой ошибок. В этой статье вы узнаете о библиотеке React Query и о том, как она может помочь решить все эти проблемы. Библиотека описывает себя как «отсутствующую библиотеку выборки данных», обеспечивающую «управление состоянием сервера» для React.

Мы будем использовать полный демонстрационный проект React Query, чтобы узнать об основных функциях, которые предоставляет библиотека. После этого вы сможете применить эти знания в своих собственных проектах. Для начала ознакомимся с рядом пунктов, прежде чем приступить к настройке проекта.

О React Query 3

React Query — это проект с открытым исходным кодом, созданный Таннером Линси. Последняя основная версия, React Query 3, была официально выпущена в декабре 2020 года. В этой новой версии были добавлены новые функции и улучшены существующие.

Вы должны знать, что есть ряд критических изменений в React Query 2.x, который был довольно популярен до выхода новой версии. Есть руководство по миграции, в котором четко объясняются эти изменения, поскольку вы, вероятно, встретите много устаревших руководств, написанных для более старой версии.

Новая версия предлагает значительные улучшения, и многие из ранее сообщенных ошибок были исправлены. Версия 3, хотя и готова к производству, все еще находится в стадии разработки, поскольку регулярно исправляются новые ошибки.

Предпосылки

Эта статья написана для промежуточных и продвинутых интерфейсных разработчиков, которые приобрели фундаментальные навыки и знания в следующих областях:

  • Реагировать
  • React Router
  • Реагировать на хуки
  • Получение данных REST API

В машинной среде вашего разработчика вам необходимо настроить следующее:

  • Node.js
  • Git
  • Клиент REST, такой как Postman, Insomnia или расширение REST VS Code.
Читайте также:  5 основных навыков для каждого веб-разработчика

Разобравшись с этим, давайте приступим к настройке демонстрационного проекта.

О проекте

Демо-проект, который мы будем анализировать, представляет собой интерфейсное приложение React, которое отображает данные, предоставленные сервером REST JSON API. Приложение состоит всего из пяти страниц, которые демонстрируют функции React Query, о которых мы будем изучать. Эти функции включают:

  • Базовый запрос
  • Запрос с разбивкой на страницы
  • Бесконечный запрос
  • Создать мутацию
  • Обновить мутацию
  • Удалить мутацию

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

React Query предоставляет гораздо больше функций, которые, к сожалению

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

Прежде чем мы начнем настройку, я считаю, что лучше вкратце ознакомиться с дополнительными зависимостями, используемыми в проекте. Это включает:

  • Vite : очень быстрый инструмент для сборки
  • WindiCSS : очень быстрый компилятор Tailwind CSS
  • React Hook Form : конструктор форм и библиотека проверки с использованием React-хуков
  • React Modal : доступный модальный компонент
  • Axios : HTTP-клиент на основе обещаний для браузеров
  • Сервер JSON : полный поддельный сервер REST API

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

# Clone the project
git clone git@github.com:sitepoint-editors/react-query-demo.git

# Navigate to project directory
cd react-query-demo

# Install package dependencies
npm install

# Setup database file for `json-server`
cp api/sample.db.json api/db.json

# Start the `json-server`
npm run json-server

Файл базы данных, используемый объектом, json-serverсодержит массив пользователей. Когда вы выполняете npm run json-server, на порту запускается поддельный сервер API 3004. Вы можете получить доступ к данным пользователей через. Выполнение запроса GET даст следующий пример ответа JSON:

[
  {
    "id": 1,
    "first_name": "Siffre",
    "last_name": "Timm",
    "email": "stimmes0@nasa.govz",
    "gender": "Male"
  },
  {
    "id": 2,
    "first_name": "Fonzie",
    "last_name": "Coggen",
    "email": "fcoggen1@weather.com",
    "gender": "Female"
  },
  {
    "id": 3,
    "first_name": "Shell",
    "last_name": "Kos",
    "email": "skos2@prweb.com",
    "gender": "Female"
  }
]

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

# In another terminal, start the React dev server
npm run dev

Перейдите в свой браузер и откройте http: // localhost: 3000, чтобы получить доступ к приложению. У вас должно быть такое же впечатление, как показано на превью выше. Убедитесь, что вы выполняете следующие задачи, чтобы тщательно изучить возможности приложения:

  • Просмотрите страницу базового запроса (домашняя страница).
  • Посетите страницу с разбивкой на страницы и используйте кнопки » Назад» и » Далее».
  • Посетите страницу Infinite и взаимодействуйте с кнопкой Загрузить еще.
  • Вернитесь на страницу базового запроса и нажмите кнопку » Создать пользователя». Вы будете перенаправлены на страницу создания пользователя. Заполните форму и нажмите кнопку Сохранить.
  • В таблице пользователей найдите значок » Изменить». Нажмите здесь. Вы попадете на страницу редактирования пользователя. Внесите любые изменения, которые вам нравятся, затем нажмите кнопку » Сохранить».
  • В таблице пользователей найдите значок Удалить. Нажмите здесь. Откроется модальное диалоговое окно с просьбой подтвердить действие удаления. Нажмите кнопку » Удалить«, чтобы подтвердить.

После того, как мы выполнили все вышеперечисленные задачи, мы можем приступить к разбивке проекта. Изучите структуру проекта, чтобы знать, где расположены каждый компонент и представление. В статье я буду предлагать их урезанные версии, чтобы вы могли понять основы использования React Query в своих проектах.

Примечание: в урезанных версиях classNamesудалено локальное состояние и другие компоненты пользовательского интерфейса, которые не являются предметом обсуждения.

Установка React Query

React Query можно установить в пустой или существующий проект React с помощью следующей команды:

npm install react-query

Пакет поставляется со всем, что вам нужно, включая служебную функцию Devtools, которую мы рассмотрим в следующем разделе. После установки пакета вам необходимо обновить самый верхний компонент, — App.jsx- следующим образом:

import { QueryClient, QueryClientProvider } from "react-query";

function App() {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      /* place application containers/views here */
    </QueryClientProvider>
  );
}

export default App;

Любой дочерний компонент QueryClientProviderсможет получить доступ к хукам, предоставляемым библиотекой React Query. В этой статье мы будем использовать следующие хуки:

  • useQuery
  • useInfiniteQuery
  • useMutation
  • useQueryClient

Вот обновленная (упрощенная) версия App.jsxдочерних представлений, которые мы будем использовать:

import { QueryClient, QueryClientProvider } from "react-query";

import BasicQuery from "./views/BasicQuery";
import InfiniteQuery from "./views/InfiniteQuery";
import PaginatedQuery from "./views/PaginatedQuery";
import CreateUser from "./views/CreateUser";
import EditUser from "./views/EditUser";

function App() {
  const queryClient = new QueryClient();

  return (
    <QueryClientProvider client={queryClient}>
      <Switch>
        <Route path="/" exact>
          <BasicQuery />
        </Route>
        <Route path="/paginated">
          <PaginatedQuery />
        </Route>
        <Route path="/infinite">
          <InfiniteQuery />
        </Route>
        <Route path="/user/create">
          <CreateUser />
        </Route>
        <Route path="/user/edit/:id">
          <EditUser />
        </Route>
      </Switch>
    </QueryClientProvider>
  );
}

export default App;

Компоненты пользовательского интерфейса

Прежде чем мы перейдем к следующему разделу, я думаю, что лучше всего иметь обзор основных компонентов пользовательского интерфейса, используемых в проекте для отображения, создания и обновления пользовательских данных. Начнем с components/UserTable.jsx. Это таблица компонент отображает пользовательские данные и используется BasicQuery.jsxи PaginatedQuery.jsxстраницы. Для этого требуется одна опора, множество пользователей. Ниже представлена ​​урезанная версия готового файла:

import React, { useState, useContext } from "react";
import { Link } from "react-router-dom";
import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete";

function UserTable({ users }) {
  const rows = users.map((user, index) => (
    <tr key={index}>
      <td>{user.id}</td>
      <td>{user.first_name}</td>
      <td>{user.last_name}</td>
      <td>{user.email}</td>
      <td>{user.gender}</td>
      <td>
        <Link to={`/user/edit/${user.id}`}>
          <EditIcon />
        </Link>
        <button onClick={() => showDeleteModal(user.id)}>
          <DeleteIcon />
        </button>
      </td>
    </tr>
  ));

  return (
    <React.Fragment>
      <div>
        <Link to="/user/create">Create User</Link>
      </div>
      <table>
        <thead>
          <tr>
            <th>Id</th>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>
            <th>Gender</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>{rows}</tbody>
      </table>
    </React.Fragment>
  );
}

Далее мы посмотрим на components/UserForm.jsx. Эта форма компонент используется views/CreateUser.jsxи views/EditUser.jsxстраницы для выполнения своих задач. Ниже представлена ​​упрощенная версия компонента:

import React from "react";
import { useForm } from "react-hook-form";
import { useHistory } from "react-router-dom";

import "./form.css";

function UserForm({ user, submitText, submitAction }) {
  const {
    register,
    formState: { errors },
    handleSubmit,
  } = useForm({
    defaultValues: user || {},
  });

  const history = useHistory();

  return (
    <div>
      <form onSubmit={handleSubmit(submitAction)}>
        {user && (
          <section className="field">
            <label htmlFor="id">User Id</label>
            <input type="text" name="id" value={user.id} disabled />
          </section>
        )}

        <section className="field">
          <div>
            <label htmlFor="first_name">First Name</label>
            <input
              type="text"
              {...register("first_name", { required: true })}
            />
            <span className="errors">
              {errors.first_name && "First name is required"}
            </span>
          </div>
          <div>
            <label htmlFor="last_name">Last Name</label>
            <input type="text" {...register("last_name", { required: true })} />
            <span className="errors">
              {errors.last_name && "Last name is required"}
            </span>
          </div>
        </section>

        <section className="field">
          <label htmlFor="email">Email</label>
          <input
            type="email"
            {...register("email", { required: true, pattern: /^\S+@\S+$/i })}
          />
          <span className="errors">
            {errors.email &&
              errors.email.type === "required" &&
              "Email is required"}
            {errors.email &&
              errors.email.type === "pattern" &&
              "Provide a valid email address"}
          </span>
        </section>

        <section className="field">
          <label htmlFor="gender">Gender</label>
          <select {...register("gender", { required: true })}>
            <option value=""></option>
            <option value="Male">Male</option>
            <option value="Female">Female</option>
          </select>
          <span className="errors">
            {errors.gender && "Gender is required"}
          </span>
        </section>

        <div>
          <button type="submit"> {submitText} </button>
          <button type="button" onClick={() => history.goBack()}>
            Back
          </button>
        </div>
      </form>
    </div>
  );
}

export default UserForm;

UserFormКомпонент предназначен для выполнения проверки на представленных данных пользователя. Ожидается следующий реквизит:

  • user: объект данных (необязательно)
  • submitText: текстовое значение для кнопки » Отправить»
  • submitAction: функция, обрабатывающая отправку формы

В следующем разделе мы начнем рассматривать основные функции React Query

В следующем разделе мы начнем рассматривать основные функции React Query.

Базовый запрос

Получить данные с помощью React Query довольно просто. Все, что вам нужно сделать, это определить функцию выборки, а затем передать ее в качестве параметра useQueryмутации. Вы можете увидеть пример views/BasicQuery.jsxстраницы ниже:

import React from "react";
import { useQuery } from "react-query";

import UserTable from "../components/UserTable";

function BasicQuery() {
  const fetchAllUsers = async () =>
    await (await fetch("http://localhost:3004/users")).json();

  const { data, error, status } = useQuery("users", fetchAllUsers);

  return (
    <div>
      <h2>Basic Query Example</h2>
      <div>
        {status === "error" && <div>{error.message}</div>}

        {status === "loading" && <div>Loading...</div>}

        {status === "success" && <UserTable users={data} />}
      </div>
    </div>
  );
}

export default BasicQuery;

Давайте разберемся:

  1. Сначала мы импортируем useQueryчерез оператор import { useQuery } from «react-query».
  2. Затем мы объявляем функцию обещания, fetchAllUsersкоторая извлекает данные с нашего поддельного сервера JSON API.
  3. Затем мы запускаем useQueryфункцию перехвата. Обязательны следующие параметры:
    1. ключевой запрос, который может быть либо строкой или массивом. Он используется для идентификации и отслеживания результатов запроса в целях кэширования.
    2. функция запроса, который должен возвращать обещание, что либо данные решительность или выдаст ошибку.
  4. useQueryФункция возвращает следующие переменные состояния:
    1. data: это результат функции выборки (обещания).
    2. error: если выдается ошибка, это будет установлено. В противном случае он равен нулю, если запрос на выборку успешен.
    3. status: Это строка, которая может иметь значение idle, loading, errorили success.

useQueryКрюк принимает гораздо больше параметров и возвращает намного больше переменных, которые были задокументированы в React документации Запроса. Приведенный выше пример предназначен для демонстрации минимальных настроек, необходимых для выполнения запроса API с использованием библиотеки.

Также обратите внимание на то, как statusпеременная является реактивной. Изначально установлено значение loading. Затем, когда запрос будет успешным, он будет установлен в значение success, в результате чего React повторно отрендерит компонент и обновит пользовательский интерфейс.

Запрос отдельной записи

Запрос одной записи может быть выполнен с использованием аналогичного синтаксиса, который использовался в предыдущем разделе. Разница здесь в том, что:

  • вам нужно передать аргумент функции выборки через анонимную функцию
  • вам нужно уникальное имя запроса для каждой отдельной записи, что вы можете сделать с помощью массива: [queryName, {params}]
function() {
   const fetchUser = async (id) =>
    await (await fetch(`http://localhost:3004/users/${id}`)).json();

  const { data, error, status } = useQuery(["user", { id }], (id) =>
    fetchUser(id)
  );

  return (...)
}

Однако есть альтернативный способ передачи аргументов. Рассмотрим следующий код:

const { data, error, status } = useQuery(["user", { id }], fetchUser);

Используя приведенный выше синтаксис, вам необходимо изменить fetchUserфункцию для приема queryKeyобъекта следующим образом:

const fetchUser = async ({ queryKey }) => {
  const [_key, { id }] = queryKey;
  const response = await fetch(`http://localhost:3004/users/${id}`);

  if (!response.ok) {
    throw new Error(response.statusText);
  }

  return response.json();
};

Поскольку мы используем Fetch API, ответы 404 не считаются ошибками. Вот почему нам нужно написать дополнительную логику, чтобы справиться с этой ситуацией. При использовании клиентской библиотеки Axios API выполнение этой дополнительной проверки не требуется.

Посмотрите, views/EditUser.jsxкак был реализован весь код. Там есть код мутации, который мы обсудим позже в статье.

Devtools

Отладку кода React Query можно легко выполнить с помощью Devtools. Это утилита, которая визуализирует внутреннюю работу React Query в реальном времени по мере выполнения кода вашего приложения. Настройка происходит следующим образом:

import { ReactQueryDevtools } from "react-query/devtools";

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

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

Когда вы запустите приложение, в нижнем левом углу появится значок

Как вы можете видеть на скриншоте выше, вы можете наблюдать ряд свойств, которые помогут вам отладить свое приложение. В следующем разделе мы рассмотрим несколько вариантов конфигурации, которые помогут вам разобраться в некоторых этих свойствах и действиях.

Конфигурация

В приложении React Query, когда страница загружается в первый раз, библиотека извлекает данные из API, представляет их вам и затем кэширует. Когда это произойдет, вы заметите сообщение «загрузка».

Когда вы посещаете эту страницу во второй раз, React Query сначала возвращает данные из кеша, а затем выполняет выборку в фоновом режиме. После завершения асинхронного процесса данные на странице обновляются. Когда это произойдет, вы вообще не увидите сообщения о загрузке.

Кэширование позволяет вашему клиентскому приложению работать быстро, особенно если у вас медленный сервер API. Однако это может привести к ситуации, когда пользователи могут начать работать с устаревшими данными. В React Query это называется устаревшими данными.

Есть несколько вариантов конфигурации, которые помогут вам оптимизировать приложение по производительности или надежности:

  • cacheTime : по умолчанию 5 минут или 300000 миллисекунд.
  • staleTime : по умолчанию 0 миллисекунд

cacheTimeопределяет, как долго данные могут храниться в кэше до их удаления. staleTimeопределяет, сколько времени требуется, чтобы данные устарели. Когда данные становятся устаревшими, они помечаются для повторной выборки. Это произойдет в следующий раз, когда пользователь снова зайдет на страницу или перефокусирует окно / вкладку браузера.

Увеличение staleTimeзначения может повысить производительность приложения, если вы знаете, что извлекаемые данные имеют низкую вероятность обновления. Вы можете определить эти настройки, передав ловушке третий аргумент useQuery:

 function Todos() {

   const result = useQuery('todos', () => fetch('/todos'), {
     staleTime: 60 * 1000 // 1 minute
     cacheTime: 60 * 1000 * 10 // 10 minutes
   })

 }

Вы также можете установить Infinityлюбое свойство. Это отключит сборку мусора cacheTimeи предотвратит устаревание данных staleTime.

Запросы с разбивкой на страницы

В примере с базовым запросом все 250 записей были загружены одновременно. Более удобный подход — разбить данные на страницы. Этого можно добиться с помощью useQueryкрючка. В предыдущих версиях React Query это делалось с помощью usePaginateQueryловушки, которая больше не доступна в React Query 3.

Реализация пагинации фактически начинается с внутреннего сервера API. К счастью для нас, json-serverесть поддержка пагинации. Чтобы получить доступ к этой функции, вам необходимо добавить следующие параметры к URL-адресу конечной точки:

  • _page: номер страницы
  • _limit: количество записей на странице

Пример: http: // localhost: 3004 / users? _Page = 5 & _limit = 10.

Давайте теперь посмотрим, как осуществляется разбивка на страницы с помощью useQueryхука:

import React, { useState } from "react";
import { useQuery } from "react-query";

const pageLimit = 15;

const fetchUsers = async (page = 1) =>
  await (
    await fetch(`http://localhost:3004/users?_page=${page}&_limit=${pageLimit}`)
  ).json();

function Users() {
  const [page, setPage] = useState(1);
  const { data } = useQuery(["paginatedUsers", page], () => fetchUsers(page), {
    keepPreviousData: true,
  });
}

Этот пример очень похож на базовый запрос, который мы рассматривали ранее. Однако есть несколько ключевых отличий:

  1. Функция обещания fetchUsersтеперь принимает целочисленный pageпараметр. Размер страницы устанавливается с помощью переменной pageLimit.
  2. useQueryКрючок подпись выглядит совсем другое:
    1. Первый параметр — это массив [«paginatedUsers», page]. Это необходимо для отслеживания данных каждой страницы отдельно.
    2. Второй параметр — анонимная функция. Он определен таким образом, чтобы передать pageаргумент fetchUsersфункции.
    3. Третий аргумент — это конфигурация объекта, в которую мы можем передать несколько настроек. В этом случае установка для keepPreviousDataсвойства значения true сообщает React Query о необходимости кэширования ранее полученных данных. По умолчанию этот параметр имеет значение false, что приводит к обновлению ранее просмотренных страниц.

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

import { useQuery, useQueryClient } from "react-query";

function Example() {
  const queryClient = useQueryClient();
  const [page, setPage] = React.useState(0);

  // Prefetch the next page!
  React.useEffect(() => {
    if (data?.hasMore) {
      queryClient.prefetchQuery(["paginatedUsers", page + 1], () =>
        fetchUsers(page + 1)
      );
    }
  }, [data, page, queryClient]);
}

Обратите внимание, что data.hasMoreэто свойство, специфичное для API сервера. К сожалению, наш поддельный сервер API этого не поддерживает. При использовании реальной серверной части API вы, вероятно, получите ответ, который выглядит примерно так:

{
  "items": [
    {
      "lives": 9,
      "type": "tabby",
      "name": "Bobby"
    },
    {
      "lives": 2,
      "type": "Ginger",
      "name": "Garfield"
    },
    ...
  ],
  "meta": {
    "itemCount": 10,
    "totalItems": 20,
    "itemsPerPage": 10,
    "totalPages": 5,
    "currentPage": 2
  },
  "links" : {
    "first": "http://cats.com/cats?limit=10",
    "previous": "http://cats.com/cats?page=1&limit=10",
    "next": "http://cats.com/cats?page=3&limit=10",
    "last": "http://cats.com/cats?page=5&limit=10"
  }
}

Обратите внимание, что в структуре тела ответа есть дополнительные метаданные, которые могут помочь в проверке кнопок нумерации страниц. При json-serverвыполнении запроса с разбивкой на страницы мы получаем следующий результат:

HTTP/1.1 200 OK
X-Powered-By: Express
Vary: Origin, Accept-Encoding
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
X-Total-Count: 250
Access-Control-Expose-Headers: X-Total-Count, Link
Link: <http://localhost:3004/users?_page=1&_limit=10>; rel="first", <http://localhost:3004/users?_page=4&_limit=10>; rel="prev", <http://localhost:3004/users?_page=6&_limit=10>; rel="next", <http://localhost:3004/users?_page=25&_limit=10>; rel="last"
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"567-FwlexqEes6H/+Xt0qULv2G4aUN4"
Content-Encoding: gzip
Date: Thu, 29 Apr 2021 15:24:58 GMT
Connection: close
Transfer-Encoding: chunked

[
  {
    "id": 42,
    "first_name": "Whitby",
    "last_name": "Damrell",
    "email": "wdamrell15@i2i.jp",
    "gender": "Female"
  },
  {
    "id": 43,
    "first_name": "Fairleigh",
    "last_name": "Staner",
    "email": "fstaner16@tripod.com",
    "gender": "Female"
  },
  ...
]

Обратите внимание на атрибут Link, указанный в разделе заголовка. Мы можем использовать эту информацию для написания лучшего кода разбивки на страницы. К сожалению, формат данных не подходит для использования с кодом JavaScript. В следующем разделе мы узнаем, как с этим справиться. На данный момент мы просто воспользуемся простой проверкой, чтобы определить, достигли ли мы последней страницы.

Ниже представлена ​​урезанная версия финальной views/PaginatedQuery.jsxстраницы:

import React, { useState } from "react";
import { useQuery } from "react-query";

import UserTable from "../components/UserTable";

const pageLimit = 15;

const fetchUsers = async (page = 1) => {
  const response = await fetch(
    `http://localhost:3004/users?_page=${page}&_limit=${pageLimit}`
  );
  return response.json();
};

function PaginatedQuery() {
  const [page, setPage] = useState(1);
  const { data, isLoading, isError, status, error } = useQuery(
    ["paginatedUsers", page],
    () => fetchUsers(page),
    {
      keepPreviousData: true,
    }
  );

  const prevPage = () => {
    if (page > 1) setPage(page - 1);
  };

  const nextPage = () => {
    setPage(page + 1);
  };

  return (
    <div>
      <h2>Paginated Query Example</h2>
      <div>
        {isError && <div>{error.message}</div>}

        {isLoading && <div>Loading...</div>}

        {status === "success" && <UserTable users={data} />}
      </div>

      {/* start of pagination buttons */}
      <div>
        <button onClick={prevPage} disabled={page <= 1}>
          Prev
        </button>
        <span>Page: {page}</span>
        <button onClick={nextPage} disabled={data && data.length < pageLimit}>
          Next
        </button>
      </div>
      {/* end of pagination buttons */}
    </div>
  );
}

export default PaginatedQuery;

В приведенном выше примере кода мы добавили функции и кнопки для обеспечения взаимодействия с разбивкой на страницы. Обратите внимание, что мы также с использованием isLoadingи isErrorсостояниями, которые являются просто удобными альтернативами использования statusсостояния.

Ниже скриншот PaginatedQueryстраницы.

Ниже скриншот PaginatedQueryстраницы

Бесконечные запросы

До сих пор мы использовали useQueryловушку только для управления извлечением данных из нашего внутреннего API. В этом разделе вы узнаете, как реализовать функцию «бесконечной прокрутки». Пользователи должны будут нажать кнопку » Загрузить еще», чтобы запустить выборку данных.

Для этого мы будем использовать useInfiniteQueryловушку, которая очень похожа на useQueryловушку, но имеет несколько ключевых отличий. Во-первых, вам понадобится внутренний API, поддерживающий разбиение курсора на страницы:

fetch("/api/projects?cursor=0");

К сожалению, наша json-serverсерверная часть этого не делает. Для наших целей мы реализуем обходной путь, используя существующую поддержку разбивки на страницы, чтобы выполнять бесконечные запросы. Давайте посмотрим, как мы определяем нашу fetchUsersфункцию:

const pageLimit = 5;

const fetchUsers = ({ pageParam = 1 }) =>
  axios.get(
    `http://localhost:3004/users?_page=${pageParam}&_limit=${pageLimit}`
  );

Функция fetchUsersаналогична его PaginatedQueryверсии, за исключением того, что мы возвращаем полный Responseобъект вместо разрешенного массива данных. Мы сделали это для того, чтобы иметь доступ к Linkобъекту, указанному в заголовке:

Link: <http://localhost:3004/users?_page=1&_limit=10>; rel="first",
<http://localhost:3004/users?_page=2&_limit=10>; rel="next",
<http://localhost:3004/users?_page=25&_limit=10>; rel="last"

LinkЗаголовок возвращает строку, которая содержит мета — данные о текущей позиции страницы. При использовании Axios мы можем получить доступ к вышеуказанной информации, используя response.headers.link. При использовании Fetch API для выполнения запроса используйте response.headers.get(’Link’)для доступа к нему.

Затем нам нужно преобразовать Linkметаданные в формат, к которому мы можем легко получить доступ в коде. Мы можем выполнить преобразование, используя эту функцию, описанную в статье Джоша Фрэнка :

const parseLinkHeader = (linkHeader) => {
  const linkHeadersArray = linkHeader
    .split(", ")
    .map((header) => header.split("; "));
  const linkHeadersMap = linkHeadersArray.map((header) => {
    const thisHeaderRel = header[1].replace(/"/g, "").replace("rel=", "");
    const thisHeaderUrl = header[0].slice(1, -1);
    return [thisHeaderRel, thisHeaderUrl];
  });
  return Object.fromEntries(linkHeadersMap);
};

Когда мы передаем Linkв функцию строку заголовка, мы получаем следующий объект JavaScript:

{
  first: "http://localhost:3004/users?_page=1&_limit=5",
  next: "http://localhost:3004/users?_page=2&_limit=5",
  last: "http://localhost:3004/users?_page=50&_limit=5"
}

Теперь мы можем извлечь значение для следующей страницы с помощью функции URLSearch. Вам нужно будет указать частичный URL-адрес в том формате, ?_page=2&_limit=5в котором он будет работать. Вот фрагмент кода, из которого мы извлекаем nextPageзначение:

const nextPageUrl = parseLinkHeader(response.headers.link)["next"];
// split URL string
const queryString = nextPageUrl.substring(
  nextPageUrl.indexOf("?"),
  nextPageUrl.length
); // returns '?_page=2&_limit=5'
const urlParams = new URLSearchParams(queryString);
const nextPage = urlParams.get("_page"); // returns 2

Используя код, который мы определили до сих пор, у нас теперь есть обходной путь функции «курсора» для нашей поддельной серверной части API. Вам, вероятно, будет легче с реальной серверной частью API, которая поддерживает разбиение курсора на страницы. Имея эту логику, мы можем определить наши useInfiniteQuery:

const {
  data,
  error,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  status,
} = useInfiniteQuery("infiniteUsers", fetchUsers, {
  getNextPageParam: (lastPage) => {
    // The following code block is specific to json-server api
    const nextPageUrl = parseLinkHeader(lastPage.headers.link)["next"];
    if (nextPageUrl) {
      const queryString = nextPageUrl.substring(
        nextPageUrl.indexOf("?"),
        nextPageUrl.length
      );
      const urlParams = new URLSearchParams(queryString);
      const nextPage = urlParams.get("_page");
      return nextPage;
    } else {
      return undefined;
    }
  },
});

Приведенный выше фрагмент кода выглядит сложным, поэтому позвольте мне пояснить useInfiniteQueryвам синтаксис:

const { ... } = useInfiniteQuery(queryKey, queryFn, {...options})

Мы должны предоставить только три аргумента:

  • Первый аргумент — это queryKey.
  • Второй аргумент — queryFnэто функция обещания, которая выбирает данные с разбивкой на страницы курсора.
  • Третий аргумент — это объект конфигурации JavaScript, в котором вы определяете такие параметры, как staleTimeи cacheTime.

В случае useInfiniteQuery, вы должны предоставить функцию, вызываемую getNextPageParamдля работы кнопки бесконечной прокрутки. Эта функция определяет следующую страницу для загрузки. Он имеет следующий синтаксис:

{
  getNextPageParam: (lastPage, allPages) => {
    // lastPage: the last page(in our case last `Response` object) fetched by `fetchUsers` function
    // allPages: List of all pages that have already been fetched
    // return int|undefined : return `nextPage` as integer. Return `undefined` when there are no more pages
  };
}

Прочтите комментарии, чтобы понять назначение входов и выходов функции. Давайте теперь посмотрим на дополнительные состояния, которые возвращает ловушка:

  • data: возвращает массив страниц, data.pages[]
  • fetchNextPage: когда эта функция выполняется, она загружает следующую страницу, полагаясь на работу getNextPageParamфункции
  • hasNextPage: возвращает истину, если есть следующая страница
  • isFetchingNextPage: возвращает истину при выборке следующей страницы с помощью fetchNextPage

Ниже приведен фрагмент того, как возвращаемые состояния используются для определения нашей Load moreкнопки:

<button
  onClick={() => fetchNextPage()}
  disabled={!hasNextPage || isFetchingNextPage}
>
  Load More...
</button>

В data.pages[]массиве каждый pageэлемент представляет собой массив, содержащий записи данных. Каждый раз, когда пользователь нажимает кнопку « Загрузить еще» , pageк data.pages[]массиву добавляется новый элемент . Нам нужно определить новую функцию для извлечения записей из этой вложенной структуры. Обратите внимание, что в этом случае каждый pageявляется Responseобъектом Axios , поэтому нам нужно указать page.dataдоступ к каждой записи пользователя.

Ниже приведен фрагмент кода, который мы будем использовать для сопоставления каждого пользователя с <li>тегом:

userList = data.pages.map((page, index) => (
  <React.Fragment key={index}>
    {page.data.map((user) => (
      <li key={user.id}>
        {user.id}. {user.first_name} {user.last_name}
      </li>
    ))}
  </React.Fragment>
));

К настоящему моменту у вас должно быть фундаментальное понимание того, как использовать useInfiniteQueryкрючок. Давайте теперь посмотрим, как все views/InfiniteQuery.jsxвыглядит:

import React from "react";
import { useInfiniteQuery } from "react-query";
import axios from "axios";

function InfiniteQuery() {
  const pageLimit = 5;

  const fetchUsers = ({ pageParam = 1 }) =>
    axios.get(
      `http://localhost:3004/users?_page=${pageParam}&_limit=${pageLimit}`
    );

  const parseLinkHeader = (linkHeader) => {
    const linkHeadersArray = linkHeader
      .split(", ")
      .map((header) => header.split("; "));
    const linkHeadersMap = linkHeadersArray.map((header) => {
      const thisHeaderRel = header[1].replace(/"/g, "").replace("rel=", "");
      const thisHeaderUrl = header[0].slice(1, -1);
      return [thisHeaderRel, thisHeaderUrl];
    });
    return Object.fromEntries(linkHeadersMap);
  };

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery("infiniteUsers", fetchUsers, {
    getNextPageParam: (lastPage) => {
      // The following code block is specific to json-server api
      const nextPageUrl = parseLinkHeader(lastPage.headers.link)["next"];
      if (nextPageUrl) {
        const queryString = nextPageUrl.substring(
          nextPageUrl.indexOf("?"),
          nextPageUrl.length
        );
        const urlParams = new URLSearchParams(queryString);
        const nextPage = urlParams.get("_page");
        return nextPage;
      } else {
        return undefined;
      }
    },
  });

  let userList;

  if (data) {
    userList = data.pages.map((page, index) => (
      <React.Fragment key={index}>
        {page.data.map((user) => (
          <li key={user.id}>
            {user.id}. {user.first_name} {user.last_name}
          </li>
        ))}
      </React.Fragment>
    ));
  }

  return (
    <div>
      <h2>Infinite Query</h2>
      <div>
        {error && <div>An error occurred: {error.message}</div>}

        {isFetchingNextPage && <div>Fetching Next Page...</div>}

        {status === "success" && <ul className="my-8 ml-4">{userList}</ul>}
      </div>
      <div>
        <button
          onClick={() => fetchNextPage()}
          disabled={!hasNextPage || isFetchingNextPage}
        >
          Load More...
        </button>
      </div>
    </div>
  );
}

export default InfiniteQuery;

Надеюсь, к настоящему времени завершенный код должен иметь смысл, поскольку все разделы были объяснены. Ниже приведен снимок экрана со страницей «Пример бесконечного запроса». Я сократил количество db.jsonпользователей до 13, чтобы продемонстрировать результаты ниже:

Я сократил количество db.jsonпользователей до 13

Обратите внимание, что кнопка » Загрузить еще» неактивна, так как мы достигли последней страницы. Это знаменует конец нашего исследования ловушек запросов. Давайте посмотрим, как мы можем достичь функциональности CRUD с помощью библиотеки React Query.

Мутации

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

Для useMutationловушки требуется только функция обещания, которая будет отправлять данные во внутренний API. Он вернет следующие состояния:

  • isLoading: возвращает истину во время выполнения асинхронной операции
  • isError: возвращает истину, если произошла ошибка
  • error: возвращает объект ошибки, если он присутствует
  • isSuccess: возвращает истину после того, как мутация стала успешной

Все, что вам нужно сделать, чтобы выполнить собственно мутационное действие, — это выполнить mutation.mutate(data). Вы можете заключить его как функцию и назначить его событию нажатия кнопки.

Ниже приведен снимок views/CreateUser.jsxстраницы. Вы можете увидеть, как каждая переменная состояния использовалась для визуализации различных элементов пользовательского интерфейса.

import { useMutation } from "react-query";
import axios from "axios";
import { Redirect } from "react-router-dom";
import UserForm from "../components/UserForm";

const postUser = async (newUser) =>
  await (await axios.post("http://localhost:3004/users", newUser)).data;

function CreateUser() {
  const mutation = useMutation((newUser) => postUser(newUser));
  const { isLoading, isError, error, isSuccess } = mutation;

  const onSubmit = async (data) => {
    mutation.mutate(data);
  };

  if (isSuccess) {
    return <Redirect to="/" />;
  }

  return (
    <div>
      <h2>New User</h2>

      {isError && <div>An error occurred: {error.message}</div>}

      {isLoading && <div>Loading...</div>}

      <UserForm submitText="Create" submitAction={onSubmit} />
    </div>
  );
}

Мутация для действий обновления и удаления аналогична. Единственное отличие — это функция обещания, которую вы предоставляете, и требуемые аргументы.

Пример обновления мутации:

const mutation = useMutation((updatedUser) =>
  axios.put(`http://localhost:3004/users/${id}`, updatedUser)
);

Пример удаления мутации:

const deleteMutation = useMutation((id) =>
  axios.delete(`http://localhost:3004/users/${id}`)
);

Если ваш код мутации выполняется на странице, где отображаются данные вашего запроса, вы заметите, что после фиксации ничего не изменится. Чтобы вызвать повторную выборку данных после успешной мутации, вам необходимо выполнить queryClient.invalidateQueries()функцию. См. Пример ниже о том, как его назвать:

import { useMutation, useQueryClient } from "react-query";

function UserTable() {
  const deleteMutation = useMutation(
    (id) => axios.delete(`http://localhost:3004/users/${id}`),
    {
      onSuccess: () => {
        queryClient.invalidateQueries();
      },
    }
  );
}

Проверьте полный справочный документ для, useMutationчтобы узнать обо всех штатах и функциях, которые она поддерживает.

Заключение

В заключение, React Query — отличная библиотека для управления состоянием сервера, которая упрощает получение данных. Мы еще не рассмотрели множество других функций, которые могут помочь вам реализовать еще более продвинутые варианты использования. Если вам интересно, есть ли другие альтернативы React Query, вам следует ознакомиться с их руководством по сравнению, чтобы получить подробное представление о том, какие функции предлагает каждая библиотека.

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