これは何?
App Routerが話題なので一通り試した結果を残しておく。先に以下のページを一通り眺めておくとよい。
npm run dev
とnpm 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の順で書かれていることがわかる。
なお、localhost:3000/は、DashBoardLayoutが書かれてないことはわかる。
テンプレートの作成
レイアウトとは別にテンプレートなるものもある。
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の順で書かれてそうである。
レイアウトとテンプレートの挙動確認
レイアウトとテンプレートの違いを確認するために、以下のような、ページ遷移したときにカウントを更新する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は都度リセットされることを確認する。
リンクとナビゲーション
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も再レンダリングされているので引き続き調査が必要。
ソフトナビゲーション
ここでは用語の確認だけに止めておく。
- Hard Navigation
- Reloadや直リンク時に発生
- ブラウザの状態をリセットする
- Soft Navigation
- next/linkやrouter.push時に発生
- Reactのstateや状態は保持される
戻ると進む
戻るや進むを繰り返して、Router Cacheが使われて、networkリクエストが飛んでいないことを確認する。
今回使ったソースはこちら。