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


<<:  Day12_HTML语法9

>>:  【12】新手容易忽略的 logit 与 loss 之间的搭配

Day20 样式变化(动画4)

元件间的转换 在元素间的转换可以更加简单,,因为有了这一个动态组件 当按下单选键A会出现Compon...

Swift 新手-打造第一个 iOS App

开发前的新手纠结 商学院出身,非资工背景,团队内也没有熟悉 app 开发的人才 决定做 app 接触...

[Day 26] 建立table

先到laravel专案找到环境变数档(.env) DB_CONNECTION=mysql DB_HO...

Day9 用python写UI-聊聊Message & Messagebox

在处理一些短讯息的时候可以用 Message 的功能,这个功能跟 Label 有点类似,不一样的地方...

透明这回事 (Part 2)

前言 今天接着把「透明」这个主题再延伸一些。 Scrum 在开发团队人数的考量上,从 2017 版本...