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 でのロード中に表示される。 普通はロードが完了するまで何も表示されない。 (試したところ、SPA モードでのみ使える模様)
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' },
// )
}
Navigating
ナビゲーションは以下のいずれかで行う。
<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
useNavigation
をroot.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')
}