React 18からReact 19へ: useEffectを卒業する認証フローの実践例

## 1. はじめに

React 19とNext.jsのApp Routerは、近年のWebアプリケーション開発における大きな転換点となっています。  
特に注目すべきは、これまで開発者にとって煩わしい`useEffect`に依存していた処理の多くを、より**宣言的かつ直感的な構成**で記述できるようになっている点にあります。
本記事では、こうした最新の技術背景を踏まえながら、「認証フロー」という明確な題材にフォーカスし、React 18以前の構成（React 18 + React Router）からの移行を意識した実践的な実装方法を解説していきます。

### React 19 へ移行したいモチベーション

React 19では、useの拡張やuseActionStateの新設によって、非同期処理やフォームアクションの管理がより宣言的かつ簡潔に記述できるようになりました。React 18では冗長になりがちだった一部の記述パターンが改善されたことで、乗り換えを検討する開発者にとって強い動機になります。

#### useの拡張による宣言的なデータフェッチ

useフックがPromiseに対応したことで、非同期データの取得や状態管理をReactコンポーネントの中でそのまま書けるようになりました。認証フローでは、セッションの確認やリダイレクト処理など、非同期処理が頻繁に発生するため、この改善は大きなメリットです。

#### useActionStateによる状態管理の簡素化

新たに導入されたuseActionStateは、フォームに紐づく状態やエラー処理、ローディング状態などを統合的に管理できるフックです。認証フローではサインインやログアウトなど、ユーザー操作に応じた状態変化を伴う処理が多く、この機能の恩恵を受けやすい代表的なユースケースです。

### Next.js のメリット

Next.jsもまた、App Routerやサーバーコンポーネントの導入によって、ルーティングとバックエンド処理の一体化が進み、よりフルスタックな開発が可能になっています。React 19との組み合わせにより、クライアント・サーバーの境界を意識することなく、アプリケーション全体を統一的に設計できます。

#### App Routerによる直感的なルーティング

App Routerは、ファイルベースのルーティングシステムを採用しており、pagesディレクトリよりも柔軟で構造化されたルーティングが可能です。たとえば、app/signin/page.tsx を作成するだけで /signin ルートが即座に機能し、レイアウトの共有やローディング状態の管理も標準で提供されます。これにより、開発の生産性とユーザー体験の両方が向上します。

#### サーバーコンポーネントによるバックエンド機能の統合

サーバーコンポーネントを利用することで、データベース接続やAPIトークンの処理などをフロントエンドから切り離し、安全にサーバー上で実行できます。認証フローでは、セッション管理やトークンの検証処理などを安全に扱えるため、非常に相性が良い設計です。

### なぜ「認証フロー」なのか？

認証フローは、状態管理・非同期処理・セキュリティ・ユーザー体験といった重要な観点が集約される機能です。React 19の新機能やNext.jsのApp Routerの恩恵を良く活かせる題材だと思いました。

## 本記事の目的と技術構成

本記事では、React 19とNext.jsをベースに、最新構成による認証フローの実装方法を解説します。フォームや通知の構築にも配慮した実践的な構成を紹介します。UIには Material UI 5を採用しています。

### 採用した技術スタック

本記事では、以下の技術スタックを使用して認証フローを実装します：

- React 19
- Next.js App Router
- iron-session（セッション管理）
- useSWR（データフェッチ）
- notistack（通知システム）

次のセクションでは、これらの技術スタックのセットアップ方法について詳しく説明します。

## 2. プロジェクトのセットアップ

### Next.js アプリの作成

Next.jsプロジェクトを作成します。TypeScriptを使用することで、型安全性を確保します。コンソール上に表示されるハンズオンに従って、App Routerを選択します。

```bash
npx create-next-app@latest my-app --typescript --app
cd my-app
```

### iron-sessionの導入

セッション管理のためにiron-sessionを導入します。

```bash
npm install iron-session
```

### useSWRの導入

データフェッチのためにuseSWRを導入します。キャッシュ機能を活用することで、不要なリクエストを削減し、パフォーマンスを向上させます。

```bash
npm install swr
```

### notistackの導入

ユーザーフィードバックのためにnotistackを導入します。通知システムの基本的な設定を行い、必要なコンポーネントで使用できるようにします。

```bash
npm install notistack
```

このプロジェクトは次のコマンドでローカル開発環境にホストすることができます。

```bash
npm run dev
```

これで基本的なセットアップは完了です。次のセクションでは、データアクセス層の実装について説明します。

## 3. データアクセス層(Data Access Layer, DAL)の実装

データアクセスを抽象化するDALを設計します。これにより、データソースの変更やAPIの変更に対応しやすくなります。

認証システムにおいて、DALはアクセス制御を担当します。
例えば、未認証ユーザーやトークンの有効期限が切れているユーザーをサインインページにリダイレクトすることができます。

```typescript:lib/dal.ts
export const verifySession = cache(async () => {
  const session = await getIronSession<TSessionData>(
    await cookies(),
    sessionOptions
  )
  if (!session.auth) {
    redirect('/signin', RedirectType.replace)
  }
  const {accessToken, expiresAt} = session.auth
  if (!accessToken || (expiresAt ?? 0) < Date.now()) {
    redirect('/signin', RedirectType.replace)
  }
  return session
})
```

```typescript:app/account/page.tsx
import {verifySession} from '@/lib/dal'
import AccountClient from './AccountClient'
import {use} from 'react'

export default function AccountPage() {
  const session = use(verifySession())
  return <AccountClient session={{...session}} />
}
```

## 4. 認証フローの実装

認証フローでは、iron-sessionによるセッション管理とReact 19のuseActionStateによる状態管理を組み合わせることで、堅牢な認証システムを構築できます。iron-sessionは、セッション情報を暗号化されたCookieとしてクライアントに保存し、それをサーバー側で復号・検証して扱います。、useActionStateはクライアントサイドでユーザーアクションの状態を効率的に管理します。この組み合わせにより、セキュアな認証処理と優れたユーザー体験を両立できます。
React 19の新機能であるuseActionStateを使用して、認証状態を管理します。サインインやログアウトなどのユーザーアクションの状態を効率的に管理できます。

例えば、ログアウト処理では、useActionStateを使用して処理の状態を管理します：

```typescript:app/account/AccountClient.tsx

function AccountContent() {
  const router = useRouter()
  const [error, action, pending] = useActionState(signOut, undefined)
  
  useEffect(() => {
    if (!error) return
    const {message, severity = 'success'} = error
    if (severity === 'success') {
      router.push('/signin')
      return
    }
    if (message) {
      enqueueSnackbar(message, {variant: severity})
    }
  }, [error])

  return (
    <Stack spacing={2} sx={{p: 2}}>
      // ...
      <Box component="form" action={action}>
        <Button type="submit" disabled={pending}>
          ログアウト
        </Button>
      </Box>
    </Stack>
  )
}
```

## 5. ユーザーフィードバックの実装

notistackを使用して、ユーザーへのフィードバックを提供します。認証処理の結果やエラー情報を適切に表示することで、ユーザー体験を向上させます。

### サーバーサイドでの通知管理

Next.jsのApp Routerでは、ページ遷移時にクライアントの状態がリセットされるため、クライアントの一時状態（例：トースト通知）を保持するのが難しくなります。そのため、通知情報は一時的にサーバーサイド（セッション）で管理し、ページ遷移後にクライアントへ引き継いで表示するという設計が効果的です。

ここでは iron-session を使って、セッション上に flashMessages を保存し、クライアント側で取得・表示後に削除する仕組みを実装します。

ページ読み込み時に /api/session/flash を叩き、サーバーから通知を取得して表示します。表示後すぐに mutate([]) を実行してキャッシュを空にすることで、同じ通知が二重に表示されるのを防止しています。

```typescript:components/layout/FlashMessage.tsx
export default function FlashMessage() {
  const {mutate} = useSWR('/api/session/flash', getFlashMessages, {
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
    shouldRetryOnError: false,
    onSuccess: (flashMessages) => {
      if (!flashMessages?.length) return
      console.info('data: ', flashMessages)
      flashMessages.forEach((message) => {
        console.info('message: ', message.message)
        enqueueSnackbar(message.message, {
          variant: message.severity,
        })
      })
      mutate([])
    },
    onError: (error) => {
      console.error('useNotification error', {error})
    },
  })
  const {enqueueSnackbar} = useSnackbar()
  return null
}
```

セッション上の flashMessages は、以下のように GET APIの中で読み出され、即座に空配列にリセットされます。これにより、1回限りの通知（Flash Message）として機能します。

```typescript:app/api/session/route.ts
export async function GET() {
  const session = await getIronSession<TSessionData>(
    await cookies(),
    sessionOptions
  )
  const flashMessages = session.flashMessages ?? []

  // 表示後にセッションから削除
  session.flashMessages = []
  await session.save()

  return Response.json(flashMessages)
}
```

このように、セッションを経由して一時的な通知情報を安全にクライアントへ伝え、使い終わったら即座に破棄するという構成にすることで、App Router環境でも意図通りの通知動作を実現できます。

### 通知のカスタマイズ

notistackの機能を活用して、通知の見た目や動作をカスタマイズします。アプリケーションのデザインに合わせた通知スタイルを実装します。preventDuplicateオプションを使用することで、同じ内容の通知が重複して表示されることを防ぎます。

```typescript:components/layout/ClientLayout.tsx
// ...
<SnackbarProvider
  maxSnack={3}
  preventDuplicate
  anchorOrigin={{
    vertical: 'top',
   horizontal: 'right',
  }}
>
  {children}
</SnackbarProvider>
```

## 7. 制約

### iron-sessionの制約

iron-sessionはセッションベースの認証を手軽に導入できますが、idTokenやrefreshTokenのようなトークンの管理には適していません。多端末でのセッション管理やトークンの失効処理を行いたい場合は、Redisなどの外部ストレージを使ったセッション管理が必要になります。

### Next.jsのデプロイ環境の制約

Next.jsのApp Routerやサーバーコンポーネントは、動的なサーバーサイド処理を含むため、実行環境によっては一部機能に制限や追加設定が必要となる場合があります。

## 8. まとめ

React 19とNext.jsのApp Routerを組み合わせることで、モダンでセキュアな認証フローを簡潔に構築できるようになりました。useやuseActionStateによって、非同期処理やアクション管理をより宣言的に記述できるため、useEffectのしがらみから解放され、開発体験とユーザー体験の両方を向上させることができます。

また、iron-sessionによるセッション管理や、notistackによる通知システムを取り入れることで、実用的かつユーザーフレンドリーな認証機能を素早く実装できます。

## 9. 参考リンク

本記事で紹介した内容に関連するNext.jsおよびReactの公式ドキュメントはこちらです：

- 🔗 [React 19 – What’s New (React公式)](https://react.dev/blog/2024/12/05/react-19)
React 19で導入されたuseActionStateやuseの拡張など、新機能の概要。
- 🔗 [React Docs: useActionState](https://react.dev/reference/react/useActionState)
フォームアクションの状態管理を行う新しいフックの詳細。
- 🔗 [React Docs: use](https://react.dev/reference/react/use)
PromiseをReactコンポーネント内で直接扱うための新しい使い方。
- 🔗 [Next.js App Router ドキュメント](https://nextjs.org/docs/app)
App Routerの基本構成やファイルベースルーティングのガイド。
- 🔗 [Next.js Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components)
- サーバーサイドでの安全なデータ処理方法についての詳細。
- 🔗[Next.js Authentication](https://nextjs.org/docs/app/building-your-application/authentication)
App Routerベースでの認証処理（セッション、リダイレクト、サーバーコンポーネント対応など）のベストプラクティスがまとめられています。