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

Redux FAQ: コード構造

目次

ファイル構造はどうあるべきですか?プロジェクト内でアクションクリエイターとリデューサーをどのようにグループ化すべきですか?セレクターはどこに置くべきですか?

Redux は単なるデータストアライブラリであるため、プロジェクトをどのように構成すべきかについて直接的な意見はありません。ただし、ほとんどの Redux 開発者が使用する傾向があるいくつかの一般的なパターンがあります。

  • Rails スタイル:「actions」、「constants」、「reducers」、「containers」、「components」用の個別のフォルダ
  • 「機能フォルダ」/「ドメイン」スタイル:機能またはドメインごとに個別のフォルダを使用し、必要に応じてファイルタイプごとにサブフォルダを使用します。
  • 「Ducks/Slices」:ドメインスタイルに似ていますが、アクションとリデューサーを明示的に関連付け、多くの場合、同じファイルで定義します。

一般的に、セレクターはリデューサーと一緒に定義してエクスポートし、他の場所(mapStateToProps 関数、非同期アクションクリエイター、saga など)で再利用し、状態ツリーの実際の形状を知っているすべてのコードをリデューサーファイルにまとめることをお勧めします。

ヒント

特に、ロジックを「機能フォルダ」に整理し、特定の機能のすべての Redux ロジックを単一の「slice/ducks」ファイルにまとめることをお勧めします。.

例については、このセクションを参照してください。

詳細な説明:フォルダ構造の例

フォルダ構造の例は次のようになります。
  • /src
    • index.tsx: React コンポーネントツリーをレンダリングするエントリポイントファイル
    • /app
      • store.ts: ストアの設定
      • rootReducer.ts: ルートリデューサー (オプション)
      • App.tsx: ルート React コンポーネント
    • /common: フック、汎用コンポーネント、ユーティリティなど
    • /features: すべての「機能フォルダ」が含まれます
      • /todos: 単一の機能フォルダ
        • todosSlice.ts: Redux リデューサーのロジックと関連するアクション
        • Todos.tsx: React コンポーネント

/app には、他のすべてのフォルダに依存するアプリ全体のセットアップとレイアウトが含まれています。

/common には、真に汎用的で再利用可能なユーティリティとコンポーネントが含まれています。

/features には、特定の機能に関連するすべての機能を含むフォルダがあります。この例では、todosSlice.ts は、RTK の createSlice() 関数を呼び出し、スライスリデューサーとアクションクリエイターをエクスポートする「duck」スタイルのファイルです。

最終的にコードをディスク上にどのように配置するかは重要ではありませんが、アクションとリデューサーは個別に考えるべきではないことを覚えておくことが重要です。あるフォルダで定義されたリデューサーが、別のフォルダで定義されたアクションに応答することも(推奨されています)、完全に可能です。

詳細情報

ドキュメント

記事

ディスカッション

リデューサーとアクションクリエイターの間でロジックをどのように分割すべきですか?「ビジネスロジック」はどこに置くべきですか?

リデューサーまたはアクションクリエイターに正確にどのロジックを入れるべきかについての明確な単一の答えはありません。一部の開発者は、「太った」アクションクリエイターを好み、アクションのデータを受け取り、対応する状態に盲目的にマージするだけの「薄い」リデューサーを好みます。他の開発者は、アクションをできるだけ小さく保ち、アクションクリエイターでの getState() の使用を最小限に抑えることを強調しようとします。(この質問の目的では、saga や observable などの他の非同期アプローチは「アクションクリエイター」カテゴリに含まれます。)

リデューサーにより多くのロジックを入れることには、いくつかの潜在的な利点があります。アクションタイプは、より意味的で意味のあるものになる可能性があります("SET_STATE" の代わりに "USER_UPDATED" など)。さらに、リデューサーにより多くのロジックがあるということは、タイムトラベルデバッグによってより多くの機能が影響を受けることを意味します。

このコメントは、二分法をうまくまとめています。

さて、問題は、アクションクリエイターに入れるものとリデューサーに入れるもの、つまり、太いアクションオブジェクトと薄いアクションオブジェクトのどちらを選択するかです。アクションクリエイターにすべてのロジックを入れると、基本的に状態への更新を宣言する太いアクションオブジェクトになります。リデューサーは、純粋で単純な、これの追加、それの削除、これらの更新を行う関数になります。それらは構成するのが容易になります。しかし、ビジネスロジックの多くはそこには存在しません。リデューサーにより多くのロジックを入れると、優れた薄いアクションオブジェクトになり、ほとんどのデータロジックが1か所にまとめられますが、リデューサーは他のブランチからの情報が必要になる可能性があるため、構成が難しくなります。結果として、大きなリデューサーまたは状態の上位から追加の引数を受け取るリデューサーになります。

ヒント

できるだけ多くのロジックをリデューサーに入れることをお勧めします。アクションに入れる内容を準備するのに役立つロジックが必要になる場合もありますが、リデューサーがほとんどの作業を行う必要があります。

詳細情報

ドキュメント

記事

ディスカッション

なぜアクションクリエイターを使うべきですか?

Redux ではアクションクリエイターは必須ではありません。dispatch にオブジェクトリテラルを渡すなど、最適な方法でアクションを自由に作成できます。アクションクリエイターは、Flux アーキテクチャ から生まれ、Redux コミュニティで採用されてきました。これは、いくつかの利点があるためです。

アクションクリエイターはより保守しやすいです。アクションへの更新は1か所で行うことができ、どこにでも適用できます。アクションのすべてのインスタンスは、同じ形状とデフォルト値を持つことが保証されています。

アクションクリエイターはテスト可能です。インラインアクションの正確性は、手動で検証する必要があります。関数と同様に、アクションクリエイターのテストは1回作成して自動的に実行できます。

アクションクリエイターはドキュメント化が容易です。アクションクリエイターのパラメータは、アクションの依存関係を列挙します。また、アクション定義の一元化は、ドキュメントコメントに便利な場所を提供します。アクションがインラインで記述されている場合、この情報をキャプチャして伝達することが難しくなります。

アクションクリエイターは、より強力な抽象化です。アクションの作成には、多くの場合、データの変換やAJAXリクエストの実行が含まれます。アクションクリエイターは、この多様なロジックに対して統一されたインターフェースを提供します。この抽象化により、コンポーネントはアクションの作成の詳細に煩わされることなく、アクションをディスパッチできます。

詳細情報

記事

ディスカッション

WebSocketやその他の永続的な接続はどこに配置すべきか?

ミドルウェアは、ReduxアプリにおけるWebSocketのような永続的な接続に最適な場所です。それにはいくつかの理由があります。

  • ミドルウェアはアプリケーションのライフサイクル全体にわたって存在します。
  • ストア自体と同様に、アプリ全体が使用できる単一の接続インスタンスのみが必要になる可能性があります。
  • ミドルウェアは、ディスパッチされたすべてのアクションを確認し、自身もアクションをディスパッチできます。つまり、ミドルウェアはディスパッチされたアクションをWebSocket経由で送信されるメッセージに変換し、WebSocket経由でメッセージを受信したときに新しいアクションをディスパッチできます。
  • WebSocket接続インスタンスはシリアライズできないため、ストアの状態自体に含めるべきではありません

ソケットミドルウェアがReduxアクションをどのようにディスパッチおよび応答するかを示すこの例を参照してください。

WebSocketやその他の同様の接続には多くの既存のミドルウェアがあります。以下のリンクを参照してください。

ライブラリ

コンポーネントファイル以外でReduxストアを使用するにはどうすればよいですか?

アプリケーションごとにReduxストアは1つだけ存在する必要があります。これにより、アプリのアーキテクチャの観点から見ると、実質的にシングルトンになります。Reactで使用する場合、ストアはルート<App>コンポーネントを囲む<Provider store={store}>をレンダリングすることにより、実行時にコンポーネントに注入されるため、アプリケーションのセットアップロジックのみがストアを直接インポートする必要があります。

ただし、コードベースの他の部分がストアとやり取りする必要がある場合もあります。

他のコードベースファイルにストアを直接インポートすることは避けるべきです。一部のケースでは機能する可能性がありますが、多くの場合、循環インポート依存エラーが発生します。

いくつかの可能な解決策は次のとおりです。

  • ストアに依存するロジックをサンクとして記述し、コンポーネントからそのサンクをディスパッチする
  • コンポーネントからdispatchへの参照を、関連する関数の引数として渡す
  • ロジックをミドルウェアとして記述し、セットアップ時にストアに追加する
  • アプリの作成時に、ストアインスタンスを関連ファイルに注入する。

一般的なユースケースの1つは、Axiosインターセプター内から、Redux状態からトークンなどのAPI認証情報を読み取ることです。インターセプターファイルはstore.getState()を参照する必要がありますが、APIレイヤーファイルにもインポートする必要があるため、循環インポートにつながります。

代わりに、インターセプターファイルからinjectStore関数を公開できます

common/api.js
let store

export const injectStore = _store => {
store = _store
}

axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})

次に、エントリポイントファイルで、ストアをAPIセットアップファイルに注入します

index.js
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)

このようにすると、アプリケーションのセットアップのみがストアをインポートする必要があり、ファイル依存関係グラフは循環依存を回避します。