LoginSignup
1
0

App Router実践編 (1)

Last updated at Posted at 2023-10-06

これは何?

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

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

目次

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

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

ページ作成

まずはファイルを作る。

src/app/page.tsx

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

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

src/app/dashboard/page.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

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

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

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

スクリーンショット 2023-10-06 194601.png

テンプレートの作成

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

src/app/template.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

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

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

"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に入れる。

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

リンクとナビゲーション

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

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

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に

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

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

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

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

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

"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>
}
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をスクローラブルになるように改造する。

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()をコンポーネントの頭で呼んでやればお手軽に作成できる。

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

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

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

ソフトナビゲーション

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

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

戻ると進む

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0