跳到內容
文件
分頁

分頁

請更新至最新版本(≥ 0.3.0)以使用此 API。先前的 useSWRPages API 現在已棄用。

SWR 提供專用的 API useSWRInfinite 來支援常見的 UI 模式,例如分頁無限載入

何時使用 useSWR

分頁

首先,如果我們正在建立如下的內容,我們可能需要 useSWRInfinite,而可以使用 useSWR 就好

...這是一個典型的分頁 UI。讓我們看看如何使用 useSWR 輕鬆實現它

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // The API URL includes the page index, which is a React state.
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
 
  // ... handle loading and error states
 
  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>
}

此外,我們可以為這個「頁面元件」建立一個抽象

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);
 
  // ... handle loading and error states
 
  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>
}

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

僅需 1 行程式碼,我們就可以獲得更好的 UX。useSWR Hook 非常強大,大多數場景都涵蓋在其中。

無限載入

有時我們想要建立一個無限載入 UI,帶有一個「載入更多」按鈕,該按鈕將資料附加到清單中(或在您捲動時自動完成)

為了實現這一點,我們需要在這個頁面上發出動態數量的請求。React Hooks 有一些規則(在新分頁開啟),因此我們不能做這樣的事情

function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 This is wrong! Commonly, you can't use hooks inside a loop.
    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>
}

相反,我們可以使用我們建立的 <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)}>Load More</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>??? items</p>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

此外,如果分頁 API 是基於游標的,該解決方案也無效。因為每個頁面都需要前一個頁面的資料,它們並非孤立的。

這就是這個新的 useSWRInfinite Hook 可以提供幫助的原因。

useSWRInfinite

useSWRInfinite 讓我們能夠使用一個 Hook 觸發多個請求。它的外觀如下

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

useSWR 類似,這個新的 Hook 接受一個返回請求鍵的函式、一個 fetcher 函式和選項。它返回 useSWR 返回的所有值,包括 2 個額外的值:頁面大小和頁面大小設定器,就像 React 狀態一樣。

在無限載入中,一個頁面是一個請求,我們的目標是獲取多個頁面並渲染它們。

⚠️

如果您正在使用 SWR 0.x 版本,則需要從 swr 匯入 useSWRInfinite
import { useSWRInfinite } from 'swr'

API

參數

  • getKey:一個接受索引和前一頁資料的函式,返回頁面的鍵
  • fetcher:與 useSWRfetcher 函式相同
  • options:接受 useSWR 支援的所有選項,外加 4 個額外選項
    • initialSize = 1:初始載入的頁面數量
    • revalidateAll = false:總是嘗試重新驗證所有頁面
    • revalidateFirstPage = true:總是嘗試重新驗證第一頁
    • persistSize = false:當第一頁的鍵值變更時,不要將頁面大小重設為 1(或如果設定了 initialSize 的值)
    • parallel = false:平行取得多個頁面
💡

請注意,initialSize 選項不允許在生命週期中變更。

回傳值

  • data:每個頁面的抓取回應值陣列
  • error:與 useSWRerror 相同
  • isLoading:與 useSWRisLoading 相同
  • isValidating:與 useSWRisValidating 相同
  • mutate:與 useSWR 的綁定 mutate 函數相同,但會操作資料陣列
  • size將會 抓取並回傳的頁面數量
  • setSize:設定需要抓取的頁面數量

範例 1:基於索引的分頁 API

對於一般的基於索引的 API

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
// A function to get the SWR key of each page,
// its return value will be accepted by `fetcher`.
// If `null` is returned, the request of that page won't start.
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'
 
  // We can now calculate the number of all users
  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` is an array of each page's API response.
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

getKey 函數是 useSWRInfiniteuseSWR 之間的主要差異。它接受目前頁面的索引,以及前一頁的資料。因此,索引式和游標式分頁 API 都能良好地支援。

此外,data 不再只是一個 API 回應。它是一個包含多個 API 回應的陣列

// `data` will look like this
[
  [
    { name: 'Alice', ... },
    { name: 'Bob', ... },
    { name: 'Cathy', ... },
    ...
  ],
  [
    { name: 'John', ... },
    { name: 'Paul', ... },
    { name: 'George', ... },
    ...
  ],
  ...
]

範例 2:基於游標或偏移量的分頁 API

假設現在 API 需要一個游標,並在資料旁邊回傳下一個游標

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

我們可以將 getKey 函數變更為

const getKey = (pageIndex, previousPageData) => {
  // reached the end
  if (previousPageData && !previousPageData.data) return null
 
  // first page, we don't have `previousPageData`
  if (pageIndex === 0) return `/users?limit=10`
 
  // add the cursor to the API endpoint
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

平行抓取模式

請更新到最新版本(≥ 2.1.0)以使用此 API。

useSWRInfinite 的預設行為是依序抓取每個頁面的資料,因為金鑰的建立是基於先前抓取的資料。但是,對於大量頁面依序抓取資料可能不是最佳選擇,特別是當頁面之間沒有相互依賴關係時。透過將 parallel 選項指定為 true,您就能夠平行且獨立地抓取頁面,這可以顯著加快載入過程。

// 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 })
}
⚠️

當您啟用 parallel 選項時,getKey 函數的 previousPageData 引數會變成 null

重新驗證特定頁面

請更新到最新版本(≥ 2.2.5)以使用此 API。

useSWRInfinite 的 mutation 的預設行為是重新驗證所有已載入的頁面。但您可能只想重新驗證已變更的特定頁面。您可以將函數傳遞至 revalidate 選項,藉此僅重新驗證特定頁面。

會為每個頁面呼叫 revalidate 函數。

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
  });
}

使用 useSWRInfinite 進行全域 Mutate

useSWRInfinite 將所有頁面資料儲存到快取中,每個頁面資料都有特殊的快取金鑰,因此您必須在 swr/infinite 中使用 unstable_serialize,以使用全域 mutate 重新驗證資料。

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

顧名思義,unstable_serialize 不是穩定的 API,因此我們未來可能會對其進行變更。

進階功能

這裡有一個範例,說明如何使用 useSWRInfinite 實作下列功能

  • 載入狀態
  • 如果為空,則顯示特殊 UI
  • 如果已到達結尾,則停用「載入更多」按鈕
  • 可變更的資料來源
  • 重新整理整個清單