メインコンテンツへスキップ

Next.js での Redux Toolkit のセットアップ

学習内容
前提条件

導入

Next.js は、React の人気のあるサーバーサイドレンダリングフレームワークで、Redux を適切に使用するための独自の課題があります。これらの課題には、以下のようなものがあります。

  • リクエストごとの安全な Redux ストアの作成: Next.js サーバーは複数のリクエストを同時に処理できます。これは、Redux ストアがリクエストごとに作成され、ストアがリクエスト間で共有されないようにする必要があることを意味します。
  • SSR 対応のストアのハイドレーション: Next.js アプリケーションは 2 回レンダリングされます。最初はサーバー上で、次にクライアント上です。クライアントとサーバーの両方で同じページコンテンツのレンダリングに失敗すると、「ハイドレーションエラー」が発生します。したがって、ハイドレーションの問題を回避するには、Redux ストアをサーバー上で初期化し、クライアントで同じデータで再初期化する必要があります。
  • SPA ルーティングのサポート: Next.js は、クライアントサイドルーティングのハイブリッドモデルをサポートしています。顧客の最初のページ読み込みでは、サーバーから SSR の結果が得られます。その後のページナビゲーションは、クライアントによって処理されます。これは、レイアウトで定義されたシングルトンストアの場合、ルート固有のデータをルートナビゲーションで選択的にリセットする必要があり、ルート固有ではないデータはストアに保持する必要があることを意味します。
  • サーバーキャッシング対応: Next.js の最近のバージョン(特に App Router アーキテクチャを使用するアプリケーション)は、積極的なサーバーキャッシングをサポートしています。理想的なストアアーキテクチャは、このキャッシングと互換性がある必要があります。

Next.js アプリケーションには、Pages RouterApp Router の 2 つのアーキテクチャがあります。

Pages Router は、Next.js の元のアーキテクチャです。Pages Router を使用している場合、Redux のセットアップは、主に next-redux-wrapper ライブラリを使用して処理されます。このライブラリは、Redux ストアを getServerSideProps のような Pages Router のデータフェッチメソッドと統合します。

このガイドでは、Next.js の新しいデフォルトのアーキテクチャオプションである App Router アーキテクチャに焦点を当てます

このガイドの読み方

このページでは、App Router アーキテクチャに基づく既存の Next.js アプリケーションが既にあることを前提としています。

もし一緒に進めたい場合は、npx create-next-app my-app で新しい空の Next プロジェクトを作成できます。デフォルトのプロンプトで、App Router が有効になった新しいプロジェクトがセットアップされます。次に、@reduxjs/toolkitreact-redux を依存関係として追加します。

このページで説明されている初期設定が含まれている、npx create-next-app --example with-redux my-app で新しい Next + Redux プロジェクトを作成することもできます。

App Router アーキテクチャと Redux

Next.js App Router の主な新機能は、React Server Components(RSC)のサポートが追加されたことです。RSC は、クライアントとサーバーの両方でレンダリングする「クライアント」コンポーネントとは対照的に、サーバー上でのみレンダリングされる特別なタイプの React コンポーネントです。RSC は async 関数として定義でき、レンダリングするデータを非同期にリクエストする際に、レンダリング中に Promise を返します。

RSC がデータリクエストをブロックできるということは、App Router では、レンダリング用のデータをフェッチするための getServerSideProps がなくなったことを意味します。ツリー内のどのコンポーネントも、データの非同期リクエストを作成できます。これは非常に便利ですが、グローバル変数(Redux ストアなど)を定義すると、それらがリクエスト間で共有されることも意味します。これは、Redux ストアが他のリクエストからのデータで汚染される可能性があるため、問題です。

App Router のアーキテクチャに基づいて、Redux の適切な使用に関する一般的な推奨事項は次のとおりです。

  • グローバルストアは使用しない - Redux ストアはリクエスト間で共有されるため、グローバル変数として定義しないでください。代わりに、ストアはリクエストごとに作成する必要があります。
  • RSC は Redux ストアを読み書きしないでください - RSC はフックやコンテキストを使用できません。これらはステートフルになることを意図していません。RSC にグローバルストアから値を読み書きさせることは、Next.js App Router のアーキテクチャに違反します。
  • ストアには変更可能なデータのみを含める必要があります - Redux をグローバルで変更可能なデータ用に控えめに使用することをお勧めします。

これらの推奨事項は、Next.js App Router で作成されたアプリケーションに固有のものです。シングルページアプリケーション(SPA)はサーバーで実行されないため、ストアをグローバル変数として定義できます。SPA には RSC が存在しないため、RSC について心配する必要はありません。シングルトンストアには、必要なデータを格納できます。

フォルダー構造

Next アプリは、ルートに /app フォルダーがあるか、/src/app の下にネストされるように作成できます。Redux ロジックは、/app フォルダーと一緒に、別のフォルダーに配置する必要があります。一般的に、Redux ロジックは /lib という名前のフォルダーに配置されますが、必須ではありません。

その /lib フォルダー内のファイルとフォルダーの構造はユーザー次第ですが、一般的に、Redux ロジックには「フィーチャーフォルダー」ベースの構造をお勧めします。

一般的な例は次のようになります。

/app
layout.tsx
page.tsx
StoreProvider.tsx
/lib
store.ts
/features
/todos
todosSlice.ts

このガイドでは、そのアプローチを使用します。

初期設定

RTK TypeScript チュートリアルと同様に、Redux ストアのファイルと、推論された RootState および AppDispatch 型を作成する必要があります。

ただし、Next の複数ページアーキテクチャでは、シングルページアプリのセットアップとはいくつかの違いが必要です。

リクエストごとの Redux ストアの作成

最初の変更は、ストアをグローバルとして定義することから、各リクエストに対して新しいストアを返す makeStore 関数を定義することに移行することです。

lib/store.ts
import { configureStore } from '@reduxjs/toolkit'

export const makeStore = () => {
return configureStore({
reducer: {},
})
}

// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']

これで、Redux Toolkit が提供する強力な型安全性を保持しながら(TypeScript を使用することを選択した場合)、リクエストごとにストアインスタンスを作成するために使用できる makeStore 関数ができました。

エクスポートされた store 変数はありませんが、makeStore の戻り値の型から RootState および AppDispatch 型を推論できます。

また、後で使用を簡単にするために、React-Redux フックの事前に型付けされたバージョンを作成してエクスポートすることもできます

lib/hooks.ts
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { RootState, AppDispatch, AppStore } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()

ストアの提供

この新しい makeStore 関数を使用するには、ストアを作成し、React-Redux Provider コンポーネントを使用してストアを共有する新しい「クライアント」コンポーネントを作成する必要があります。

app/StoreProvider.tsx
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'

export default function StoreProvider({
children,
}: {
children: React.ReactNode
}) {
const storeRef = useRef<AppStore>()
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}

return <Provider store={storeRef.current}>{children}</Provider>
}

このサンプルコードでは、参照の値を確認することで、ストアが一度だけ作成されるようにし、このクライアントコンポーネントが再レンダリングに対して安全であることを保証しています。このコンポーネントはサーバー上でリクエストごとに一度だけレンダリングされますが、ツリー内のこのコンポーネントより上位にステートフルなクライアントコンポーネントがある場合や、このコンポーネント自体が再レンダリングを引き起こす他の可変ステートを含んでいる場合は、クライアント上で複数回再レンダリングされる可能性があります。

なぜクライアントコンポーネントなのか?

Reduxストアとやり取りする(作成、提供、読み取り、書き込み)コンポーネントは、クライアントコンポーネントである必要があります。これは、ストアへのアクセスにはReactコンテキストが必要であり、コンテキストはクライアントコンポーネントでのみ利用可能だからです。

次のステップは、ストアが使用されるツリー内の任意の場所にStoreProviderを含めることです。そのレイアウトを使用するすべてのルートがストアを必要とする場合は、レイアウトコンポーネントにストアを配置できます。または、ストアが特定のルートでのみ使用される場合は、そのルートハンドラーでストアを作成して提供できます。ツリーの下位にあるすべてのクライアントコンポーネントでは、react-reduxが提供するフックを使用して、通常と同じようにストアを使用できます。

初期データのロード

ストアを親コンポーネントからのデータで初期化する必要がある場合は、そのデータをクライアントのStoreProviderコンポーネントのpropsとして定義し、以下に示すように、スライスのReduxアクションを使用してストアにデータを設定します。

src/app/StoreProvider.tsx
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'
import { initializeCount } from '../lib/features/counter/counterSlice'

export default function StoreProvider({
count,
children,
}: {
count: number
children: React.ReactNode
}) {
const storeRef = useRef<AppStore | null>(null)
if (!storeRef.current) {
storeRef.current = makeStore()
storeRef.current.dispatch(initializeCount(count))
}

return <Provider store={storeRef.current}>{children}</Provider>
}

追加の設定

ルートごとの状態

next/navigationを使用してNext.jsのクライアント側SPAスタイルのナビゲーションのサポートを使用している場合、顧客がページからページに移動すると、ルートコンポーネントのみが再レンダリングされます。これは、レイアウトコンポーネントで作成および提供されたReduxストアがある場合、ルート変更間で維持されることを意味します。ストアをグローバルな可変データのみに使用している場合は問題ありません。ただし、ストアをルートごとのデータに使用している場合は、ルートが変更されたときにストア内のルート固有のデータをリセットする必要があります。

以下に示すのは、製品の可変名を管理するためにReduxストアを使用するProductNameの例のコンポーネントです。ProductNameコンポーネントは、製品詳細ルートの一部です。ストアに正しい名前があることを保証するために、製品詳細ルートへのルート変更時に発生するProductNameコンポーネントが最初にレンダリングされるたびに、ストア内の値を設定する必要があります。

app/ProductName.tsx
'use client'
import { useRef } from 'react'
import { useAppSelector, useAppDispatch, useAppStore } from '../lib/hooks'
import {
initializeProduct,
setProductName,
Product,
} from '../lib/features/product/productSlice'

export default function ProductName({ product }: { product: Product }) {
// Initialize the store with the product information
const store = useAppStore()
const initialized = useRef(false)
if (!initialized.current) {
store.dispatch(initializeProduct(product))
initialized.current = true
}
const name = useAppSelector((state) => state.product.name)
const dispatch = useAppDispatch()

return (
<input
value={name}
onChange={(e) => dispatch(setProductName(e.target.value))}
/>
)
}

ここでは、ルート固有のデータを設定するために、ストアにアクションをディスパッチするという以前と同じ初期化パターンを使用しています。initialized refは、ストアがルート変更ごとに一度だけ初期化されるようにするために使用されます。

useEffectでストアを初期化することは、useEffectがクライアントでのみ実行されるため機能しないことに注意することが重要です。これにより、サーバー側のレンダリングの結果がクライアント側のレンダリングの結果と一致しないため、ハイドレーションエラーやちらつきが発生します。

キャッシング

App Routerには、fetchリクエストとルートキャッシュを含む4つの独立したキャッシュがあります。問題を引き起こす可能性が最も高いキャッシュは、ルートキャッシュです。ログインを受け入れるアプリケーションがある場合は、ユーザーに基づいて異なるデータをレンダリングするルート(例:ホームルート、/)がある可能性があり、ルートハンドラーからdynamicエクスポートを使用することで、ルートキャッシュを無効にする必要があります。

export const dynamic = 'force-dynamic'

ミューテーション後、必要に応じてrevalidatePathまたはrevalidateTagを呼び出して、キャッシュも無効にする必要があります。

RTK Query

データフェッチには、クライアントでのみRTK Queryを使用することをお勧めします。サーバーでのデータフェッチは、asyncRSCからのfetchリクエストを使用する必要があります。

Redux Toolkit QueryチュートリアルでRedux Toolkit Queryの詳細を学ぶことができます。

注意

将来的には、RTK QueryはReact Server Componentsを介してサーバーでフェッチされたデータを受信できるようになる可能性がありますが、それはReactとRTK Queryの両方に変更が必要な将来の機能です。

作業の確認

Redux Toolkitが正しく設定されていることを確認するために確認する必要がある3つの重要な領域があります。

  • サーバーサイドレンダリング - Reduxストアのデータがサーバーサイドでレンダリングされた出力に存在することを確認するために、サーバーのHTML出力を確認してください。
  • ルート変更 - 同じルートのページ間だけでなく、異なるルートの間を移動して、ルート固有のデータが正しく初期化されていることを確認してください。
  • ミューテーション - ミューテーションを実行し、ルートから離れて元のルートに戻ることで、ストアがNext.js App Routerキャッシュと互換性があることを確認し、データが更新されていることを確認してください。

全体的な推奨事項

App Routerは、Pages RouterまたはSPAアプリケーションのいずれとも大幅に異なるReactアプリケーションのアーキテクチャを提供します。この新しいアーキテクチャを考慮して、状態管理へのアプローチを再考することをお勧めします。SPAアプリケーションでは、アプリケーションを駆動するために必要な、可変と不変の両方のすべてのデータを含む大きなストアを持つのが一般的です。App Routerアプリケーションでは、次のようにすることをお勧めします。

  • グローバルに共有された可変データにのみReduxを使用する
  • 他のすべての状態管理には、Next.jsの状態(検索パラメータ、ルートパラメータ、フォーム状態など)、Reactコンテキスト、Reactフックの組み合わせを使用します。

学んだこと

これは、App RouterでRedux Toolkitを設定して使用する方法の簡単な概要でした。

まとめ
  • makeStore関数でラップされたconfigureStoreを使用して、リクエストごとにReduxストアを作成します
  • 「クライアント」コンポーネントを使用して、ReduxストアをReactアプリケーションコンポーネントに提供します
  • Reactコンテキストにアクセスできるのはクライアントコンポーネントだけなので、クライアントコンポーネントでのみReduxストアとやり取りします
  • React-Reduxで提供されているフックを使用して、通常と同じようにストアを使用します
  • レイアウトにあるグローバルストアにルートごとの状態がある場合を考慮する必要があります

次は何?

Reduxコアドキュメントの「Redux Essentials」と「Redux Fundamentals」のチュートリアルを確認することをお勧めします。これにより、Reduxの仕組み、Redux Toolkitが何をするか、およびその正しい使い方を完全に理解できます。