先前我們已經暸解了 CSR、SSR 與 SSG 的優劣,SSR 與 SSG 都是 pre-rendering 的策略,在 Next.js 中分別提供 getServerSideProps
與 getStaticProps
兩個 API,可以很簡單地讓頁面變成 pre-rending。
但是讓所有的頁面都是 pre-rendering 會讓伺服器的負擔很大,並非所有的頁面都需要 pre-rendering,像是資料變動快速的頁面,或是需要動態跟使用者互動拿到資料的頁面,採用 CSR 的策略會是更好的選擇。
在 Next.js 中並沒有內建的 CSR 的解決方案,同個團隊開發了一個名為 SWR 的套件,可以用來打 API 獲得資料,並且擁有許多很棒的功能,讓跟伺服器互動拿到資料這件事如魚得水。
SWR 是由開發 Next.js 的團隊成員在 2019/10/29 開源的專案,到 2021 年 9 月時每週的下載量大約 30 萬左右,且星星數有 1.8 萬顆。
SWR 這個套件的名稱是來自於 stale-while-revalidate
,這是一個判別 HTTP cache 失效的策略,被發表於 HTTP RFC 5861。 SWR 會優先從 cache 中取得資料,如果資料的 cache 已經過期,再打 API 取得新的資料 (revalidate),同時也會更新 cache 中的資料。
這裡的 revalidate 策略是不是聽起來跟 getStaticProps
的 revalidate
有點相近,在前面幾個章節中我們有談到 revalidate
這項參數可以決定一個頁面多久需要重新被生成新的 HTML 檔案,同樣地也是使用 stale-while-revalidate
的策略。
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 個參數:
key
: key
是一個字串,作為資料 unique idfetcher
: fetcher
被用來傳入如何取得資料的函式,例如: 請求 API 經常使用的 fetch 或是 axios,將它們包裝成函式傳入。範例中 SWR 的回傳值有 data
與 error
,它們分別為 fetcher
的回傳結果,以及從 fetcher
被丟出的錯誤。
在資料尚未載入時,data
都是回傳 undefined
,因此,我們就可以利用 data
的回傳值判斷要不要渲染資料,用這種方式實現非同步渲染的效果。
有時候,我們希望 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);
有時候 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 中很常見 dynamic routes 的頁面,在這些頁面中想要使用 SWR 則必須搭配 conditional fetching 與 multiple arguments 兩種技巧。在前面的章節中有提到 Next.js 中 router.query
在第一次渲染時是空物件 {}
,所以從 router.query
中解構賦值的 id
為 undefined
,此時如果直接打 API 就會發生錯誤。
而為了根據不同的頁面,需要傳入不同的 id
至 fetcher
中,所以要用 multiple arguments 的方法傳遞 id
。
const router = useRouter();
const { id } = router.query;
const { data: product } = useSWR(id ? ["/products", id] : null, fetcher);
在前面幾個章節中,我們使用 pre-rendering 建構了產品列表頁面與產品詳細頁面,資料是透過 getStaticProps
或 getServerSiderProps
傳入 props 到 component 中,現在我們要嘗試另一種做法,在 component 中打 API 拿到頁面中需要的資料,並渲染頁面。
產品列表頁面可以看到所有商品的訊息,以卡片列表是呈現所有的商品;而產品詳細頁面則是顯示單一商品的詳細訊息,使用者可以點擊列表中的卡片標題進入產品詳細頁面。
/pages/products/index.tsx
style: https://gist.github.com/leochiu-a/c4b8ac14ed823bcf6b8326717e594910
SWR 會需要放入兩個參數,分別為 key
與 fetcher
, key
是用來定義資料的 unique id,而 fetcher
則是用來呼叫 API 取得資料的 function。
因為我們需要的資料是所有的產品,先定義 key
為 /products
代表的是產品列表這個資源,而 fetcher
則是使用原生的 fetch API,打 [https://fakestoreapi.com](https://fakestoreapi.com)
這個服務提供的 API,並回傳 json
資料格式。
由於 SWR 是非同步的,第一次渲染時從 data
解構賦值的 products
是 undefined
,如果直接透過 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
第一次渲染時是空物件 {}
,解構賦值取得的 id
是 undefined
,如果打 API 其 url 則會是 /products/undefined
,此時就會發生錯誤id
有值時才打 APIid
是動態的,所要使用前面提及的 multiple arguments 傳入 id
至 fetcher
中剩下的程式碼則是與產品列表頁面大同小異,所以就不再贅述。
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
/products
取得資料/products/1
/products/1
取得資料/products
/products/1
取得資料/products/1
時, SWR 也可以直接讀取 cache,不用再次打 API 取得資料因為我們使用 TypeScript 撰寫 Next.js,如果 useSWR
沒有定義範型,則 data
會是 any
的型別,不可控的 any
對於 component 不是件好事。
讀者可以從上面的範例中看到,在使用 useSWR
時候會傳入一個範型,如此一來 data
的型別就不會是 any
,而是我們傳入的型別。
我們再近一步看到 useSWR
的型別定義,實際上 useSWR
可以傳入兩個範型,分別是 data
與 error
,所以如果有需要使用 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 的優點,在切換頁面時,可以加速頁面載入的速度,提升使用者體驗。