嗨大家!像昨天說的,今天會講怎麼用 SWR 實作 Notion 部落格的 pagination (分頁) 功能~ 還沒看昨天的文章的大家,可以點這裡,今天的文章會用到昨天的 API Route 喔!還不知道什麼是 SWR 的大家也可以看這篇~

在昨天的文章我們開了一個新的 API,路徑為 /api/blogs,這 API 會回傳某 Notion database 的內容 (部落格文章):
// pages/api/blogs.js
import { queryDatabase } from "lib/notion";
export default async function handler(req, res) {
  // pagination 的 cursor
  const { cursor } = req.query;
  // 去 query Notion database  
  const resp = await queryDatabase({ start_cursor: cursor });
  // 用 Next.js 提供的 response helper 回傳 JSON 格式的 `resp`
  res.json(resp);
}
getStaticProps在使用 API Route 之前,我們先去 pages/blogs.js 裡寫 getStaticProps 的 data fetching 方式,來抓取資料~
// pages/blogs.js
export const getStaticProps = async () => {
  // 去 query Notion database
  const blogs = await queryDatabase();
  return {
    props: { fallback: { "/api/blogs": blogs } },
    revalidate: 60, // 過了 60 秒後至少會更新一次資料
  };
};
看了上面的 code 之後,可能會發現跟之前的寫法不太一樣。原本應該可以直接回傳 props: { blogs },可是今天是回傳 props: { fallback: { "/api/blogs": blogs } }。為什麼呢?
fallback 物件包含各 SWR key 的備份資料 (沒內容時的預設資料)/api/blogs 就是我們 API Route 的路徑SWRConfig在上一段已經拿完的資料,該怎麼提供給 components 去顯示呢?該怎麼給 useSWR 當 fallback 料?答案就是透過 SWRConfig 的 context!
const Blogs = ({ fallback }) => {
  return (
    <main>
      // 用 SWRConfig 去包需要用到 fallback 資料的 components
      <SWRConfig value={{ fallback }}>
        // 部落個文章的 list
        <List />
      </SWRConfig>
    </main>
  );
};
SWRConfig 裡的所有 components 可以用 useSWR 拿到 fallback 資料~
SWR 提供兩種 pagination 模式,一個是一般的上下頁那種,另一種是可以一直按 load more 的 infinite loading。今天這篇文是用 useSWRInfinite,也就是頁面可以一直往下滑的那種,用法跟一般的 useSWR 類似:
import useSWRInfinite from 'swr/infinite'
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)
不過要注意的是他第一個 parameter 不是 key 而是 getKey,一個回傳 key 的 function:
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // 到最後一頁了
  return `/api/blogs?page=${pageIndex}` // 回傳 SWR key
}
getKey上面的 getKey function 只是例子,而且他是用 pageIndex 做分頁的,可是 Notion SDK 是用 cursor 做分頁,所以我們可以寫一個 cursor based 的 getKey:
const getKey = (pageIndex,previousData) => {
  if (pageIndex === 0) {
    return "/api/blogs"; // 第ㄧ頁
  }
  return previousData && previousData.has_more && previousData.next_cursor
    ? `/api/blogs?cursor=${previousData.next_cursor}` // key 加 cursor
    : null; // 到底
};
List component現在可以寫 component 來顯示資料了!
const List = () => {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher);
  if (!data) {
    return <p>Loading...</p>;
  }
  return (
    <div>
      // array of API responses
      {data.map((blogs) =>
        // results 為 page list
        blogs.results.map((page) => (
          <Box key={page.id}>
            <AvatarComponent size={80} user={page.properties.Owner} />
            // Notion page 的 Title 資料格式比較複雜XD
            {page.properties.Title.type === "title" &&
              page.properties.Title.title.map((t) => t.plain_text).join(" ")}
          </Box>
        ))
      )}
      <Button onClick={() => setSize(size + 1)}>
        Load more
      </Button>
    </div>
  );
};
這裡要注意,如果用 useSWR 的話,data 會是 API 回傳的資料,也就是 data == blogs == { results }。可是因為我們用 useSWRInfinite,data 變成是 array (陣列) 格式,裡面包含很多個 API response 為一頁,
// data
[
  API response 1, // 第一頁
  API response 2, // 第二頁
  ...
]
所以需要用兩個回圈去顯示資料喔!最後給大家看成果~

耶~ 終於做完部落格的分頁功能了,而且今天寫得還滿順利,SWR 配 Next.js 的 API Routes 真的很讚!而且用 SWR 資料會自動被 cache,所以只有第一次 loading 會比較久。大家有什麼問題都可以問喔~
大家想要看看之前的網站可以看這裡,或是直接到首頁~ 有任何問題可以問我,或是也可以討論未來要開發的 No-code 專案喔。
祝大家連假愉快!
午安 <3