React Router v7 SPA Mode でクライアントミドルウェアを使ってみた
React Router v7.3.0 で導入された「クライアントミドルウェア」機能を使って認証ガードをリファクタリングしてみました。この記事では、Firebase 認証を使った SPA アプリ「しずかな Remix SPA Example」での実際の使用例をもとに解説します。
TL;DR
- React Router v7.3.0 から
unstable_clientMiddleware
がサポートされました(リリースノート) - ミドルウェアを使うと、各ルートに散らばっていた認証チェックを一箇所にまとめられます
- コンテキストを使ってユーザー情報を共有できるので、ルートローダーでの重複した認証処理が不要になります
- まだ unstable 機能ですが、コードがすっきりして可読性が上がるのでおすすめです
従来の認証実装の問題点
Firebase 認証を使った SPA アプリでは、通常、保護されたルートごとに認証チェックを行う必要があります。例えば:
export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
const user = await requireAuth(request, { failureRedirect: href('/') })
if (user.handle) {
return redirect(href('/:handle', { handle: user.handle }))
}
return null
}
これを すべての保護されたルート に書く必要があり、認証ロジックが散らばって管理が難しくなります。また、ユーザー情報を取得するだけの場合も同様のコードが必要でした。
ミドルウェアを使った改善
React Router v7.3.0 から導入された unstable_clientMiddleware
を使うと、これらの問題を解決できます。
1. セットアップ
まず react-router.config.ts
でミドルウェア機能を有効にします:
// react-router.config.ts
import type { Config } from '@react-router/dev/config'
declare module 'react-router' {
interface Future {
unstable_middleware: true
}
}
export default {
ssr: false,
future: {
unstable_middleware: true,
},
} satisfies Config
2. コンテキストの作成
認証ユーザー情報を共有するためのコンテキストを作成します:
// middlewares/user-context.ts
import { unstable_createContext } from 'react-router'
import type { AppUser } from '~/services/auth'
export const userContext = unstable_createContext<AppUser | null>()
3. 認証ミドルウェアの実装
2種類のミドルウェアを作成しました:
必須認証ミドルウェア(オンボーディング用)
// middlewares/on-boarding-auth-middleware.ts
import {
href,
redirect,
type unstable_MiddlewareFunction as MiddlewareFunction,
} from 'react-router'
import { requireAuth } from '~/services/auth'
import { userContext } from './user-context'
export const onBoardingAuthMiddleware: MiddlewareFunction = async ({
request,
context,
}) => {
// オンボーディング手続きはログインしていないとアクセスできない
const user = await requireAuth(request, { failureRedirect: href('/') })
context.set(userContext, user)
// すでにオンボーディング済みの場合はプロフィールページにリダイレクト
if (user.handle) {
throw redirect(href('/:handle', { handle: user.handle }))
}
}
オプショナル認証ミドルウェア(一般ページ用)
// middlewares/optional-auth-middleware.ts
import type { unstable_MiddlewareFunction as MiddlewareFunction } from 'react-router'
import { isAuthenticated } from '~/services/auth'
import { userContext } from './user-context'
export const optionalAuthMiddleware: MiddlewareFunction = async ({
request,
context,
}) => {
// ログインしている場合はユーザー情報をセット
const user = await isAuthenticated(request)
context.set(userContext, user)
}
4. ミドルウェアの適用
レイアウトルートにミドルウェアを設定することで、そのレイアウト配下のすべてのルートに適用されます:
// routes/welcome+/_layout/route.ts
import { onBoardingAuthMiddleware } from '~/middlewares/on-boarding-auth-middleware'
// Middleware を設定
export const unstable_clientMiddleware = [onBoardingAuthMiddleware]
// routes/$handle+/_layout/route.tsx
import { Outlet } from 'react-router'
import { AppFooter } from '~/components/AppFooter'
import { optionalAuthMiddleware } from '~/middlewares/optional-auth-middleware'
export const unstable_clientMiddleware = [optionalAuthMiddleware]
export default function UserPageLayout() {
return (
<>
<Outlet />
<AppFooter />
</>
)
}
5. ルートローダーでの使用
ミドルウェアを設定すると、ルートローダーでは context
からユーザー情報を取得できるようになります:
export const clientLoader = async ({
params: { handle },
context,
}: Route.ClientLoaderArgs) => {
const isExist = await isAccountExistsByHandle(handle)
if (!isExist) throw data(null, { status: 404 })
// ミドルウェアからセットされたユーザー情報を取得
const user = context.get(userContext)
const posts = await listUserPosts(handle)
return { handle, user, posts, isAuthor: handle === user?.handle }
}
リファクタリング前後の比較
Before:各ルートで認証チェック
// 認証必須ルート
export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
const user = await requireAuth(request, { failureRedirect: href('/') })
if (user.handle) {
return redirect(href('/:handle', { handle: user.handle }))
}
return null
}
// ユーザー情報だけ必要なルート
export const clientLoader = async ({ request, params }: Route.ClientLoaderArgs) => {
// ...
const user = await isAuthenticated(request)
// ...
return { /* ... */ }
}
After:ミドルウェアとコンテキスト
// レイアウトルートにミドルウェアを設定
export const unstable_clientMiddleware = [onBoardingAuthMiddleware]
// ルートローダーでは context からユーザー情報を取得
export const clientLoader = async ({ context }: Route.ClientLoaderArgs) => {
const user = context.get(userContext)
// ...
return { /* ... */ }
}
メリット
- 認証ロジックの集約: 各ルートに散らばっていた認証ロジックを一箇所にまとめられる
- 重複コードの削減: 同じ認証チェックを何度も書く必要がなくなる
-
ユーザー情報の共有:
context
を通じてユーザー情報を簡単に共有できる - 関心の分離: 認証とルートの本来の機能を分離できる
注意点
-
unstable_
プレフィックスがついているように、まだ安定版ではありません - プロジェクトの構造によっては、複数のレイアウトルートにミドルウェアを設定する必要があるかもしれません
- 将来的に API が変更される可能性があるので、アップデート時には注意が必要です
まとめ
React Router v7.3.0 のクライアントミドルウェア機能を使うことで、認証ロジックを一箇所にまとめ、コードの可読性と保守性を大幅に向上させることができました。まだ unstable 機能ですが、大きなメリットがあるので、小〜中規模の SPA プロジェクトでは積極的に活用していきたい機能です。
今回紹介したコードは「しずかな Remix SPA Example」というFirebase認証を使ったReact Router v7のサンプルアプリで実装しています。実際の使い方を確認したい方はぜひ参照してみてください。
Firebase 認証を使った SPA アプリの開発において、React Router v7 のミドルウェア機能が皆さんのコード品質向上に役立てば幸いです。
Discussion