Skip to content
Docs
Paginación

Paginación

Por favor, actualice a la última versión (≥ 0.3.0) para utilizar esta API. La anterior API useSWRPages ha quedado obsoleta.

SWR proporciona una API dedicada useSWRInfinite para admitir patrones de UI comunes como la paginación y la carga infinita.

Cuándo utilizar useSWR

Paginación

En primer lugar, es posible que NO necesitemos useSWRInfinite, sino que podemos utilizar simplemente useSWR si estamos construyendo algo como esto:

...que es un típico UI de paginación. Veamos cómo se puede implementar fácilmente con useSWR:

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // La URL de la API incluye el índice de la página, que es un React state.
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
 
  // ... manejar los estados de carga y error
 
  return <div>
    {data.map(item => <div key={item.id}>{item.name}</div>)}
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

Además, podemos crear una abstracción para este "page component":

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);
 
  // ... manejar los estados de carga y error
 
  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)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

Gracias a la caché de SWR, tenemos la ventaja de precargar la siguiente página. La página siguiente se presenta dentro de un hidden div, por lo que SWR activará la obtención de datos de la página siguiente. Cuando el usuario navega a la siguiente página, los datos ya están allí:

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)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

Con sólo 1 línea de código, conseguimos una UX mucho mejor. El hook useSWR es tan potente, que la mayoría de los escenarios están cubiertos por él.

Carga infinita

A veces queremos construir una UI de carga infinita, con un botón "Load More" que añada datos a la lista (o que lo haga automáticamente al desplazarse):

Para implementar esto, necesitamos hacer número de peticiones dinámicas en esta página. Los React Hooks tienen un par de reglas (opens in a new tab), por lo que NO PODEMOS hacer algo así:

function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 Esto es un error. Comúnmente, no se pueden usar hooks dentro de un bucle.
    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)}>Load More</button>
  </div>
}

En su lugar, podemos utilizar la abstracción <Page /> que hemos creado para conseguirlo:

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)}>Load More</button>
  </div>
}

Casos avanzados

Sin embargo, en algunos casos de uso avanzado, la solución anterior no funciona.

Por ejemplo, seguimos implementando la misma UI "Load More", pero también necesitamos mostrar un número sobre cuántos item hay en total. No podemos utilizar la solución <Page /> porque la UI de nivel superior (<App />) necesita los datos dentro de cada página:

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>??? items</p>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

Además, si la API de paginación es cursor based, esa solución tampoco funciona. Porque cada página necesita los datos de la página anterior, no están aisladas.

Así es como este nuevo hook useSWRInfinite puede ayudar.

useSWRInfinite

useSWRInfinite nos da la posibilidad de lanzar un número de peticiones con un solo Hook. Así es como se ve:

import useSWRInfinite from 'swr/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

Al igual que useSWR, este nuevo hook acepta una función que devuelve la key de la solicitud, un fetcher función y options. Devuelve todos los valores que devuelve useSWR, incluyendo 2 valores extra: page size y page size setter, como un React state.

En la carga infinita, una page es una petición, y nuestro objetivo es obtener varias páginas y renderizarlas.

⚠️

If you are using SWR 0.x versions, useSWRInfinite needs to be imported from swr:
import { useSWRInfinite } from 'swr'

API

Parametrós

  • getKey: una función que acepta el índice y los datos de la página anterior, devuelve la key de una página
  • fetcher: igual que la función fetcher de useSWR
  • options: acepta todas las opciones que soporta useSWR, con 3 opciones adicionales:
    • initialSize = 1: número de páginas que deben cargarse inicialmente
    • revalidateAll = false: intentar siempre revalidar todas las páginas
    • revalidateFirstPage = true: always try to revalidate the first page
    • persistSize = false: no restablecer el page size a 1 (o initialSize si está establecido) cuando la key de la primera página cambia
    • parallel = false: fetches multiple pages in parallel
💡

Tenga en cuenta que la opción InitialSize no puede cambiar en el ciclo de vida.

Valores de retorno

  • data: una array de valores de respuesta fetch de cada página
  • error: El mismo valor devuelto de error que useSWR
  • isLoading: El mismo valor devuelto de isLoading que useSWR
  • isValidating: El mismo valor devuelto de isValidating que useSWR
  • mutate: same as useSWR's bound mutate function but manipulates the data array
  • size: el número de páginas que se obtendrán y devolverán
  • setSize: establecer el número de páginas que deben ser recuperadas

Ejemplo 1: API paginada basada en índices

Para las APIs normales basadas en índices:

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
// Una función para obtener la key de SWR de cada página,
// su valor de retorno será aceptado por `fetcher`.
// Si se devuelve `null`, la petición de esa página no se iniciará.
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // reached the end
  return `/users?page=${pageIndex}&limit=10`                    // SWR key
}
 
function App () {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  if (!data) return 'loading'
 
  // Ahora podemos calcular el número de todos los usuarios
  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` es un array con la respuesta de la API de cada página.
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

La función getKey es la mayor diferencia entre useSWRInfinite y useSWR. Acepta el índice de la página actual, así como los datos de la página anterior. Así que tanto la API de paginación basada en el índice como la basada en el cursor pueden ser soportadas de forma adecuada.

Además, la data ya no son sólo es una respuesta de la API. Es una array de múltiples respuestas de la API:

// `data` tendrá el siguiente aspecto
[
  [
    { name: 'Alice', ... },
    { name: 'Bob', ... },
    { name: 'Cathy', ... },
    ...
  ],
  [
    { name: 'John', ... },
    { name: 'Paul', ... },
    { name: 'George', ... },
    ...
  ],
  ...
]

Ejemplo 2: Cursor or Offset Based Paginated API

Digamos que la API ahora requiere un cursor y devuelve el siguiente cursor junto con los datos:

GET /users?cursor=123&limit=10
{
  data: [
    { name: 'Alice' },
    { name: 'Bob' },
    { name: 'Cathy' },
    ...
  ],
  nextCursor: 456
}

Podemos cambiar nuestra función getKey por:

const getKey = (pageIndex, previousPageData) => {
  // reached the end
  if (previousPageData && !previousPageData.data) return null
 
  // la primera página, no tenemos `previousPageData`.
  if (pageIndex === 0) return `/users?limit=10`
 
  // añadir el cursor al punto final de la API
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

Parallel Fetching Mode

Please update to the latest version (≥ 2.1.0) to use this API.

The default behavior of useSWRInfinite is to fetch data for each page in sequence, as key creation is based on the previously fetched data. However, fetching data sequentially for a large number of pages may not be optimal, particularly if the pages are not interdependent. By specifying parallel option to true will let you fetch pages independently in parallel, which can significantly speed up the loading process.

// parallel = false (default)
// page1 ===> page2 ===> page3 ===> done
//
// parallel = true
// page1 ==> done
// page2 =====> done
// page3 ===> done
//
// previousPageData is always `null`
const getKey = (pageIndex, previousPageData) => {
  return `/users?page=${pageIndex}&limit=10`
}
 
function App () {
  const { data } = useSWRInfinite(getKey, fetcher, { parallel: true })
}
⚠️

The previousPageData argument of the getKey function becomes null when you enable the parallel option.

Revalidate Specific Pages

Please update to the latest version (≥ 2.2.5) to use this API.

The default behavior of the mutation of useSWRInfinite is to revalidate all pages that have been loaded. But you might want to revalidate only the specific pages that have been changed. You can revalidate only specific pages by passing a function to the revalidate option.

The revalidate function is called for each page.

function App() {
  const { data, mutate, size } = useSWRInfinite(
    (index) => [`/api/?page=${index + 1}`, index + 1],
    fetcher
  );
 
  mutate(data, {
    // only revalidate the last page
    revalidate: (pageData, [url, page]) => page === size
  });
}

Global Mutate with useSWRInfinite

useSWRInfinite stores all page data into the cache with a special cache key along with each page data, so you have to use unstable_serialize in swr/infinite to revalidate the data with the global mutate.

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

As the name implies, unstable_serialize is not a stable API, so we might change it in the future.

Características avanzadas

Aquí hay un ejemplo que muestra cómo se pueden implementar las siguientes características con useSWRInfinite:

  • estados de carga
  • mostrar una UI especial si está vacía
  • desactivar el botón "Load More" si reached the end
  • fuente de datos modificable
  • actualizar toda la lista