# Next.js 13 - App router ## Next 如何渲染 Server Component? > render 的工作會根據 router 和 suspense 切分成 chunk 來載入 ### chuck 執行兩個步驟 1. 取得 React Server Component Payload:React 先 render Server Component 成 RSC Payload > RSC Payload(壓縮的二進位內容)說明 rendered 的 React Server Components tree,會包含: > 1. Server Components 的 rendered result > 2. 紀錄哪裡會有 Client Component > 3. Server Components 要傳給 Client Component 的 props > > 這也是為什麼 Server Component 只能包含可序列化的資料,不能包含 function。 2. 在 server 渲染出 HTML:Next 根據 RSC Payload,並把其他的 Client Component (其他的 JavaScript chunk) 在 server 進行 render,轉成初始的 HTML ### client 端 (接收內容的 browser) 1. 得到 initial HTML 產生 router 的初始畫面 (initial page load only)。 2. 有 RSC Payload 後,React 能 reconcile Server component 與 Client component 來建構 Component tree,並操作 DOM 完成更新。接著再根據發 request 來取得 Client component 要做互動性的 chunk。 > [React source code:Component tree 裡不同 tag 的內容是如何被判斷要進行什麼處理](https://github.com/facebook/react/blob/9d17b562ba427adb1bced98f4231406ec573c422/packages/react-client/src/ReactFlightClientStream.js#L44-L60) 3. streaming 得到 Client component chunk,進行 hydrate 來更新 Client component 的互動性到 DOM,讓 Application 能完整運作。 言下之意,Server Component 與 SSR 都會在網頁原始碼上顯示,都有利於爬蟲。但主要差異在於產生 initial page 的 HTML 可能是不一樣的。 舉例來說,一般而言 page 都會預設是 static render,所以無論是 RSC 或 RCC 的初始內容都會被放到 initial HTML。 但 RSC 可以以 async 讀取 fetch 的資料,也就是到了 NextJS v13 的 App Router 後,單純使用 fetch 就可以表達出資料是以 getStaticProps 還是 getServerSideProps。然而,如果是 Client component,仍然需要搭配 useEffect 來 render API data 在畫面,在 initial HTML 是不會有 useEffect 產生的資料。 所以,如果是 NextJS v13 的 Client component 沒有接收 Server component 傳下來的 props,而資料來源是透過 useEffect 再更新到 DOM 上,那麼初始狀態不會包含這些 API data,所以在 loading Client component 的時候,可能都只包含了 skeleton。 ## Render Pattern > TL;DR; > > render pattern 是指 client routes 在什麼階段下進行 render HTML。由於 Next13 有 Server Component 的特色,所以進行 cache 主要可以分為 full-route cache 和 data cache 的處理。(RSC Payload 和 Fetching Data 是分開處理) > > Nextjs 會自動根據一個 route 的 Component + Fetching method 來決定要使用 static rendering 和 dynamic rendering。開發者需要決定的內容是: > 1. 使用的 Component 類型 > 2. Fetching data 1. 完全 cached (SSG) 2. revalidate > 0 (ISG) 3. revalidate = 0 (SSR) > 3. 是否使用到 dynamic function ### Static rendering (預設) > 採用 CDN 進行 cached 1. 在 build time 已經完成 (SSG) 2. data revalidation 完成後在 background 執行 (ISG) 如果 page.js 沒有加上 "use client" 去改變預設頁面以 Server Component 進行處理與其他調整,則該頁的內容會被判定為 SSG ### Dynamic rendering 1. 在每次 request 時進行 (SSR) Next 有提供 dynamic function,如果有使用的話,該 route 會自動變成 dynamic rendering。 #### Dynamic Functions - `cookies()` + `headers()`: 1. 在 Server Component 使用 2. 使用會讓整頁會變成 SSR - `useSearchParams()`: 1. 在 Client Component 使用 2. 自動尋找一個最靠近有使用 Suspense 的 parnet (官方推薦使用的話可以把這個 Client 抽出來,並直接包上 <Suspense /> 避免其他內容失去 static rendering) - searchParams: 1. Page props 2. 使用會讓整頁會變成 SSR ### Streaming Next13 的 Client component chunk 本身是使用 streamnig 的方式來顯示 UI,所以 hydrate 也可以變成 chunk,因為網站的 TTI 也能提升。 ### 實際使用 承接上面描述,根據設定 fetch API 的 options,Next.js 會自動判斷要使用什麼 render pattern。 - SSR 產生邊境牧羊犬的圖片 (`app/border-collie/page.tsx`) ```ts import Image from 'next/image'; async function getBorderCollie() { const response = await fetch( 'https://dog.ceo/api/breed/collie/border/images/random', { cache: 'no-store', }, ); const data = await response.json(); return data.message; } export default async function BorderCollie() { const dog = await getBorderCollie(); return ( <div className="text-lg"> <Image src={dog} alt="" /> </div> ); } ``` - SSG 生成鬆獅犬的圖片 (`app/chow/page.tsx`) ```ts import Image from 'next/image'; async function getChow() { const response = await fetch('https://dog.ceo/api/breed/chow/images/random'); const data = await response.json(); return data.message; } export default async function Chow() { const dog = await getChow(); return ( <div className="text-lg"> <Image src={dog} alt="" /> </div> ); } ``` - 執行 next build 的結果 ```bash Route (app) Size First Load JS ┌ ○ / 137 B 78.6 kB ├ λ /border-collie 178 B 83.6 kB └ ○ /chow 178 B 83.6 kB + First Load JS shared by all 78.4 kB ├ chunks/596-c294a7d39d9fe754.js 26.1 kB ├ chunks/fd9d1056-a99b58d3cc150217.js 50.5 kB ├ chunks/main-app-89ee0ba7722c6c8b.js 219 B └ chunks/webpack-28022adcc3d465a2.js 1.64 kB λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps) ○ (Static) automatically rendered as static HTML (uses no initial props) ``` ### 可以針對最新 n 秒的資料產生 static page,避免全部 route 都變成 dynamic - render pattern 可以是 layout (包含 head), page (只有 body), fetching data (部分 component),又稱作 [Route Segment Config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) - 在 page 使用 generateStaticParams 來記錄,讓部分 dynamic routes 可以有 static 產生(感覺也適合做多國語言) - 不包含在 generateStaticParams 的 dynamic routes,則可以搭配在 layout 設定 dynamicParams (Boolean),來決定要 SSR 產生 page,還是其他 fallback (default: 404) > 類似之前寫 getServerSideProps 或 getStaticProps 設定 fallback。 > [完整的 functions 介紹](https://nextjs.org/docs/app/api-reference/functions) > [Next.js 13 SSG, SSR & ISR | Nextjs 13 tutorial](https://www.youtube.com/watch?v=E1HzFvXgrCs) ## Fetching Data ### React 的 Fetch API 有對 GET 進行快取 React 的 Fetch 有進行過一層封裝,所以在 React 延伸的框架中, Fetch API 中的 GET 自帶 Data Cache 的機制。([RSC Payload 和 Fetched Data 是分開處理](https://nextjs.org/docs/app/building-your-application/caching#react-cache-function)) - 有 Server Component 後,跟 db 直接互動的語法也可以使用(以前端來說是 firebase) ```ts= import { cache } from 'react' import db from '@/lib/db' export const getItem = cache(async (id: string) => { const item = await db.item.findUnique({ id }) return item }) ``` ### Next 封裝 Web 的 Fetch API - `options.cache` - `force-cache`:Next.js 如果有找到對應的 request 請求會直接拿 cache data;反之,則會向 endpoint 拿最新的資料,並儲存成 cache data - `no-store`:Next.js 不考慮有無 cache data 都會直接發請求 1. 決定 fetch 會如何與 Next 的 cache data 互動 2. 如果沒有使用 dynamic function,則 default 都是採用 force-cache。 - `options.next.revalidate` 1. 以「秒」為單位決定 cache data 的使用期間 2. 有三種狀況可以設定 1. false:表示不需要 revalidate,直接拿 cache data 2. 0 (number):不會把請求拿到的資料寫成 cache 3. \> 0 (number):在多久期間內都先使用 cache data 3. 如果同一個 route 對同一支 endpoint 設定不同 revalidate,會取 lower revalidate > 建議:可以按照 revalidate 的設定再決定使用的 cache,不然蠻容易寫出 error - `options.next.tags` 1. 紀錄 cache data,協助判斷一個 fetcher 是否需要進行 revalidate ## Cache:Route & Data 在 NextJS v13 後,將快取分拆成針對 router 跟 data 進行,加大了頁面與資料的取用彈性。綜合 render pattern 和 fetching data 的知識,主要可以分為幾個層面來處理快取: 1. fetcher 的執行:不同 component 拿資料的狀況 2. static router 是 static、特定時間 revalidate、永遠產生全新 HTML 3. dynamic router 是否有 static 內容 和 其他同 static router 的快取設定 ## 分拆 Server Component 與 Client Component 的技巧 ### 組合不同 component 的限制 主要閱讀[這篇官方文件](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns),說明兩者在搭配時,會遇到一些使用限制: 1. Server Component 不能寫 event handler 2. Server Component 也不能使用 hook (也意味不能共用 context data) ```ts export const ServerC = () => { // ❌ ReactServerComponentsError: You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default. const [msg, setMsg] = useState('') return ( <div className="text-lg"> {/* ❌Error: Event handlers cannot be passed to Client Component props */} <button onClick={() => setMsg('Hello!')}> {msg} </button> </div> ); }; ``` 3. utils 要特別區分 官方建議只在 server side 會用到的共用 script,可以安裝 `server-only` 這個 package 來協助在 build time 發現錯誤。 ```ts import 'server-only' export async function getData() { const res = await fetch('https://external-service.com/data', { headers: { authorization: process.env.API_KEY, }, }) return res.json() } ``` ### 使用 children 傳遞:解決不能在 Client Component 中 import Server Component ### JSX.Element 可以是 props:解決 Server Component 無法傳 common component 給 Client Component (e.g. image, inline svg) ```ts // client component 'use client'; const ClientC = (props: { compoent: () => JSX.Element }) => { return ( <div> <props.compoent /> </div> ); }; export { ClientC }; // server component import { TokenIcon } from '@/app/AccessFaucetForm/styles'; import { ClientC } from './ClientC'; const CommonComponent = (props: { Compoent: () => JSX.Element }) => { return ( <div id="hello"> <props.Compoent /> </div> ); }; export const ServerC = () => { return ( <> {/* ❌ Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". */} <ClientC compoent={TokenIcon} /> {/* ✅ 可以直接傳 function 給 server component */} <CommonComponent Compoent={TokenIcon} />; </> ); }; ``` --- ## 實際查看專案 > 分析案例:[MindChat - Batch#20 前端作品](https://www.mindchat.me/) 在 `</body>` 前透過 async 的方式載入 `<script>`,藉此在 HTML parse 完成後產生 DOM tree,透過 React Runtime 去理解 RSC Payload,並確認哪些內容可以直接以 JavaScript 解讀 RSC Payload(JSON 文檔)來完成 DOM 更新。接著其餘的 Client Component 則還需要向 Server 請求 JavaScript 的 chunk,以 hydrate 的方式慢慢 render 到畫面上,來完成互動性。 `預設為 SSG` 首頁以 Server Component 載入,如果無其他設定 (fetch option、dynamic function),預設為 SSG。 ![](https://hackmd.io/_uploads/SJzQ3YF1T.png) `使用 "use client"` 在 page.tsx 加上 "use client",整頁會變成 CSR,也因此會把整頁產生 Component 的方式變成一個大的 .js。如:signin | signup | map 都是這個模式,所以只有一個大的 .js 在各自的 dir 底下,裡面會記錄如何 hydrate 一個 element 的互動性。 像是如果在 page 或 layout 也有這類型的 chunk,是因為有些 Next 提供的 component,也是 client component (像是 <Link />),它們的 hydrate 內容會同樣紀錄在這。 ![](https://hackmd.io/_uploads/Sy0u49Yya.png) `<Link /> 有支援 prefetch 資源` hover Logo 跟 SIGN IN 時,如果沒有 RSC Payload 的 cache data,則會再拿了一次 RSC Payload;反之如果網頁沒有特別設定,預設走 SSG,這些內容都會被 cache 起來。除非改變 revalidate 的時間,或是 SSR 載入頁面,不然都會拿到重複的 HTML 和 RSC Payload。 <img src="https://hackmd.io/_uploads/r1gVQJqt1p.png"/> --- ## App Router 生態系支援度 ### Netlify:App Router 在 Vercel 的支援度比較好 `Features 支援度` 目前比較多的 issue 是關於 Suspense、Navigator、Serverless functions > - [針對 NextJS v13 的 runtime 集合串](https://github.com/netlify/next-runtime/discussions/1724) > - [<Link /> 有實現了 prefetch 能在 client side 先拿到資源](https://github.com/netlify/next-runtime/pull/1855) `Deployment` 目前佈署的生態系整合還不夠完善,可能只有 Vercel 是目前處理最好。舉例來說,像是直接在瀏覽器輸入特定網址,會有 Server error,原因是初始的環境變數 __NEXT_PRIVATE_PREBUNDLED_REACT 預設造成。其他的設定還有關 trailing slash 和 prefetch 都還需要額外的設定才能順利讓 App Router 運行。 > [My experience deploying a Next.js application on Netlify (using Next App Router)](https://dev.to/alvesjessica/my-experience-deploying-a-nextjs-application-on-netlify-using-next-app-router-3k1b) ### UI library - `MUI` - 發現 MUI 也在為了 Next13 的 App Router 做準備,這支 PR 為 UI component 與 type 定義加上 ‘use client’ ([PR#37656](https://github.com/mui/material-ui/pull/37656))。同時也更新文件,說明如何在 App Router 中使用 MUI,也提供 Next 相關文件的引導,讓之後的 migrate 可以更 smooth。([MUI GitHub: Guide - Next.js App Router](https://github.com/mui/material-ui/blob/master/docs/data/material/guides/next-js-app-router/next-js-app-router.md)) ## 問題紀錄 - [Windows 上建置 Next13.4.x 專案除錯紀錄](https://medium.com/@liaoweil05621/windows-上建置-next13-4-x-專案除錯紀錄-eea1f01a1fb8)