メインコンテンツまでスキップ

React Router v7 (ex. Remix)

Installation

npx create-react-router@latest my-react-router-app

Routing

設定

app/routes.tsに書く。 各ラウトは URL パターンとファイルパス(=Route modules)の組み合わせからなる。 Route modules については詳細後述。

// app/routes.ts
import {
type RouteConfig,
route,
index,
layout,
prefix,
} from '@react-router/dev/routes'

export default [
// indexはパスなし(デフォルト)
index('./home.tsx'),

// routeはパスあり
route('about', './about.tsx'),

// 共通レイアウトを使うラウト。ネストではない。
layout('./auth/layout.tsx', [
route('login', './auth/login.tsx'),
route('register', './auth/register.tsx'),
]),

// 特定のprefixを持つラウト。ネストではない。
...prefix('concerts', [
index('./concerts/home.tsx'),
route(':city', './concerts/city.tsx'),
route('trending', './concerts/trending.tsx'),
]),
] satisfies RouteConfig

v7 ではコンフィグベースがデフォルトになった模様だが、 ファイルベースも設定すれば使えるし、混在も可能。

Nested Routes

ラウトはネスト可能。

export default [
route('dashboard', './dashboard.tsx', [
index('./home.tsx'), // `/dashboard`のときに描写される子ラウト
route('settings', './settings.tsx'), // `/dashboard/settings`のときに描写される子ラウト
]),
] satisfies RouteConfig

子ラウトの描写は<Outlet /> で行う。

import { Outlet } from 'react-router'

export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* will either be home.tsx or settings.tsx */}
<Outlet />
</div>
)
}

Root Route

routes.tsに書いた全てのラウトは、app/root.tsxという特殊なラウトモジュールの<Outlet />に描写される。

Layout Routes

layout()は共通のレイアウトを使用したい場合に便利。 コンポーネントのネストを作成するが、パス的には何も加えない。

layout('./marketing/layout.tsx', [
/* このレイアウトで描写したいラウト群 */
])

レイアウトのコンポーネントで<Outlet/>を使うことで、ネストされたラウトを描写できる。

export default function ProjectLayout() {
return (
<div>
<aside>Example sidebar</aside>
<Outlet />
</div>
)
}

Index Routes

index()で書いたラウトは、**親のOutlet**に描写される。 トップレベルならroot.tsxの Outlet に、ネストされた場合は親の Outlet に描写される。

Route Prefixes

prefix()は、指定したパスの前にプレフィックスを追加する。 わざわざラウトをネストするまでもない場合に便利。

Dynamic Segments

:cityのように Dynamic Segment を書くと、URL から値を取得できる。パースされて型がつく。

import type { Route } from './+types/team'

export async function loader({ params }: Route.LoaderArgs) {
// ^? { teamId: string }
}

export default function Component({ params }: Route.ComponentProps) {
params.teamId
// ^ string
}

Optional Segments

?をつけると、そのセグメントはオプショナルになる。

// OptionalなDynamic Segmentの例
// langはあってもなくてもいい
route(':lang?/categories', './categories.tsx')

// OptionalなStatic Segmentの例
// editはあってもなくてもいい
route('users/:userId/edit?', './user.tsx')

Splats

いわゆる catch-all とか star といわれるもの。

route('files/*', './files.tsx')

// とすると以下のように利用できる

export async function loader({ params }: Route.LoaderArgs) {
const { '*': splat } = params
// splatには /files/ 以降のパスが全部入っている
}

Route Module

ラウトモジュールは以下の機能を実現するための要となっている。

  • automatic code-splitting
  • data loading
  • actions
  • revalidation
  • error boundaries

ラウトモジュールは以下のように書く。

// `route("teams/:teamId", "./team.tsx")` の場合
// こうするとラウトに関する型情報が得られる
import type { Route } from './+types/team'

// デフォルトでエクスポートしたものがコンポーネントとして描写される
export default function Component({ loaderData }: Route.ComponentProps) {
return <h1>{loaderData.someData}</h1>
}

Root Route Module

root.tsxはルートラウトモジュールという特殊なもので、全てのラウトモジュールの祖先となる。

Props

  • loaderData - loader 関数の返値
  • actionData - action 関数の返値
  • params - ラウトパラメータ
  • matches - 現在のラウトツリーでマッチするものの配列

それぞれ hooks でも取得可能で、型も自動でつくのでそっちの方がいいかもね。とのこと。マジ?

以下、ラウトモジュールからエクスポートするもの

loader

コンポーネントがレンダリングされる前に取得したいデータがある場合に使う。 クライアント上で実行されることはなく、サーバーサイドもしくはプリレンダリング時にのみ実行される。

export async function loader({ params }) {
const data = await fetch(`/api/teams/${params.teamId}`)
return { someData: data }
}

clientLoader

クライアントでのみ呼ばれるローダー。通常のローダーに加えて、もしくは代わりに使う。

export async function clientLoader({ serverLoader }) {
// 必要があればサーバー側のローダーを呼び出すことも可能
// const serverData = await serverLoader()
const clientData = getDataFromClient()
return clientData
}

action

サーバーサイドのデータ変更を行うための関数。 実行後にはリバリデーションが自動的に行わ、すべての loader が再実行される。 <Form>,useFetcher,useSubmitなどを使って実行する。

export default function Items({ loaderData }) {
return (
<div>
<Form method="post" navigate={false} action="/list">
<input type="text" name="title" />
<button type="submit">Create Todo</button>
</Form>
</div>
)
}

export async function action({ request }) {
const data = await request.formData()
// DBに登録する処理 goes here
return { ok: true }
}

clientAction

クライアント側でのみ実行される action。

export async function clientAction({ serverAction }) {
// クライアント側でのみ実行したい処理 goes here

// 必要があればサーバー側のアクションを呼び出すことも可能
// const data = await serverAction();
return { ok: true }
}

ErrorBoundary

これがエクスポートされていると、ラウト内でエラーが発生した場合に描写される。 ルートラウトモジュールにおいては必須である。

HydrateFallback

これがエクスポートされていると loader でのロード中に表示される。 普通はロードが完了するまで何も表示されない。

export function HydrateFallback() {
return <p>Loading Game...</p>
}

headers

サーバーレンダリング時のレスポンスヘッダーを設定する。

export function headers() {
return {
'X-Stretchy-Pants': 'its for fun',
'Cache-Control': 'max-age=300, s-maxage=3600',
}
}

handle

ここでセットした値はuseMatches()で受け取ることができる。 パンくずリストを作りたい時などに使う。

links,meta

<head>内に追加したい<link><meta>を設定する。 最終的に全ての links や meta は集約されて、root.tsx<Links /><Meta />で描写される。

shouldRevalidate

アクション実行後に再検証したいかどうかを設定する。

Rendering Strategies

レンダリング戦略は以下の 3 つから選べる。 Pre-render はビルド時に画面を生成する手法で、対象ラウトは個別に指定できる。

  • Client Side Rendering
  • Server Side Rendering
  • Static Pre-rendering

TypeScript

型の情報は+typesディレクトリ?に自動生成されるらしい。 このRoute型が魔法のように型情報を集約し配布する。 ラウトパラメーターも、loader の返り値も、コンポーネントの props も。 以下のように書けばコンポーネントでは型情報が使える。

// route("products/:pid", "./product.tsx");
import type { Route } from './+types/product'

export async function loader({ params }: Route.LoaderArgs) {}
export async function clientLoader({ params }: Route.ClientLoaderArgs) {}
export async function action({ params }: Route.ActionArgs) {}
export async function clientAction({ params }: Route.ClientActionArgs) {}

export default function Product({ loaderData }: Route.ComponentProps) {}

Data Loding

  • Client Data Loading
    • clientLoaderを使う
    • loaderと組み合わせて使うことも可能。その場合は loader->clientLoader の順で実行される
  • Server Data Loading
    • loaderを使う
  • Static Data Loading
    • loaderを使ったうえで、プリレンダリングするパスを指定する

Actions

  • Client Actions
    • ブラウザでのみ実行される
    • 通常の action よりも優先される
  • Server Actions
    • サーバーでのみ実行される
    • クライアント側のコードからは削除される

アクションはコンポーネントと同じファイル内に書くことも可能だし、 必要に応じて独立したファイルにアクションだけを書くことも可能。 ローダーもしかり。

アクションの呼び出し

宣言的に書くにはフォームを使う。

import { Form } from 'react-router'

function SomeComponent() {
return (
<Form action="/projects/123" method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
)
}

命令的に書きたい場合はuseSubmitを使う。

import { useSubmit } from 'react-router'
// hooksで呼び出して、
let submit = useSubmit()
// 何かのイベントハンドラー内などにおいて
submit({ quizTimedOut: true }, { action: '/end-quiz', method: 'post' })

当該ラウトモジュール以外の Action を叩きたい場合や、ブラウザに履歴を積みたくない場合はuseFetcherを使う。 こちらのページに Action で使う例と Loader で使う例がある。

import { useFetcher } from 'react-router'

function Task() {
let fetcher = useFetcher()
let busy = fetcher.state !== 'idle'

return (
// 宣言的に書く場合
<fetcher.Form
method="post"
// いまいるラウトモジュール以外の(アクションだけをもつ独立したラウドモジュールの)アクションを叩くことができる
action="/update-task/123"
>
<input type="text" name="title" />
<button type="submit">{busy ? 'Saving...' : 'Save'}</button>
</fetcher.Form>
)

// 命令的に書く場合 (どこかのイベントハンドラー内などで)
// fetcher.submit(
// { title: 'New Title' },
// { action: '/update-task/123', method: 'post' },
// )
}

ナビゲーションは以下のいずれかで行う。

  • <Link> - デフォルトのナビゲーション
  • <NavLink> - デフォルトのナビゲーションの進化版。アクティブ時、ペンディング時、トランジション時のスタイルを設定できる機能を加えたもの
  • <Form> - 後述
  • redirect - action や loader の中で使う。典型的なのはデータ作成後にそのページに遷移するなど。
  • useNavigate - hooks 内でナビゲーションしたいときに使うが、ユースケースは限られる。なるべく避けるべき。

Form

Form に紐づくのが loader の場合、例えば以下の場合、ユーザーは/search?q=journeyに遷移する。

<Form action="/search" method="get">
<input type="text" name="q" />
</Form>

アクションの場合もページ(以下の場合/create-post)に遷移するが、データはサーチパラメータではなくFormDataとして送信される。

<Form action="/create-data" method="post">
<input type="text" name="title" />
</Form>

Pending UI

loader や action の実行が始まったらすぐにユーザーにフィードバックすべき。

Global Pending UI

useNavigationroot.tsxで使うことで、グローバルにローディングスピナーを表示することができる。

import { useNavigation } from 'react-router'

export default function Root() {
const navigation = useNavigation()
const isNavigating = Boolean(navigation.location)

return (
<html>
<body>
{isNavigating && <GlobalSpinner />}
<Outlet />
</body>
</html>
)
}

Local Pending UI

リンクテキストのスタイルを変える方法。 NavLink の children, className, style props でisPendingの時にしかるべきスタイルを適用する。

Pending Form Submission

フォームのサブミット後は速やかにボタンを無効にするなどのフィードバックを行うべき。 Global UI だと不十分。

useFetcherはそれ単体で独立した state を持つため、これを使うのが最も簡単である。

const fetcher = useFetcher()
// <fetcher.Form>...</fetcher.Form>の中のボタンなどを以下で制御する
const busy = fetcher.state !== 'idle'

non-fetcher なフォームの場合は、useNavigationを使う。

// <Form>...</Form>の中のボタンなどを以下で制御する
const navigation = useNavigation()
const busy = navigation.formAction === '/projects/new' // navigationはグローバルなので手動で指定が必要

Optimistic UI

action 実行時の楽天更新は、fetcher.formDataを先行して利用することで実現する。

const fetcher = useFetcher()
let age = 40
if (fetcher.formData) {
age = fetcher.formData.get('age')
}