---
# System prepended metadata

title: next app router

---

### SSR 讓你造成的誤解

#### 前情提要：
在 next 中說的 client component 指的是 CSR component + SSR page，要區別是否是 client component 或是 server component 在於 pre-render + hydrate 是否都在 server 端去執行，所以 SSR(getServerSideProps) 的 component 屬於 client component 範疇。

**實作 getServerSideProps 在next 中會遇到的狀況：**
1. app : 因為 next 對於 server component的解析會分辨 app 中的 component 是 client component 或是 server component，差別在於：

     **server component**： 所有的 html css js 甚至是 hydrate 都在 server 完成 client 端只是負責接收 chunk response  去 render，所以你無法在 server component 中用 getServerSideProps。
     
     **client component**： next 不會去執行或是去渲染給出 inition html，所以在 app folder 中使用 getServerSideProps 是很詭異的事情，因為不會去 trigger getServerSideProps 這個 function，同時你這個頁面也不會有原本 ssr效果，會是白頁。
 
** 小節：** 你不能在 app folder 中使用 getServerSideProps
 
2. page :  回傳 inition html 、 json payload 、css，讓client端負責下載以上內容後，在進行 hydrate，

補充：不管你是在 page 或是 app 所有 web 內容都是 serverSide，差別在於他們解析模塊的方式不同。

#### 模塊解釋：
app: chunk response，所有內容都完成瀏覽器只是負責渲染。
page: json payload ，會有inition html 、 json data、bundle js 、 css，client 端負責 hydrate。


```typescript

// chunk response
module.exports = {
    tag: 'Server Root',
    props: {...},
    children: [
        { tag: "Client Component1", props: {...}: children: [] },
        { tag: "Server Component1", props: {...}: children: [
            { tag: "Server Component2", props: {...}: children: [] },
            { tag: "Server Component3", props: {...}: children: [] },
        ]}
    ]
}
```

總結：
1. hydrate 時機點，app大部分比page都在server完成，所以這大大減少 ttl 時間，page 可能會有等待 ttl 問題（ 儘管 UI 渲染好但你的 js 目前無法 interactive）


### redirect not use in try cache

因為 redirect 本身會 throw 整個 error 出來讓 next 做出 redirect 功能，這樣導致redirect 在try cache中無法引用，因為如果包起來沒有 `throw error` `next` 在 `global` 中知道要轉頁，解決方式如 `handleSubmitBeforeAfter` 用 `isRedirectError` 判斷是不是 `redirect error` 然後 `throw error` 出去 ：

![](https://hackmd.io/_uploads/SJYdrGA8n.png)

https://github.com/vercel/next.js/issues/49298#issuecomment-1537433377

或是在 `try catch` 中再次 `redirect`
```typescript
export async function createCourse(form: FormData) {
	try {
		const data = {
			id: CourseId.random().value,
			name:  form.get("name") as string,
			duration: form.get("duration") as string,
		}

		const url = "mongodb://localhost:27017/next13-fullstack"
		const client = MongoClientFactory.createClient("mooc", { url })
		const repository = new MongoCourseRepository(client)

		const course = Course.fromPrimitives(data)
		await repository.save(course);

		redirect("/", RedirectType.push)
	} catch (error) {
		let message= "Something went wrong"

		console.log(error)

		if (error instanceof Error) {
			message = error.message
		}


		redirect(`/?error=${message}`, RedirectType.push)
	}
}
```
## slot 
react 會用 slot 方式處理 server component render 的原因，有一部分是設計規則。


```typescript
'use client'
 
// This pattern will **not** work!
// You cannot import a Server Component into a Client Component.
import ExampleServerComponent from './example-server-component'
 
export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
 
      <ExampleServerComponent />
    </>
  )
}
```


你不能直接在 client component 中直接引用 server component，這樣會造成 server 無數次往返，導致 react 無法正確在 client 端 render ExampleServerComponent，以上的寫法目前 server component 不能這樣做。

但 如果你需要在client component 的 children 中，加入 server component的話改成這樣寫法就ok。


```typescript

'use client'
 
import { useState } from 'react'
 
export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
 
      {children}
    </>
  )
}

// some page
<ExampleClientComponent>
    <ExampleServerComponent/>
</ExampleClientComponent>
```

## Layout vs template

### Layout

只會渲染一次不會重新 `render` 好處是可以保留靜態資源

#### Layouts do not receive searchParams

因為 `layout` 永遠不會 `render` 所以他會在導頁時候會保留 `searchParams` 的狀態，例如底下的 `/dashboard/settings` 跟 `/dashboard/analytics` ，如果你在 `layout` 中有 `searchParams` 的 `common UI` 永遠都不會 `render` 

![image](https://hackmd.io/_uploads/rkEzNN8kll.png)

所以如果你有關於 `common searchParams UI`  你需要包一個 `ClientComponent` 如此這個 `ClientComponent` 就不會被`layout` 的 `render` 受限

```typescript
import { ClientComponent } from '@/app/ui/ClientComponent'
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <ClientComponent />
      {/* Other Layout UI */}
      <main>{children}</main>
    </>
  )
}
```

`layout` 還有一個建議是不要在這邊做 `auth` 驗證，這會讓整個頁面失去 `PPR` 的優勢，因為如果在 `layout` 做驗證，因為會呼叫 `get cookie` 的 `function` 而這個 `function` 就會被 `next` 當作是 `dynamic function`  變相告訴 `next` 
### template

跟 `layout` 相反他會在每次 `render page` 的時候都會觸發，所以很適合做一些需要 `reset` 的共用 `UI` 例如 `page` 的 `transition` 或是 `breadcrumb`


```typescript
"use client"

import { usePathname } from "next/navigation"

export default function Template({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()

  return (
    <div className="animate-in slide-in-from-bottom-30 duration-500 p-4 ">
      <pre className="inline-block bg-muted px-2 py-1 rounded-lg">{pathname}</pre>
      {children}
    </div>
  )
}
```


### Layout approach

1. 基於 layout 做 auth 的前提是如果只需要做單次的驗證就可以的話就可以使用這個 `pattern` 在頂層確認 `user` 的 `access token` 存在與否有就可以進入到 `page` 反之就到 `sign in` ，但這樣有一個風險會是可能在 `next` 傳送 `json payload` 給 `browser render` 的 `chunk` 會有入要燈入後才能看到的內容被包起來。

```typescript
export default function Layout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const isAuthenticated = false
  if (!isAuthenticated) {
    redirect('/login')
  }
  return (
    <div>
      {children}
    </div>
  );
}

```
![截圖 2025-04-24 下午2.15.45](https://hackmd.io/_uploads/SJiIvUvJll.png)

### Next page rendering flow
1. `browser` 向 `nextjs` 請求 `render page` 跟 `layout`
2. `layout` 跟 `page` 的 `render` 彼此是平行的，且 `layout` 只會 `render` 一次最終 `next` 會做最後的組裝 `page` + `layout`
3. 如果 `user` 拜訪另外一個 `page` 是基於同一個 `layout` 底下的話同樣的 `layout` 不會再重新渲染，而是直接請求新的 `page` 然後再組裝離這個 `page` 的 `layout`
![截圖 2025-04-24 下午2.17.27](https://hackmd.io/_uploads/BkKtDLDklx.png)

## 每個頁面去驗證

另一個則是在 `page` 頁面每次都重新發送請求，這樣也不會有內容外洩的疑慮，同時也可以 `dynamic` 的確保 `user` 的 `auth` 情況來決定 `context` 要是什麼。

```typescript
import React from 'react'
const geyPaidContent = (token: string) => {
  if (token !== 'correct') return 'Login to view this page'
  return 'subscribe to me!'
}
function PaidPage() {
  const token = 'exampletoken'
  const paid = geyPaidContent(token)
  return (
    <div>{paid}</div>
  )
}

export default PaidPage
```
![截圖 2025-04-24 下午2.29.51](https://hackmd.io/_uploads/H1UtcLPylx.png)


## Middleware

另外一個最安全且保有彈性的寫法是基於 `middleware` 去做身份驗證，透過 `middleware` 去驗證 `user token` 存在與否，同時也不像在 `layout` 只會 `render` 一次，也不會有 `senstive data` 外流的狀況 
![截圖 2025-04-24 下午2.36.56](https://hackmd.io/_uploads/SkZ7nLPkeg.png)

```typescript
import { unauthorized } from 'next/navigation'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  const sessionToken = request.cookies.get('authToken')?.value
  const isPaid = false
  if (!isPaid && sessionToken) {
    unauthorized()
  }
  if (!isPaid && !sessionToken) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

// See "Matching Paths" below to learn more
export const config = {
  matcher: '/middleware',
}
```

## Implement Incremental Static Regeneration

1. 在 `app router` 中可以採用底下的方式去 `static render ` 概念是我可以先 `pre-build` `10` 頁的 `blog page` 減少 `build time` 時間
2. 透過 `export const dynamic = 'force-static'` 告 `next` 如果 `user` 訪問第 `11` 頁的時候再產出第 `11` 頁的內容。

```typescript
// app/blog/[id]/page.tsx
export const revalidate = 60
export const dynamicParams = true
export const dynamic = 'force-static'

export async function generateStaticParams() {
    const posts = await fetch('https://api.vercel.app/blog').then((res) =>
        res.json()
    ) as Post[]
    return posts.slice(0, 10).map((post) => ({
        id: String(post.id),
    }))
}
```
所以當你 `next build` 好後會在 `.next` 中預先有前 `10` 頁的內容
![截圖 2025-09-12 上午10.52.11](https://hackmd.io/_uploads/SJHgj-Wsll.png)
等到在 `request time` 需要第 `11` 頁的東西後 `next` 才會產出對應的 `html`
![截圖 2025-09-12 上午10.52.20](https://hackmd.io/_uploads/SkSeo--sxg.png)


## Partial Prerendering

在這邊 `Toaster` 使用 `Suspense` 原因是 `async` 的 `component` ，另一個原因是在 `PPR` 中 `component` 使用 `dynamic` 的 `API` 會強制讓頁面變成`dynamic` ，`Suspense` 可以讓頁面保留靜態的頁面。

```typescript
import { cookies } from "next/headers";

export async function Toaster() {
  const cookieStore = await cookies();
  const toasts = cookieStore
    .getAll()
    .filter((cookie) => cookie.name.startsWith("toast-") && cookie.value)
    .map((cookie) => ({
      id: cookie.name,
      message: cookie.value,
    }));

  return (
    <div>
      {toasts.map((toast) => (
        <div key={toast.id} className="mt-4">
          <p>{toast.message}</p>
        </div>
      ))}
    </div>
  );
}

```

```typescript
import { Toaster } from "./toaster";

export default function RootLayout({
  children,
}: Readonly<{
  children: ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="mx-auto max-w-2xl bg-slate-100 antialiased">
        {children}

        <Suspense>
          <Toaster />
        </Suspense>
      </body>
    </html>
  );
}
```

## Server Type Component
https://conform.guide/