Skip to content

Latest commit

 

History

History
324 lines (248 loc) · 13.4 KB

pagination.ru.mdx

File metadata and controls

324 lines (248 loc) · 13.4 KB

import { Callout } from 'nextra-theme-docs'

Пагинация

Пожалуйста, обновитесь до последней версии (≥ 0.3.0), чтобы использовать этот API. Предыдущий API useSWRPages является устаревшим.

SWR предоставляет специальный API useSWRInfinite для поддержки общепринятых UI шаблонов, таких как пагинация и бесконечная загрузка.

Когда использовать useSWR

Пагинация

Для начала, нам может НЕ понадобится useSWRInfinite, а вместо него использовать просто useSWR, если мы создаем что-то вроде этого:

import { Pagination } from '@components/diagrams/pagination'

...что является типичной UI пагинацией. Посмотрим, как это легко реализовать с помощью useSWR:

function App () {
  const [pageIndex, setPageIndex] = useState(0);

  // URL-адрес API включает индекс страницы, который является состоянием React.
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);

  // ... обработка состояния загразки и ошибки

  return <div>
    {data.map(item => <div key={item.id}>{item.name}</div>)}
    <button onClick={() => setPageIndex(pageIndex - 1)}>Назад</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Вперёд</button>
  </div>
}

Более того, мы можем создать абстракцию для этого «компонента страницы»:

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);

  // ... обработка состояния загразки и ошибки

  return data.map(item => <div key={item.id}>{item.name}</div>)
}

function App () {
  const [pageIndex, setPageIndex] = useState(0);

  return <div>
    <Page index={pageIndex}/>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Назад</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Вперёд</button>
  </div>
}

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

function App () {
  const [pageIndex, setPageIndex] = useState(0);

  return <div>
    <Page index={pageIndex}/>
    <div style={{ display: 'none' }}><Page index={pageIndex + 1}/></div>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Назад</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Вперёд</button>
  </div>
}

Всего с одной строкой кода мы получаем улучшенный UX. Хук useSWR настолько мощный, что он покрывает большинство сценариев.

Бесконечная загрузка

Иногда мы хотим создать интерфейс бесконечной загрузки с кнопкой «Загрузить ещё», которая добавляет данные в список (или делает это автоматически при прокрутке):

import { Infinite } from '@components/diagrams/infinite'

Чтобы реализовать это, нам нужно сделать динамическое количество запросов на этой странице. У хуков React есть пара правил, поэтому мы НЕ МОЖЕМ делать что-то вроде этого:

function App () {
  const [cnt, setCnt] = useState(1)

  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 Это не правильно! Как правило, вы не можете использовать хуки внутри цикла.
    const { data } = useSWR(`/api/data?page=${i}`)
    list.push(data)
  }

  return <div>
    {list.map((data, i) =>
      <div key={i}>{
        data.map(item => <div key={item.id}>{item.name}</div>)
      }</div>)}
    <button onClick={() => setCnt(cnt + 1)}>Загрузить ещё</button>
  </div>
}

Вместо этого мы можем использовать абстракцию <Page />, которую мы создали для этого:

function App () {
  const [cnt, setCnt] = useState(1)

  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }

  return <div>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Загрузить ещё</button>
  </div>
}

Продвинутые случаи

Однако, в некоторых продвинутых случаях, приведенное выше решение не работает.

Например, мы всё ещё реализуем тот же UI «Загрузить ещё», но нам также необходимо отображать число, показывающее, сколько всего элементов имеется. Мы больше не можем использовать решение <Page />, потому что UI верхнего уровня (<App />) нужны данные внутри каждой страницы:

function App () {
  const [cnt, setCnt] = useState(1)

  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }

  return <div>
    <p>??? элементов</p>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Загрузить ещё</button>
  </div>
}

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

Решить эту задачу нам может помочь новый хук useSWRInfinite.

useSWRInfinite

useSWRInfinite дает нам возможность запускать несколько запросов с помощью одного хука. Вот как это выглядит:

import useSWRInfinite from 'swr/infinite'

// ...
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

Подобно useSWR, этот новый хук принимает функцию, которая возвращает ключ запроса, функцию fetcher и опции. Он возвращает все значения, что и useSWR, включая 2 дополнительных значения: размер страницы и установщик размера страницы, как состояние React.

При бесконечной загрузке одна страница — это один запрос, и наша цель — получить несколько страниц и отобразить их.

Если вы используете версии SWR 0.x, `useSWRInfinite` необходимо импортировать из `swr`:
`import { useSWRInfinite } from 'swr'`

API

Параметры

  • getKey: функция, которая принимает индекс и данные предыдущей страницы, возвращает ключ страницы
  • fetcher: то же, что и fetcher-функция useSWR
  • options: принимает все опции, которые поддерживает useSWR, с 3 дополнительными опциями:
    • initialSize = 1: количество страниц, которые должны быть загружены изначально
    • revalidateAll = false: всегда пытаться ревалидировать все страницы
    • revalidateFirstPage = true: always try to revalidate the first page
    • persistSize = false: не сбрасывать размер страницы до 1 (или initialSize, если установлен), когда ключ первой страницы изменяется
Обратите внимание, что опцию `initialSize` нельзя изменять в жизненном цикле.

Возвращаемые значения

  • data: массив значений ответа выборки каждой страницы
  • error: то же , что и error в useSWR
  • isValidating: то же, что и isValidating в useSWR
  • mutate: то же, что и связанная функция мутации в useSWR, но манипулирует массивом данных
  • size: количество страниц, которые будут извлекаться и возвращаться
  • setSize: установить количество страниц, которые необходимо извлечь

Пример 1: API пагинации на основе индекса

Для обычных API на основе индекса:

GET /users?page=0&limit=10
[
  { name: 'Алиса', ... },
  { name: 'Вася', ... },
  { name: 'Катя', ... },
  ...
]
// Функция для получения ключа SWR каждой страницы,
// её возвращаемое значение будет принято в `fetcher`.
// Если возвращается `null`, запрос этой страницы не запускается.
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // достигнут конец
  return `/users?page=${pageIndex}&limit=10`                    // ключ SWR
}

function App () {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  if (!data) return 'loading'

  // Теперь мы можем подсчитать количество всех пользователей
  let totalUsers = 0
  for (let i = 0; i < data.length; i++) {
    totalUsers += data[i].length
  }

  return <div>
    <p>{totalUsers} users listed</p>
    {data.map((users, index) => {
      // `data` — это массив ответов API каждой страницы.
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Загрузить ещё</button>
  </div>
}

Функция getKey является основным отличием useSWRInfinite от useSWR. Она принимает индекс текущей страницы, а также данные с предыдущей страницы. Таким образом, оба API пагинации: на основе индекса, и на основе курсора хорошо поддерживаются.

Кроме того, data больше не является одним ответом API. Это массив из нескольких ответов API:

// `data` будет выглядеть вот так:
[
  [
    { name: 'Алиса', ... },
    { name: 'Вася', ... },
    { name: 'Катя', ... },
    ...
  ],
  [
    { name: 'Иван', ... },
    { name: 'Павел', ... },
    { name: 'Георгий', ... },
    ...
  ],
  ...
]

Пример 2: API пагинации на основе курсора или смещения

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

GET /users?cursor=123&limit=10
{
  data: [
    { name: 'Алиса' },
    { name: 'Вася' },
    { name: 'Катя' },
    ...
  ],
  nextCursor: 456
}

Мы можем изменить нашу функцию getKey на:

const getKey = (pageIndex, previousPageData) => {
  // достигнут конец
  if (previousPageData && !previousPageData.data) return null

  // первая страница, у нас нет `previousPageData`
  if (pageIndex === 0) return `/users?limit=10`

  // добавим курсор в endpoint API
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

Расширенные возможности

Вот пример, показывающий, как вы можете реализовать следующий функционал с помощью useSWRInfinite:

  • состояния загрузки
  • показ специального интерфейса, если он пуст
  • отключение кнопки «Загрузить ещё», если достигнут конец
  • изменяемый источник данных
  • обновление всего списка