Next.js での Redux Toolkit のセットアップ
- Next.js フレームワークで Redux Toolkit をセットアップして使用する方法
- ES2015 の構文と機能に関する知識
- React の用語に関する知識: JSX、ステート、関数コンポーネント、Props、および フック
- Redux の用語と概念の理解
- クイックスタートチュートリアルとTypeScript クイックスタートチュートリアルを完了し、理想的には完全なRedux Essentialsチュートリアルも完了することを推奨します。
はじめに
Next.js は、React 用の一般的なサーバーサイドレンダリングフレームワークであり、Redux を適切に使用する上でいくつかの固有の課題があります。これらの課題には以下が含まれます。
- リクエストごとの安全な Redux ストアの作成: Next.js サーバーは複数のリクエストを同時に処理できます。これは、Redux ストアをリクエストごとに作成する必要があり、ストアをリクエスト間で共有すべきではないことを意味します。
- SSR フレンドリーなストアのハイドレーション: Next.js アプリケーションは、最初にサーバー上で、次にクライアント上で再度レンダリングされます。クライアントとサーバーの両方で同じページコンテンツのレンダリングに失敗すると、「ハイドレーションエラー」が発生します。そのため、ハイドレーションの問題を回避するには、Redux ストアをサーバー上で初期化し、次にクライアント上で同じデータで再初期化する必要があります。
- SPA ルーティングのサポート: Next.js は、クライアントサイドルーティングのハイブリッドモデルをサポートしています。顧客の最初のページロードは、サーバーから SSR 結果を取得します。後続のページナビゲーションはクライアントによって処理されます。これは、レイアウトで定義されたシングルトンストアを使用すると、ルート固有のデータはルートナビゲーション時に選択的にリセットする必要があり、ルート固有ではないデータはストアに保持する必要があることを意味します。
- サーバーキャッシュフレンドリー: 最近のバージョンの Next.js (特に App Router アーキテクチャを使用するアプリケーション) は、積極的なサーバーキャッシュをサポートしています。理想的なストアアーキテクチャは、このキャッシュと互換性がある必要があります。
Next.js アプリケーションには、Pages Router と App 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/toolkit
と react-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 はステートフルになることを意図していません。RSC がグローバルストアから値を読み書きすると、Next.js App Router のアーキテクチャに違反します。
- ストアには変更可能なデータのみを含める必要がある - グローバルで変更可能なデータ用には、Redux を控えめに使用することをお勧めします。
これらの推奨事項は、Next.js App Router で記述されたアプリケーションに固有のものです。シングルページアプリケーション (SPA) はサーバー上で実行されないため、ストアをグローバル変数として定義できます。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: {}
})
}
これで、Redux Toolkit が提供する強力な型安全性を維持しながら (TypeScript を使用することを選択した場合)、リクエストごとにストアインスタンスを作成するために使用できる関数 makeStore
ができました。
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>()
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()
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
refは、ルート変更ごとにストアが一度だけ初期化されるようにするために使用されます。
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
RSCからの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ストアを作成する- "クライアント"コンポーネントを使用して、ReactアプリケーションコンポーネントにReduxストアを提供する
- クライアントコンポーネントのみがReactコンテキストにアクセスできるため、クライアントコンポーネントでのみReduxストアを操作する
- React-Reduxで提供されているフックを使用して、通常どおりにストアを使用する
- レイアウトにあるグローバルストアにルートごとの状態がある場合を考慮する必要がある
次は何ですか?
Reduxコアドキュメントの「Redux Essentials」および「Redux Fundamentals」チュートリアルを一通り行うことをお勧めします。これにより、Reduxの仕組み、Redux Toolkitの機能、およびその正しい使用方法を完全に理解できます。