このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
Redux Toolkit の Next.js でのセットアップ
- Next.js フレームワークで Redux Toolkit をセットアップして使用する方法
- ES2015 の構文と機能に精通していること
- React の用語に関する知識: JSX、ステート、関数コンポーネント、プロップス、および フック
- Redux の用語と概念の理解
- クイックスタートチュートリアル と TypeScript クイックスタートチュートリアル を一通り行うことを推奨します。理想的には、完全な Redux エッセンシャル チュートリアルも行ってください
はじめに
Next.js は React のための人気のあるサーバーサイドレンダリングフレームワークで、Redux を適切に使用する上でいくつかの特有の課題があります。これらの課題には以下が含まれます:
-
リクエストごとに安全な Redux ストアの作成: Next.js サーバーは複数のリクエストを同時に処理できます。これは、Redux ストアはリクエストごとに作成する必要があり、ストアをリクエスト間で共有してはならないことを意味します。
-
SSR フレンドリーなストアのハイドレーション: Next.js アプリケーションは 2 回レンダリングされます。最初にサーバーで、次にクライアントで再度レンダリングされます。クライアントとサーバーの両方で同じページコンテンツをレンダリングしないと、"ハイドレーションエラー"が発生します。そのため、Redux ストアはサーバーで初期化され、ハイドレーションの問題を避けるために同じデータでクライアントで再初期化する必要があります。
-
SPA ルーティングのサポート: Next.js はクライアントサイドルーティングのハイブリッドモデルをサポートしています。ユーザーが最初にページを読み込むと、サーバーから SSR 結果が得られます。それ以降のページナビゲーションはクライアントが処理します。これは、レイアウトで定義されたシングルトンストアでは、ルート固有のデータはルートナビゲーション時に選択的にリセットする必要があり、ルート固有ではないデータはストアに保持する必要があることを意味します。
-
サーバーキャッシュとの親和性: Next.js の最近のバージョン(特に App Router アーキテクチャを使用するアプリケーション)は、積極的なサーバーキャッシュをサポートしています。理想的なストアアーキテクチャは、このキャッシュと互換性があるべきです。
Next.js アプリケーションには 2 つのアーキテクチャがあります: Pages Router と App Router。
Pages Router は Next.js の元々のアーキテクチャです。Pages Router を使用している場合、Redux のセットアップは主に next-redux-wrapper ライブラリ を使用して行われます。このライブラリは Redux ストアを Pages Router の getServerSideProps のようなデータ取得メソッドと統合します。
このガイドでは App Router アーキテクチャに焦点を当てます。なぜなら、これは Next.js の新しいデフォルトのアーキテクチャオプションだからです。
このガイドの読み方
このページでは、App Router アーキテクチャに基づいた既存の Next.js アプリケーションがあることを前提としています。
手順に沿って進めたい場合は、npx create-next-app my-app で新しい空の Next プロジェクトを作成できます。デフォルトのプロンプトでは App Router が有効化された新しいプロジェクトがセットアップされます。その後、依存関係として @reduxjs/toolkit と react-redux を追加してください。
また、npx create-next-app --example with-redux my-app で新しい Next+Redux プロジェクトを作成することもできます。このコマンドには、このページで説明する初期セットアップの構成が含まれています。
App Router アーキテクチャと Redux
Next.js App Routerの主な新機能は、Reactサーバーコンポーネント(RSC)のサポート追加です。RSCはサーバーでのみレンダリングされる特別なReactコンポーネントであり、クライアントとサーバーの両方でレンダリングされる「クライアント」コンポーネントとは異なります。RSCはasync関数として定義でき、データ取得のために非同期リクエストを行う際にレンダリング中にプロミスを返します。
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が存在しないため考慮不要です。またシングルトンストアは任意のデータを保存できます。
フォルダ構成
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ストア作成
最初の変更点は、storeをグローバルまたはモジュールシングルトン変数として定義するのではなく、リクエストごとに新しいストアを返すmakeStore関数を定義することです:
- TypeScript
- JavaScript
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']
import { configureStore } from '@reduxjs/toolkit'
export const makeStore = () => {
return configureStore({
reducer: {}
})
}
これでリクエストごとにストアインスタンスを作成できるmakeStore関数ができ、Redux Toolkitが提供する強力な型安全性(TypeScript使用時)を維持できます。
store変数をエクスポートしませんが、makeStoreの戻り値型からRootStateとAppDispatch型を推論できます。
後続の使用を簡素化するため、React-Reduxフックの事前型定義バージョンも作成してエクスポートします:
- TypeScript
- JavaScript
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } 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>()
import { useDispatch, useSelector, useStore } from 'react-redux'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes()
export const useAppSelector = useSelector.withTypes()
export const useAppStore = useStore.withTypes()
ストアの提供
この新しいmakeStore関数を使用するには、ストアを作成しReact-ReduxのProviderコンポーネントで共有する新しい「クライアント」コンポーネントを作成します。
- TypeScript
- JavaScript
'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 | null>(null)
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
export default function StoreProvider({ children }) {
const storeRef = useRef(null)
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コンポーネントにそのデータをプロパティとして定義し、以下のようにスライスでReduxアクションを使用してストアにデータを設定します。
- TypeScript
- JavaScript
'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>
}
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore } from '../lib/store'
import { initializeCount } from '../lib/features/counter/counterSlice'
export default function StoreProvider({ count, children }) {
const storeRef = useRef(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コンポーネントの初期レンダリングごとに、ストアの値を設定する必要があります。
- TypeScript
- JavaScript
'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))}
/>
)
}
'use client'
import { useRef } from 'react'
import { useAppSelector, useAppDispatch, useAppStore } from '../lib/hooks'
import {
initializeProduct,
setProductName
} from '../lib/features/product/productSlice'
export default function ProductName({ 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参照は、ルート変更ごとにストアが一度だけ初期化されることを保証します。
useEffectによるストア初期化は機能しないことに注意が必要です。useEffectはクライアントでのみ実行されるため、サーバーサイドレンダリング結果とクライアントサイドレンダリング結果が一致せず、ハイドレーションエラーや表示のちらつきが発生します。
キャッシュ管理
App Routerにはfetchリクエストキャッシュやルートキャッシュなど4種類のキャッシュがあります。問題を引き起こす可能性が最も高いのはルートキャッシュです。ログインを受け付けるアプリケーションでユーザーごとに異なるデータを表示するルート(例:ホームルート/)がある場合、ルートハンドラーからdynamicエクスポートを使用してルートキャッシュを無効化する必要があります:
- TypeScript
- JavaScript
export const dynamic = 'force-dynamic'
export const dynamic = 'force-dynamic'
データ変更後は、適切にrevalidatePathまたはrevalidateTagを呼び出してキャッシュを無効化する必要があります。
RTK Queryの利用
データ取得にはクライアントサイドのみでRTK Queryを使用することを推奨します。サーバーサイドのデータ取得では、async RSCsからのfetchリクエストを使用してください。
RTK Queryの詳細についてはRedux Toolkit Queryチュートリアルをご覧ください。
将来的には、RTK QueryがReact Server Componentsを介してサーバーで取得したデータを受け取れるようになる可能性がありますが、これはReactとRTK Queryの両方に変更が必要な将来の機能です。
作業内容の確認
Redux Toolkitが正しく設定されていることを確認するために、次の3つの重要な領域をチェックする必要があります:
-
サーバーサイドレンダリング - サーバーのHTML出力を確認し、Reduxストアのデータがサーバーサイドレンダリングされた出力に含まれていることを確認します。
-
ルート変更 - 同じルート内のページ間や異なるルート間を移動し、ルート固有のデータが適切に初期化されることを確認します。
-
ミューテーション - ミューテーションを実行した後、ルートから離れて元のルートに戻り、データが更新されていることを確認することで、ストアがNext.js App Routerのキャッシュと互換性があるか検証します。
全体的な推奨事項
App Routerは、Pages RouterやSPAアプリケーションとは劇的に異なるアーキテクチャを提供します。この新しいアーキテクチャを踏まえて状態管理のアプローチを見直すことをお勧めします。SPAアプリケーションでは、変更可能なデータと不可能なデータの両方を含む大規模なストアを持つことは珍しくありませんが、App Routerアプリケーションでは次のことを推奨します:
-
グローバルに共有される変更可能なデータにのみReduxを使用する
-
その他の状態管理には、Next.jsの状態(検索パラメータ、ルートパラメータ、フォーム状態など)、Reactコンテキスト、Reactフックを組み合わせて使用する
学んだこと
以上がApp RouterでRedux Toolkitをセットアップして使用する方法の概要でした:
configureStoreをmakeStore関数でラップしてリクエストごとにReduxストアを作成する- 「クライアント」コンポーネントを使用してReactアプリケーションコンポーネントにReduxストアを提供する
- Reactコンテキストにアクセスできるのはクライアントコンポーネントのみのため、クライアントコンポーネントでのみReduxストアとやり取りする
- React-Reduxが提供するフックを使用して通常通りストアを利用する
- レイアウト内のグローバルストアにルート固有の状態がある場合に対応する必要がある
次のステップ
Reduxコアドキュメントの「Redux Essentials」と「Redux Fundamentals」チュートリアルを通じて、Reduxの動作方法、Redux Toolkitの機能、正しい使用方法について完全に理解することをお勧めします。