Skip to content
Документация
Пагинация

Пагинация

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

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

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

Пагинация

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

...что является типичной 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 настолько мощный, что он покрывает большинство сценариев.

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

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

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

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, isLoading, 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: всегда пытаться ревалидировать первую страницу
    • persistSize = false: не сбрасывать размер страницы до 1 (или initialSize, если установлен), когда ключ первой страницы изменяется
    • parallel = false: загружать много страницы параллельно
💡

Обратите внимание, что опцию initialSize нельзя изменять в жизненном цикле.

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

  • data: массив значений ответа выборки каждой страницы
  • error: то же , что и error в useSWR
  • isLoading: то же, что и isLoading в 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`
}

Режим параллельной загрузки

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

Поведение useSWRInfinite по умолчанию — последовательная загрузка данных для каждой страницы, поскольку создание ключа основывается на ранее загруженных данных. Однако последовательная загрузка данных для большого количества страниц может не быть оптимальной, особенно если страницы не зависят друг от друга. Указание опции parallel как true позволит вам загружать страницы независимо и параллельно, что может значительно ускорить процесс загрузки.

// parallel = false (по умолчанию)
// страница1 ===> страница2 ===> страница3 ===> готово
//
// parallel = true
// страница1 ==> готово
// страница2 =====> готово
// страница3 ===> готово
//
// previousPageData всегда `null`
const getKey = (pageIndex, previousPageData) => {
  return `/users?page=${pageIndex}&limit=10`
}
 
function App () {
  const { data } = useSWRInfinite(getKey, fetcher, { parallel: true })
}
⚠️

Аргумент previousPageData функции getKey становится null, когда вы включаете опцию parallel.

Глобальная мутация с useSWRInfinite

useSWRInfinite хранит все данные страниц в кеше со специальным ключом кеша вместе с данными каждой страницы, поэтому вам нужно использовать unstable_serialize в swr/infinite, чтобы ревалидировать данные с глобальной мутацией.

import { useSWRConfig } from "swr"
import { unstable_serialize } from "swr/infinite"
 
function App() {
    const { mutate } = useSWRConfig()
    mutate(unstable_serialize(getKey))
}
⚠️

Как и следует из названия, unstable_serialize не является стабильным API, поэтому мы можем изменить его в будущем.

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

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

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