App Router実践編 (1)

## これは何？
App Routerが話題なので一通り試した結果を残しておく。先に以下のページを一通り眺めておくとよい。

https://nextjs.org/docs

`npm run dev`と`npm run build && npm run start`で挙動が異なることがあり、プロダクションでの挙動を確認するために、startで起動することを推奨する。


## 目次
- Routing
  - ページの作成
  - レイアウトの作成
  - テンプレートの作成
    - レイアウトとテンプレートの挙動確認
  - リンクとナビゲーション
    - prefetchの挙動の確認
    - cacheの挙動の確認
    - Partial Renderingの確認
    - Soft Navigationの確認
    - 戻ると進むの確認

そのほかの項目は次回に続く・・・

## ページ作成

まずはファイルを作る。

src/app/page.tsx

```tsx
export default function Page() {
  return <h1>Hello, Next.js!</h1>
}
```

階層を作ることもできるらしい。

src/app/dashboard/page.tsx

```tsx
export default function Page() {
  return <h1>Hello, Next.js!</h1>
}
```

まあこの辺はPages Routerとほぼ一緒なので特に違和感ないと思う。

なお、ここに"use client"を書き、Client Componentとすることもできるのだが、"Down the tree"の方針に背くのでやめておいたほうがいい。

## レイアウト作成

Rootにlayoutを置くことで、以下全ての画面に対して同じ内容を適用できる。

src/app/layout.tsx

```tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
         <div>inserted by root layout</div>
         {children}
      </body>
    </html>
  )
}
```

dashboard配下におけば、dashboard配下のページのみにできる。

src/app/dashboard/layout.tsx

```tsx
export default function DashboardLayout({
  children, // will be a page or nested layout
}: {
  children: React.ReactNode
}) {
  return (
    <div>
      <div>inserted by dashboard layout</div>
      {children}
    </div>
  )
}
```

このとき、localhost:3000/dashboardにアクセスし、DOMを確認してみると、RootLayout → DashboardLayoutの順で書かれていることがわかる。

![スクリーンショット 2023-10-06 194541.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/51390c83-e92e-79ac-450d-eafe4184957f.png)

なお、localhost:3000/は、DashBoardLayoutが書かれてないことはわかる。

![スクリーンショット 2023-10-06 194601.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/e52d5457-7dc6-3b9a-b0b6-8ce488bc7aa4.png)


## テンプレートの作成

レイアウトとは別にテンプレートなるものもある。

src/app/template.tsx

```tsx
export default function Template({ children }: { children: React.ReactNode }) {
  return <div><div>inserted by template</div>{children}</div>
}
```

localhost:3000/dashboardにアクセスして確認する。結果、RootLayout → Template → DashboardLayoutの順で書かれてそうである。

![cap1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/d5a46bdd-479b-ac1e-1499-0a9aa7f298a9.png)

### レイアウトとテンプレートの挙動確認

レイアウトとテンプレートの違いを確認するために、以下のような、ページ遷移したときにカウントを更新する`WithCounter`を用意する。なお、`useState`はサーバコンポーネントで使用できないので、"use client"を使用し境界コンポーネントにする必要がある。


```tsx
"use client"

import { usePathname, useRouter } from "next/navigation"
import { ReactNode, useEffect, useState } from "react"

type Props = {
    children: ReactNode
}

export const WithCounter = ({children}: Props) => {
    const [count, setCount] = useState(0)
    const pathname = usePathname()
    
    useEffect(() => {
        setCount(count+1)
    }, [pathname])

    return <div>
        count={count}
        {children}
    </div>
}
```

この`WithCounter`をsrc/app/layout.tsxに入れる。

```tsx
import { WithCounter } from "./WithCounter"

export default function RootLayout({
    children,
  }: {
    children: React.ReactNode
  }) {
    return (
      <html lang="en">
        <body>
          <WithCounter>
            <div>inserted by root layout</div>
            {children}
           </WithCounter>
        </body>
      </html>
    )
  }
```

同様に、src/app/template.tsxにも入れて挙動を確認する。リンク遷移や戻る進むを繰り返し、RootLayoutに入れたstateは保持され、Templateに入れたstateは都度リセットされることを確認する。

![スクリーンショット 2023-10-06 211342.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/19f6c9f9-fe33-de26-c386-e98f340bcc38.png)

## リンクとナビゲーション

`next/link`を使うと、aタグとして吐き出してくれる。
`usePathname()`を使えば、Client Componentででパスを取ることができる。

さきほどの`WithCounter`を少し改造してpathnameを出力してみましょう。 `pathname=/dashboard`がとれてそうですね。

```tsx
export const WithCounter = ({children}: Props) => {
    const [count, setCount] = useState(0)
    const pathname = usePathname()
    
    useEffect(() => {
        setCount(count+1)
    }, [pathname])

    return <div>
        count={count}, pathname={pathname}
        {children}
    </div>
}
```

残念ながら、`pathname`をサーバコンポーネントで取得する方法はなさそう。


ここで、scrollの挙動も試す。

雑にsrc/app/page.tsxとsrc/app/layout.tsxに

```tsx
<div style={{ height: "1000px"}}>
```

を仕込み、src/app/page.tsxのLinkを以下のように変更。

```tsx
<Link href="/dashboard" scroll={false}>dashboard</Link>
```

スクロールした後に遷移したり、戻ったりで、スクロール位置が保存されることを確認できた。でもなんかこうじゃない感あるな。

一応`router`での遷移も試しておく。Pages Routerとの比較でいうと、`next/router`ではなく、`next/navigation`になっており、使えるメソッドが若干異なっている。

```tsx
"use client"

import { useRouter } from "next/navigation"

export function ToDashboardButton () {
    const router = useRouter()
    const onClick = () => {
        router.push("/dashboard")
    }
    return <button onClick={onClick}>Go To Dashboard</button>
}
```

```tsx
import Link from "next/link";
import { ToDashboardButton } from "./ToDashboardButton";

export default function Page() {
    return <div style={{ height: "1000px"}}>
        <h1>
            Hello, Home page!
        </h1>
        <Link href="/dashboard" scroll={false}>dashboard</Link>
        <ToDashboardButton />  // 追加
    </div>

  }
```

なお、できるだけ`useRouter`ではなく`Link`で遷移せよと書いてあるので、できるだけ`Link`を使うほうがよさそう。

### prefetchの挙動確認
Next.jsは遷移先のページを実際に遷移が行われる前にprefetchする。Dynamic RoutesとStatic Routesの場合で挙動が変わるようなので、両方試す。

まずはStaticな方から。src/app/page.tsxをスクローラブルになるように改造する。

```tsx
export default function Page() {
    return <div>
        <h1>
            Hello, Home page!
        </h1>
        <div style={{ height: "1000px"}}></div> // 追加
        <Link href="/dashboard">dashboard</Link>
        <ToDashboardButton />
    </div>

}
```

Static Routesの場合、`next/link`がブラウザで表示できるようになったタイミングで、遷移先のデータがFetchされていることがわかる。

<!-- 画像 -->

続いてDynamicな方。Dynamic Routesとは、動的にレンダリングされるページのことで、
`headers()`をコンポーネントの頭で呼んでやればお手軽に作成できる。

```tsx
export default function Page() {
    headers()
    return <div>
        <h1>
            Hello, Home page!
        </h1>
        <div style={{ height: "1000px"}}></div> // 追加
        <Link href="/dashboard" scroll={false}>dashboard</Link>
        <ToDashboardButton />
    </div>

}
```

Dynamic Routesも同様にprefetchするのだが、Pageの中身が入っていないようだった。


なお、`Link`はprefetchオプションがあり、falseにすればprefetchしないようにできる。

### cacheの挙動の確認
Router Cacheという機能によりページのリクエスト結果はキャッシュされる。
こちらも、Staticな場合とDynamicmな場合で挙動が異なるとのこと。
以下の結果が得られることを確認する。

- Static Routesな場合
  - 2回目のリクエストを飛ばす時、Dev toolsのNetworkタブで通信は確認できなかった。
  - 5分経つと再リクエストを確認できた。
- Dynamic Routesな場合
  - 2回目のリクエストを飛ばす時、Dev toolsのNetworkタブで通信は確認できなかった。
  - 30秒経つと再リクエストを確認できた。

なお、prefetchオプションによって挙動は変わらず、Router Cacheをオフにすることもできないらしい。


### Partial Rendering
同じディレクトリの下にあるページをレンダリングするとき、その上のレイアウトやテンプレートは再レンダリングされないことを確認する。

まず、以下のページを追加する。

src/app/dashboard/analytics/page.tsx

```tsx
import Link from "next/link"
export default function Page () {
    return <div>
        <h1>analytics</h1>
        <Link href="/dashboard/settings">To Settings</Link>
    </div>
}
```

src/app/dashboard/settings/page.tsx

```tsx
import Link from "next/link"
export default function Page () {
    return <div>
        <h1>settings</h1>
        <Link href="/dashboard/analytics">To Analytics</Link>
    </div>
}
```

次に、useEffectで再レンダリングされるのを防ぐために、以前追加した`WithCounter`をsrc/app/template.tsx, src/app/layout.tsx, src/app/dashboard/layout.tsxから外す。

/dashboard/settingsにアクセスし、/dashboard/analyticsと相互に切り替えを行う。この時、React Dev Toolsでレンダリングの様子を見てみると、RootLayoutだけ残り、Template以下が再レンダリングされていることがわかる。しかし、DashboardLayoutも再レンダリングされているので引き続き調査が必要。

![スクリーンショット 2023-10-07 15.59.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141888/c8c86b2c-057f-82e7-07aa-f22b259a8fdb.png)

### ソフトナビゲーション

ここでは用語の確認だけに止めておく。

- Hard Navigation
  - Reloadや直リンク時に発生
  - ブラウザの状態をリセットする
- Soft Navigation
  - next/linkやrouter.push時に発生
  - Reactのstateや状態は保持される

### 戻ると進む
戻るや進むを繰り返して、Router Cacheが使われて、networkリクエストが飛んでいないことを確認する。


今回使ったソースはこちら。

https://github.com/anddev68/AppRouterTutorial/tree/sample/1st