iT邦幫忙

2021 iThome 鐵人賽

DAY 11
1
Modern Web

從零開始學習 Next.js系列 第 11

Day11 - 在 Next.js 中使用 CSR - feat. useSWR

為什麼我們需要 SWR ?

先前我們已經暸解了 CSR、SSR 與 SSG 的優劣,SSR 與 SSG 都是 pre-rendering 的策略,在 Next.js 中分別提供 getServerSidePropsgetStaticProps 兩個 API,可以很簡單地讓頁面變成 pre-rending。

但是讓所有的頁面都是 pre-rendering 會讓伺服器的負擔很大,並非所有的頁面都需要 pre-rendering,像是資料變動快速的頁面,或是需要動態跟使用者互動拿到資料的頁面,採用 CSR 的策略會是更好的選擇。

在 Next.js 中並沒有內建的 CSR 的解決方案,同個團隊開發了一個名為 SWR 的套件,可以用來打 API 獲得資料,並且擁有許多很棒的功能,讓跟伺服器互動拿到資料這件事如魚得水。

SWR 簡介

SWR 是由開發 Next.js 的團隊成員在 2019/10/29 開源的專案,到 2021 年 9 月時每週的下載量大約 30 萬左右,且星星數有 1.8 萬顆。

SWR 下載次數

SWR 這個套件的名稱是來自於 stale-while-revalidate ,這是一個判別 HTTP cache 失效的策略,被發表於 HTTP RFC 5861。 SWR 會優先從 cache 中取得資料,如果資料的 cache 已經過期,再打 API 取得新的資料 (revalidate),同時也會更新 cache 中的資料。

這裡的 revalidate 策略是不是聽起來跟 getStaticPropsrevalidate 有點相近,在前面幾個章節中我們有談到 revalidate 這項參數可以決定一個頁面多久需要重新被生成新的 HTML 檔案,同樣地也是使用 stale-while-revalidate 的策略。

Basic Data Loading

import useSWR from "swr";

const fetcher = (url) => fetch(url).then((r) => r.json());

function Profile() {
  const { data, error } = useSWR("/api/user", fetcher);

  if (error) return <div>failed to load</div>;
  if (!data) return <div>loading...</div>;
  return <div>hello {data.name}!</div>;
}

SWR 的起手式非常簡單,在以上的範例中 useSWR 帶有 2 個參數:

  • 第 1 個參數為 key: key 是一個字串,作為資料 unique id
  • 第 2 個參數為 fetcher: fetcher 被用來傳入如何取得資料的函式,例如: 請求 API 經常使用的 fetch 或是 axios,將它們包裝成函式傳入。

範例中 SWR 的回傳值有 dataerror,它們分別為 fetcher 的回傳結果,以及從 fetcher 被丟出的錯誤。

在資料尚未載入時,data 都是回傳 undefined,因此,我們就可以利用 data 的回傳值判斷要不要渲染資料,用這種方式實現非同步渲染的效果。

Conditional Fetching

有時候,我們希望 API 的請求是能夠由使用者自己掌握的,而不是在元件載入時就自動發出請求。像是常見我們在購物網站看到「查看更多」的按鈕,在點選按鈕後才載入更多的商品資訊。

要使用 SWR 達成這件事也很簡單,有三種不同的寫法:

// key 值為 null 時不打 API
const { data } = useSWR(shouldFetch ? "/api/data" : null, fetcher);

// 跟前一種很像,只是變成 callback function
const { data } = useSWR(() => (shouldFetch ? "/api/data" : null), fetcher);

// 或者是讓 SWR 幫我們判斷,如果 user.id 為 undefined 則呼叫 API 會拋出 error
const { data, error } = useSWR(() => "/api/data?uid=" + user.id, fetcher);

Multiple Arguments

有時候 API 需要帶多個參數,例如 /products/[id]/posts/[year]/[month]/[day] 兩者都會額外帶一些參數,而且可能是動態的數值

❌ 然而,要注意的是在使用 SWR 時,官方建議不要這樣做:

useSWR('/product', url => fetcher(url, id))

原因是,SWR 決定要不要 refetch 取決於第一個參數 key 有沒有改變,如果像是上述這樣使用,儘管 id 變動了,key 仍然是 /product,因此,SWR 的回傳值就會是錯誤的。

? 正常的使用方式是將 key 變成 array,把 id 放 array 中:

useSWR(['/product', id], fetcher)

如此一來, 當 id 改變時,SWR 就可以成功被通知並發送 API 的請求。

你以為這樣就沒問題了嗎  ?

❌ 因為 SWR 用的是 shallow compare,當參數的型態是 Object 時,比較的是 reference。因此,在每次元件渲染時,reference 都會被重新分配,SWR 會誤以為 key 改變了,會再次發送一次請求。

useSWR(["/api/user", { id }], fetcher);

? 要解決傳物件的方法有兩種,第一種是官方推薦的方式,如果真的需要傳入物件到 fetcher 中,則把物件當作是 fetcher 的參數,而不是 key 的參數。

useSWR(["/api/user", id], (url, id) => fetcher(url, { id }));

第二種方法是用 react 的 useMemo 把物件的 reference 記憶起來,如此一來 SWR 就不會因為重新渲染而不斷執行 fetcher

const params = useMemo(() => ({ id }), [id]);
useSWR(["/api/user", params], fetcher);

Next.js + SWR 的組合技

在 Next.js 中很常見 dynamic routes 的頁面,在這些頁面中想要使用 SWR 則必須搭配 conditional fetching 與 multiple arguments 兩種技巧。在前面的章節中有提到 Next.js 中 router.query 在第一次渲染時是空物件 {} ,所以從 router.query 中解構賦值的 idundefined ,此時如果直接打 API 就會發生錯誤。

而為了根據不同的頁面,需要傳入不同的 idfetcher 中,所以要用 multiple arguments 的方法傳遞 id

const router = useRouter();
const { id } = router.query;

const { data: product } = useSWR(id ? ["/products", id] : null, fetcher);

使用 SWR 重構產品列表與產品詳細頁面

在前面幾個章節中,我們使用 pre-rendering 建構了產品列表頁面產品詳細頁面,資料是透過 getStaticPropsgetServerSiderProps 傳入 props 到 component 中,現在我們要嘗試另一種做法,在 component 中打 API 拿到頁面中需要的資料,並渲染頁面。

產品列表頁面可以看到所有商品的訊息,以卡片列表是呈現所有的商品;而產品詳細頁面則是顯示單一商品的詳細訊息,使用者可以點擊列表中的卡片標題進入產品詳細頁面。

產品列表頁面

/pages/products/index.tsx

style: https://gist.github.com/leochiu-a/c4b8ac14ed823bcf6b8326717e594910

SWR 會需要放入兩個參數,分別為 keyfetcherkey 是用來定義資料的 unique id,而 fetcher 則是用來呼叫 API 取得資料的 function。

因為我們需要的資料是所有的產品,先定義 key/products 代表的是產品列表這個資源,而 fetcher 則是使用原生的 fetch API,打 [https://fakestoreapi.com](https://fakestoreapi.com) 這個服務提供的 API,並回傳 json 資料格式。

由於 SWR 是非同步的,第一次渲染時從 data 解構賦值的 productsundefined ,如果直接透過 map 迭代資料則是會發生錯誤,為了避免這個情況,則在渲染列表之前,用條件式渲染的方式先渲染資料正在載入中訊息,並在資料取得後再渲染產品列表。

import useSWR from "swr";
import ProductCard from "../../components/ProductCard";
import { Product } from "../../fake-data";
import { PageTitle, ProductGallery } from "./index.style";

const fetcher = (url: string) =>
  fetch(`https://fakestoreapi.com${url}`).then((res) => res.json());

const Home = () => {
  const { data: products } = useSWR<Product[]>("/products", fetcher);

  if (!products) return <div>loading</div>;

  return (
    <>
      <PageTitle>商品列表</PageTitle>
      <ProductGallery>
        {products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))}
      </ProductGallery>
    </>
  );
};

export default Home;

產品詳細頁面

/pages/products/[id].tsx

style: https://gist.github.com/leochiu-a/56106bd3bd24efb7d75082f0fb60b2f3

由於產品詳細頁面是 dynamic routes, 要注意的有三點:

  • id 是一個動態的值,所以我們要藉由從 router.query 中取得 id 再決定要打哪支 API。這邊有一個要注意的地方是 router.query 第一次渲染時是空物件 {} ,解構賦值取得的 idundefined ,如果打 API 其 url 則會是 /products/undefined ,此時就會發生錯誤
  • 要避免上述情況,則要使用 conditional fetching 的方式,等 id 有值時才打 API
  • 由於 id 是動態的,所要使用前面提及的 multiple arguments 傳入 idfetcher

剩下的程式碼則是與產品列表頁面大同小異,所以就不再贅述。

import useSWR from "swr";
import { useRouter } from "next/router";
import Link from "next/link";

import { Product as ProductType } from "../../fake-data";
import ProductCard from "../../components/ProductCard";
import { PageTitle, ProductContainer, BackLink } from "./[id].style";

const fetcher = (url: string, id: string) => {
  return fetch(`https://fakestoreapi.com${url}/${id}`).then((res) =>
    res.json()
  );
};

const Product = () => {
  const router = useRouter();
  const { id } = router.query;

  const { data: product } = useSWR<ProductType>(
    id ? ["/products", id] : null,
    fetcher
  );

  if (!product) return <div>loading</div>;

  return (
    <>
      <PageTitle>商品詳細頁面</PageTitle>
      <BackLink>
        <Link href="/products">回產品列表</Link>
      </BackLink>
      <ProductContainer>
        <ProductCard product={product} all />
      </ProductContainer>
    </>
  );
};

export default Product;

在產品列表頁面與產品詳細頁面中使用 SWR 的其中一個好處是資料會被 cache ,所以在第一次打 API 取得資料後,再次回到頁面時就不用再重新打 API 取得資料,舉例來說:

  • 使用者進入產品列表頁面 /products
  • SWR 打 API - /products 取得資料
  • 使用者點擊其中一個產品標題,進入產品詳細頁面 /products/1
  • SWR 打 API - /products/1 取得資料
  • 使用者點擊「回到產品列表」,回到產品列表頁面 /products
  • SWR 使用被 cache 的資料,不用再次打 API - /products/1 取得資料
  • 同樣地,使用到再次瀏覽產品詳細頁面 /products/1 時, SWR 也可以直接讀取 cache,不用再次打 API 取得資料

SWR - TypeScript

因為我們使用 TypeScript 撰寫 Next.js,如果 useSWR 沒有定義範型,則 data 會是 any 的型別,不可控的 any 對於 component 不是件好事。

ts 推斷 product 為 any

讀者可以從上面的範例中看到,在使用 useSWR 時候會傳入一個範型,如此一來 data 的型別就不會是 any ,而是我們傳入的型別。

ts 推斷 product 為 ProductType

我們再近一步看到 useSWR 的型別定義,實際上 useSWR 可以傳入兩個範型,分別是 dataerror ,所以如果有需要使用 error 的資料,則可以傳入第二個範型至 useSWR

declare function useSWR<Data = any, Error = any>(...)

結論

在這篇文章中,我們了解了 SWR 的基本使用方法,並且知道了在 Next.js 中一個常用的方法,在 dynamic routes 的頁面,在這些頁面中想要使用 SWR 則必須搭配 conditional fetching 與 multiple arguments 兩種技巧。

此外,我們用 SWR 重構了產品列表頁面與產品詳細頁面,除了從 pre-rendering 改成了 CSR 之外,還了解透過 SWR 可以讀取 cache 的優點,在切換頁面時,可以加速頁面載入的速度,提升使用者體驗。

Reference


上一篇
Day10 - 為什麼官方不推薦使用 getInitialProps
下一篇
Day12 - 該來寫 API 了,API routes 簡介
系列文
從零開始學習 Next.js30

尚未有邦友留言

立即登入留言