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

コード構造

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

Redux FAQ: コード構造

ファイル構造はどのようにすべきか?アクションクリエーターとリデューサーをどのようにグループ化すべきか?セレクターはどこに配置すべきか?

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

  • Railsスタイル: 「actions」「constants」「reducers」「containers」「components」の各フォルダを分離

  • 「機能フォルダ」/「ドメイン」スタイル: 機能やドメインごとにフォルダを分離し、ファイルタイプごとにサブフォルダを設けることも可能

  • 「Ducks/Slices」スタイル: ドメインスタイルに似るが、アクションとリデューサーを明示的に結びつけ、しばしば同一ファイル内で定義

一般的に、セレクターはリデューサーと共に定義してエクスポートし、他の場所(mapStateToProps関数、非同期アクションクリエーター、sagaなど)で再利用することが推奨されます。これにより、状態ツリーの実際の構造を知るコードをリデューサーファイルに集約できます。

ヒント

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

サンプルは以下のセクションを参照してください:

Detailed Explanation: Example Folder Structure

An example folder structure might look something like:

  • /src
    • index.tsx: Entry point file that renders the React component tree
    • /app
      • store.ts: store setup
      • rootReducer.ts: root reducer (optional)
      • App.tsx: root React component
    • /common: hooks, generic components, utils, etc
    • /features: contains all "feature folders"
      • /todos: a single feature folder
        • todosSlice.ts: Redux reducer logic and associated actions
        • Todos.tsx: a React component

/app contains app-wide setup and layout that depends on all the other folders.

/common contains truly generic and reusable utilities and components.

/features has folders that contain all functionality related to a specific feature. In this example, todosSlice.ts is a "duck"-style file that contains a call to RTK's createSlice() function, and exports the slice reducer and action creators.

ディスク上のコードレイアウトは最終的には重要ではありませんが、アクションとリデューサーを孤立して考えてはならない点に注意してください。あるフォルダで定義されたリデューサーが、別のフォルダで定義されたアクションに応答することは完全に可能(かつ推奨)です。

参考情報

ドキュメント

記事

ディスカッション

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

ロジックのどの部分をリデューサーまたはアクションクリエーターに配置すべきかについて、明確な答えは1つだけではありません。一部の開発者は「fat」アクションクリエーター(アクション内のデータを単純に対応する状態にマージするだけの「thin」リデューサー)を好みます。他の開発者は、アクションを可能な限り小さく保ち、アクションクリエーターでのgetState()の使用を最小限に抑えることを重視します(この質問の目的上、sagaやobservableなどの他の非同期アプローチは「アクションクリエーター」カテゴリに含まれます)。

ロジックの多くをリデューサーに配置することにはいくつかの潜在的な利点があります。アクションタイプがより意味的で有意義になる可能性があります(例: "SET_STATE"ではなく"USER_UPDATED")。さらに、リデューサーに多くのロジックがあると、タイムトラベルデバッグの影響を受ける機能が増えます。

このコメントは二分法をうまく要約しています:

問題は、アクションクリエーターとリデューサーの間に何を配置するか、つまりfatとthinなアクションオブジェクトの選択です。すべてのロジックをアクションクリエーターに配置すると、基本的に状態の更新を宣言するfatアクションオブジェクトができあがります。リデューサーは純粋でダムな、追加・削除・更新関数になります。これは合成しやすいですが、ビジネスロジックのほとんどはそこには存在しません。 リデューサーにより多くのロジックを配置すると、素晴らしいthinアクションオブジェクトが得られ、データロジックのほとんどが1か所にまとまりますが、リデューサーは他のブランチからの情報が必要になる場合があるため合成が難しくなります。その結果、大きなリデューサーや、状態の上位から追加の引数を取るリデューサーができあがります。

ヒント

可能な限り多くのロジックをリデューサーに配置することを推奨します。アクションに入れる内容を準備するために何らかのロジックが必要になる場合がありますが、リデューサーがほとんどの作業を行うべきです。

参考情報

ドキュメント

記事

ディスカッション

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

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

アクションクリエイターは保守性が高い:アクションの更新を一箇所で行い、すべての使用箇所に反映できます。すべてのアクションインスタンスが同じ形式とデフォルト値を保証されます。

アクションクリエイターはテスト可能:インラインアクションの正確性は手動で検証する必要がありますが、アクションクリエイターは他の関数と同様に、一度テストを書けば自動実行できます。

アクションクリエイターはドキュメント化しやすい:パラメーターがアクションの依存関係を明示し、定義を一元化することでドキュメントコメントの記述が容易になります。インラインアクションではこの情報を捕捉・伝達するのが困難です。

アクションクリエイターは強力な抽象化レイヤー:アクション作成にはデータ変換やAJAXリクエストが伴うことがあります。アクションクリエイターはこの多様なロジックに統一インターフェースを提供し、コンポーネントが詳細を知らずにアクションをディスパッチできるようにします。

参考情報

記事

ディスカッション

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

ミドルウェアは、ReduxアプリケーションでWebSocketのような永続的な接続を扱うのに最適な場所です。その理由は以下の通りです:

  • ミドルウェアはアプリケーションのライフタイム全体にわたって存在する

  • ストア自体と同様に、アプリケーション全体で使用できる単一の接続インスタンスで十分な場合が多い

  • ミドルウェアはすべてのディスパッチされたアクションを監視でき、自身もアクションをディスパッチできる。つまり、ミドルウェアはディスパッチされたアクションをWebSocket経由で送信するメッセージに変換したり、WebSocket経由でメッセージを受信した際に新しいアクションをディスパッチしたりできる

  • WebSocket接続インスタンスはシリアライズ不可能なため、ストアの状態自体に含めるべきではありません

この例では、ソケットミドルウェアがReduxアクションに応答しディスパッチする方法を参照してください。

WebSocketや類似の接続向けに既存のミドルウェアが多数存在します - 以下のリンクを参照してください。

ライブラリ

非コンポーネントファイルでReduxストアを使用するには?

アプリケーションごとに単一のReduxストアのみ存在すべきです。これは事実上、アプリケーションアーキテクチャにおけるシングルトンとなります。Reactと併用する場合、ルートの<App>コンポーネントを<Provider store={store}>でラップすることで実行時にストアがコンポーネントに注入されるため、アプリケーションのセットアップロジックのみが直接ストアをインポートする必要があります。

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

他のコードベースファイルでストアを直接インポートすることは避けてください。場合によっては機能するかもしれませんが、循環インポート依存エラーを引き起こすことがよくあります。

解決策の例:

  • ストア依存のロジックをサンクとして記述し、コンポーネントからそのサンクをディスパッチする

  • コンポーネントから関連関数へdispatchの参照を引数として渡す

  • ロジックをミドルウェアとして記述し、セットアップ時にストアに追加する

  • アプリケーション作成時にストアインスタンスを関連ファイルに注入する

一般的なユースケースは、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)

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