# BTQ Explorer - Migration with NextJS v13 (App Router)
## Header + Footer


1. 全頁共用一個 `<Layout />` 的 file 放在 `app/layout.tsx`。(layout 只能使用 `/.(js|jsx|tsx)/`)
2. 不同畫面可以透過 generateMetadata 的 API 來生成 head 的內容。
3. 使用 Server Component 做 static rendering
> [官方文件](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#layouts):A layout is UI that is **shared between multiple pages**. On navigation, layouts preserve state, remain interactive, and **do not** re-render.
### 使用方式 & 名詞
1. `Root Layout`
所有頁面「共用的 Common Layout」或是「每頁都有自己的 Layout」,Root Layout 一定要有 html 和 body。**適合目前的 Explorer** ,因為只有一種 layout。
```bash
┌ app
├── block-list
├ ├── page.tsx # /block-list (URL)
├── tx-list
├ ├── page.tsx # /tx-list (URL)
├── block
├ ├── [hash]
├ │ ├──page.tsx # /block/{hash} (URL)
├── tx
├ ├── [hash]
├ │ ├──page.tsx # /tx/{hash} (URL)
├── layout.tsx # Root Layout
├── page.tsx # home page
```
2. `Router Group`
可以用來表達一個 group 但不影響 URL 生成,以`(<groupName>)` 作為 convention。以 Router Group 來表達分頁,同時示範如何製作 Multiple Root Layout,這樣**特定 group 可以共用同一個 Layout**,而不是整個 App 都是同一個 layout;同時,**Router Group 的 dir 不會影響 URL**,所以不會有 /list 和 /detail,能做到和上面一樣的 routing path。
```bash
┌ app
├ (list)
├ │ ├── layout.tsx # List Group's Root Layout
├ │ block-list
├ │ ├── page.tsx # /block-list (URL)
├ │ tx-list
├ │ ├── page.tsx # /tx-list (URL)
(detail)
├ │ ├── layout.tsx # Detail Group's Root Layout
├ │ block
├ │ ├── [hash]
├ │ │ ├──page.tsx # /block/{hash} (URL)
├ │ tx
├ │ ├── [hash]
├ │ │ ├──page.tsx # /tx/{hash} (URL)
├── layout.tsx # Home Page's Root Layout
├── page.tsx # home page
#...
```
## Dashboard
在 v13.3 推出 Parallel Route 的 feature,讓一個 page 的不同 content 去處理自己 fetching data 週期,或搭配不同頻率來更新畫面,它適合處理一個 layout 有非常多 dynamic view。
對於 Explorer 的 dashboard 來說,像是 Network Stats 的更新頻率應該會跟 Latest Txs 相同,而 Latest Blocks 會是相對長一點才需要更新。透過 Parallel Route,可以保留三者之間的擴充性,同時去分開處理它們的互動性與資料。



### 使用方式 & 名詞
1. `Parallel Route`
讓同一個 URL 底下的畫面能更好切割處理與呈現,從切割檔案、載入、Error Handle,都能以 page 的角度去使用。naming convention 是以 `@routeName`,該命名方式又稱為 slot,它同樣是不會影響 URL 的 router segment。
> [官方文件](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes):Parallel Routes **allow you to show more than one page in the same view**, like with complex dashboards or modals. With Parallel Routes, you can simultaneously render one or more pages in the same view that can be navigated independently.

1. 對於 Layout 來說,children 是自動匯集沒有指定名稱的 slot 總和 (`app/(dashboard)/@children/page.tsx` 即是 `app/page.tsx`)、其他有使用 Parallel Route 的 slot 則需要特別說明。
```bash
┌ app
├── (dashboard) # 這個 dashboard Group 底下的內容都是以 `/` 作為 URL
├ ├── (@networkStat)
├ │ ├──page.tsx
├ ├── (@latestBlocks)
├ │ ├──page.tsx
├ ├── (@latestTxs)
├ │ ├──page.tsx
├── block-list
├ ├── page.tsx
├── tx-list
├ ├── page.tsx
├── block
├ ├── [hash]
├ │ ├──page.tsx
├── tx
├ ├── [hash]
├ │ ├──page.tsx
├── layout.tsx
├── page.tsx # home page:可以保留或移除
```
按照上面的結構,則在 `app/layout.tsx` 可以讀取的 props 會變成這樣
```ts
export default async function Layout(props:{
children: React.ReactNode,
networkStat: React.ReactNode,
latestBlocks: React.ReactNode,
latestTxs: React.ReactNode
}) {
return (
<>
<Header />
{/* app/(dashboard)/@networkStat */}
{props.networkStat}
{/* app/(dashboard)/@latestBlocks */}
{props.latestBlocks}
{/* app/(dashboard)/@latestTxs */}
{props.latestTxs}
{/* app/page.tsx 和其他共用此 layout 的 page*/}
{props.children}
<Footer />
</>
);
}
```
2. 如果要使用 parallel route,需要要注意如何定義 layout。
**在 「獨立 URL 的頁面」 與「Router Group」都要新增 layout.tsx**(parallel route 不用),讓 Segment 和 Group 變成 Root Layout 的 nested layout。如此一來能達成:
>
- 共用 Layout,提升維護性
- 不會 re-render,也不會重新向 Server 發請求
- 可以 parallel 處理與載入同一個 URL 的內容
如果不是這樣設定,雖然可能不會造成 crash,但會有 parallel router 沒有顯示、styling 丟失、Console Warning 等問題產生。
- `獨立 URL page`
- 多寫 Root Layout:`<html>` 會被當作 `<body>` 的 node
1. Warning: validateDOMNesting(...): `<html>` cannot appear as a child of `<body>`
2. Warning: You are mounting a new html component when a previous one has not first unmounted
- layout.tsx 丟失 styling: 如果是使用 Tailwind CSS,直接在該 Segment reload 後不會有 styling,但 navigate 回來又顯示,導致行為不一致。
- `Router Group`:不會去載入 parallel 的內容
```bash
┌ app
├── (dashboard) # 這個 dashboard Group 底下的內容都是以 `/` 作為 URL
├ ├── (@networkStat)
├ │ ├──page.tsx
├ ├── (@latestBlocks)
├ │ ├──page.tsx
├ ├── (@latestTxs)
├ │ ├──page.tsx
├ ├── layout.tsx # Router Group 下的 parallel
├ ├── page.tsx
├── block-list
├ ├── layout.tsx # 其他每個 segment 也都要有 layout.tsx
├ ├── page.tsx
├── tx-list
├ ├── layout.tsx
├ ├── page.tsx
├── block
├ ├── [hash]
├ │ ├──layout.tsx
├ │ ├──page.tsx
├── tx
├ ├── [hash]
├ │ ├──layout.tsx
├ │ ├──page.tsx
├── layout.tsx # Root Layout
├── page.tsx # home page:可以保留或移除
```
3. parallel 底下的 router 如果不一致,缺失的頁面會依序找尋 default.tsx (等於可以再客製這頁的 fallback 內容) > Custom Error Page > NextJS Error Page。
## List & Detail
在 App Router 中,如果一個檔案沒有 `use client` 的描述,預設會使用 Server Component。Server Component 可以撰寫成 async,所以可以直接在同一個 page, layout, 或 component 中去寫 async fetcher。
根據需求,List 相關的 page 適合採用 static path + revalidate 的方式,在設定的秒數讓資料過期,並再下一次訪問時,請 Server 再 prerendered 一份新的 HTML 並存做快取,在 Next12 稱為 ISG;而 Detail 是 dynamic path,但 HTML 的內容在有資料後都是固定,所以會採用的方式,會是請 Server 在第一次請求時 prerendered 一份新的 HTML 並存做快取,之後訪問都使用這份快取來生成 HTML
### 使用方式 & 名詞
在 Next13 後,表達 CSR, SSR, SSG, ISG 的方式變得更簡潔。在 [Route Segment 中可以設定的變數](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic) 有很多,以下條列比較通用且簡易設定可完成的部分。
若以單純的 static path 來說,是透過`設定 fetch 的不同參數`:
- CSR:直接在 layout 或 page 加上 `use client`
- SSR:`fetch(url, {cache: 'no-store'})`
- SSG:`fetch(url)` (預設 `{cache: 'force-cache'}`)
- ISG:`fetch(url, { next: {revalidate: 5} })` (以秒為單位)
如此一來能做到不同的 cache data,像是 block-list 和 tx-list 是根據使用者進入頁面的時間點,去決定要餵多新的資料,很適合採用 ISG 的方式,在特定期間內都使用同一份 prerendered 的 HTML,到了 revalidate 設定的時間結束後,再請 Server prerendered 一份新的 HTML 並存做 cache。
---
不過若是 dynamic path 下想做到 SSG 或 ISG,則需要搭配 generateStaticParams。
```ts
// app/block/[hash]/page.tsx
export const dynamicParams = true;
export async function generateStaticParams() {
// 必須是以 Array<Params> 的形式 (Params 是 Object),且值必須是 string,不然會報錯
// ❌Error: A required parameter (id) was not provided as a string received <type> in generateStaticParams for <path_name>
return [{hash: '...'}, {hash: '...'}, ...]
}
async function getBlockDetail(params) {
...
}
export default async function BlockDetail({ params }) {
const blockDetail = await getBlockDetail(params);
return ...
}
```
然這樣根據設定了多少 hash,即可在 build time 得到 Array 數量的 prerendered HTML。不過這樣的使用情境,會產生過多 prerendered HTML,可能造成 build time 逾時或過長。
---
然而,雖然 Detail page 的內容不會改變,但是會有海量的 blockHash 和 txHash,所以不適合完全預先將所有 dynamic params 去生成 path;但是無論是 block 或是 tx 的 detail 都是在產生之後會固定,所以也不適合採用 SSR 去讓 server 在每次請求都 prerender HTML。
看到這則 [Abilitty to skipping static pages generating at build time](https://github.com/vercel/next.js/issues/45383),認為最好的 rendering pattern 可能是希望達成像 [Page Router 的 fallback='blocking'](https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-blocking) 的設定,讓 dynamic path 在第一次被請求時以 SSR 產生 HTML 並產生 cache,後續再訪問都時拿 cache 的內容。
App Router 有提到 [fallback 的參數變改成 global var 的 dynamicParams (Boolean)](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration#replacing-fallback),也就是該 dynamic routing 在沒有內容時,是回傳 404 還是 SSR 產生 HTML 並快取起來。該 Features 跟 Page Router 的 getStaticPath 很像,不過稍微不同的是,我們能控制並撰寫 getStaticPath 內實作的邏輯,不確定單純設定 dynamicParams 會不會失去彈性,且不確定能不能有同樣的效果。
> 官方文件: `dynamicParams` controls how params outside of `generateStaticParams` are handled. This replaces the fallback: true | false | 'blocking' option of getStaticPaths in the pages directory. The fallback: 'blocking' option is not included in dynamicParams because **the difference between 'blocking' and true is negligible with streaming**.
```ts
// app/block/[hash]/page.tsx
export const dynamicParams = true; // 補上這行等於 fallback=true
export async function generateStaticParams() {
return [{hash: '...'}, {hash: '...'}, ...]
}
async function getBlockDetail(params) {
...
}
export default async function BlockDetail({ params }) {
const blockDetail = await getBlockDetail(params);
return ...
}
```
> ??? 需要實驗:如果沒有使用 page 沒有使用 `generateStaticParams()` 來產生 static params,整個內容都會變成 SSR 嗎? 還是只要條列幾個,之後都會是如同 `fallback='blocking'` 的行為?
> ??? 需要實驗:如果該頁沒有資料,使用 dynamicParams 的設定是所有 id 都會進行 SSR 嗎?如果之後又有資料的話,會重新生成嗎?
> ??? 需要實驗:dynamicParams 的設定會 cache 什麼資料?是 HTML + RSC Payload 嗎?
## Server Component 的使用
page 都會預設是 static rendering,所以無論是 Server Component 或 Client Component 的初始內容都會被放到 initial HTML 上。所以**雖然 Server Component 和 SSR 的差別是初始 HTML 的不同,但實際上兩者幾乎能做到一樣的效果**。
RSC 可以以 async 讀取 fetch 的資料,到了 NextJS v13 的 App Router 後,單純使用 fetch 撰寫 async function,並在 RSC 呼叫,就可以表達出資料是以 getStaticProps 還是 getServerSideProps 取得。
同理,如果是 Client component,仍然需要搭配 useEffect 來 render API data 在畫面,這些在 hook 才完成的 data,也同樣不會包含在 initial HTML,不過若是有撰寫 loading state,是可以在 hook 完成之前就讀到的 Component 預設值,會被包含在 initial HTML。
??? 什麼是 RSC
??? RSC 建構的整個流程 ([NoScript](https://chrome.google.com/webstore/detail/noscript/doojmbjmlfjjnbmnoijecmcbfeoakpjm):可以用來逐一關掉 JS,用來協助檢測)
### Render 出 React Server Component Tree
有了 Server Component 後,render 的工作會根據 router 和 suspense 切分成 chunk 來載入,而切割 chunk 會進行兩個步驟:
1. `取得 React Server Component Payload (RSC Payload)`:React 先 render Server Component 成 RSC Payload
2. `在 Server prerender HTML`:Next 再根據 RSC Payload,將 Client Component 需要 hydration (JavaScript chunk) 以外的內容在 server 進行 render,轉成初始的 HTML
### RSC Payload
以 .rsc 作為檔名,是類似 JSON 的內容。說明 rendered 的 React Server Components tree,會包含:
> 1. Server Components 的 rendered result
> 2. 紀錄哪裡會有 Client Component 會需要額外向 Server 請求 streaming chunk
> 3. Server Components 要傳給 Client Component 的 props
> 4. Server Component 只能包含可序列化的資料,所以不能包含 function
### client 端 (接收內容的 browser)
在瀏覽器這邊,則和之前的步驟很相似,主要差別是改以 RSC Payload 來協助 DOM 建構,並分拆了 hydration 的過程。
1. `解讀 initial HTML`:產生 router 的初始畫面 (initial page load only)。
2. `Reconcile Component Tree`:有 RSC Payload 後,React 能 reconcile Server component 與 Client component 來建構 Component tree。在此之後操作 DOM 完成更新。再接著根據 RSC Payload 對於不同 source tag 的紀錄,向 Server 發 request 來取得 Client component 要做互動性與 hook 更新的 hydration chunk。
3. `Paralled Streaming 得到 Client component chunk`:進行 hydrate 來更新 Client component 的互動性到 DOM,讓 Application 能完整運作。
> [React source code:Component tree 裡不同 tag 的內容是如何被判斷要進行什麼處理](https://github.com/facebook/react/blob/9d17b562ba427adb1bced98f4231406ec573c422/packages/react-client/src/ReactFlightClientStream.js#L44-L60)
## Netlify:Lgger & Error
- 測試網站:https://test-next-app-router-feats.netlify.app/
### 本地開發 & 佈署報錯
1. 如果在 layout 使用 Component,只能使用單一個,且該 Component 不能使用其他 Component,不然會讀取不到
```ts
// ❌ 會失敗,但不會有 Error
import "../globals.css";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
const Container = ({children}) => {
return <main className="container mx-auto max-w-xl pt-8 min-h-screen">
{children}
</main>
}
// app/components/RootLayout.js:可以讀取到 main 和 children
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<Container>
{children}
</Container>
</body>
</html>
);
}
```
```ts
// ✅可以讀取到
import "../globals.css";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
// app/components/RootLayout.js:可以讀取到 main 和 children
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>
<main className="container mx-auto max-w-xl pt-8 min-h-screen">
{children}
</main>
</body>
</html>
);
}
// app/layout.js
import RootLayout from './components/RootLayout'
export default layout({children}) {
return <RootLayout>{children}</RootLayout>
}
```
2. 如果畫面有 loading.js,則 inital HTML 只會有 loading content,不會有其他載入資料寫在初始 HTML 上。可能需要在 navigate 的 link 加上 click event 來使用全頻遮罩。
3. 在 Netlify 上,如果是 dynamic path,<Link /> 的 prefetch 打 request 會 502,而且進入 dynamic path 後,會直接 crash。測試 Vercel 去 deploy 的運作情況,確定是 Netlify 還要找方式處理。


1. `解決 502 Error`: @netlify/plugin-nextjs 必須在 4.36.1、next 必須在 13.4.x ([Error 討論](https://answers.netlify.com/t/runtime-importmoduleerror-error-cannot-find-module-styled-jsx-nextjs-nextauth/95281/2))
2. `解決 500 Error`: 目前社群看到的做法是額外寫一個 prebuild.js ([Error 討論](https://answers.netlify.com/t/server-edge-not-defined-error-on-nextjs-ssr-functions-cause-site-to-return-500-errors/91793/35))
---
在[這篇文章](https://dev.to/alvesjessica/my-experience-deploying-a-nextjs-application-on-netlify-using-next-app-router-3k1b) 條列了幾乎目前遇到的問題,像是降版後仍然有 500,是因為 **\_\_NEXT_PRIVATE_PREBUNDLED_REACT 的環境變數** 造成讀取不到特定 module (next@13.4.9 已經不會被影響),還有可能是 **Netlify 如何處理 Next13 <Link /> 對於 getStaticParams 的 prefetch**。
目前穩定版本是 **@netlify/plugin-nextjs@4.36.1 + next@13.4.9**,如此可以順利在 Netlify 上使用目前提到的所有 features。
* 順利讓 RSC Payload 也變成 cache data

### Logger
- [Function Log: ISG](https://app.netlify.com/sites/classy-zuccutto-f45b6b/logs/functions/___netlify-odb-handler) 主要是處理 dynamic path (/pokeman/id)
- [Function log: Server](https://app.netlify.com/sites/classy-zuccutto-f45b6b/logs/functions/___netlify-handler) 則是處理其他沒有使用 cache 而需要 Server 重新產生的資源(像是 website icon 會在進入 /pokeman/[id] 重複請求該頁,雖然資源相同,但因為是不同頁面,所以會在第一次進入時被重新 request,但 data 已經有快取過,所以不用擔心重新請求資源)
## Reference
> 1. [Migrate to App Router](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration)