7

私のアプリケーションでは、React + Redux + Reselect + Immutable.js の究極の組み合わせを使用しています。再選択のアイデアが気に入っているのは、状態 (リデューサーによって維持される) をできるだけ単純に保つことができるからです。セレクターを使用して、必要な実際の状態を計算し、それを React コンポーネントに渡します。

ここでの問題は、レデューサーの 1 つを少し変更すると、セレクターが派生出力全体を再計算し、その結果、React UI 全体も更新されることです。純粋なコンポーネントが機能しません。遅いです。

典型的な例: データの最初の部分はサーバーから取得され、基本的に不変です。2 番目の部分はクライアントによって維持され、redux アクションを使用して変更されます。それらは別々のレデューサーによって維持されます。

セレクターを使用して両方のパーツを単一のレコード リストにマージし、それを React コンポーネントに渡します。しかし、明らかに、オブジェクトの 1 つを 1 つ変更すると、リスト全体が再生成され、Records の新しいインスタンスが作成されます。また、UI は完全に再レンダリングされます。

明らかに、毎回セレクターを実行することは正確に効率的ではありませんが、それでもかなり高速であり、そのトレードオフを喜んで行います (コードがよりシンプルでクリーンになるため)。問題は、実際のレンダリングが遅いことです。

Immutable.js ライブラリは、何も変更されていない場合に新しいインスタンスを作成しないほどスマートであるため、新しいセレクター出力を古いものと深くマージする必要があります。しかし、セレクターは以前の出力にアクセスできない単純な関数であるため、それは不可能だと思います。

私の現在のアプローチは間違っていると思います。他のアイデアを聞きたいです。

おそらく、この場合の再選択を取り除き、インクリメンタル更新を使用して目的の状態を維持するレデューサーの階層にロジックを移動することです。

4

3 に答える 3

6

問題は解決しましたが、特定の状況に大きく依存するため、正しい答えはないと思います。私の場合、次のアプローチを採用することにしました。


元のセレクターがうまく処理した課題の 1 つは、任意の順序で配信された多くの部分から最終的な情報がコンパイルされたことです。レデューサーで最終的な情報を段階的に構築することにした場合、考えられるすべてのシナリオ (情報が到着する可能性のあるすべての順序) を考慮し、考えられるすべての状態間の変換を定義する必要があります。一方、再選択では、現在持っているものを単純に取り出して、そこから何かを作ることができます。

この機能を維持するために、セレクター ロジックをラッピングする親 reducer に移動することにしました

さて、A、B、C の 3 つのレデューサーと、対応するセレクターがあるとしましょう。それぞれが 1 つの情報を処理します。作品はサーバーからロードすることも、クライアント側のユーザーから発信することもできます。これは私の元のセレクターになります:

const makeFinalState(a, b, c) => (new List(a)).map(item => 
  new MyRecord({ ...item, ...(b[item.id] || {}), ...(c[item.id] || {}) });

export const finalSelector = createSelector(
  [selectorA, selectorB, selectorC],
  (a, b, c) => makeFinalState(a, b, c,));

(これは実際のコードではありませんが、意味があることを願っています。個々のレデューサーの内容が利用可能になる順序に関係なく、セレクターは最終的に正しい出力を生成することに注意してください。)

私の問題が明確になったことを願っています。これらのレデューサーのいずれかのコンテンツが変更された場合、セレクターは最初から再計算され、すべてのレコードの完全に新しいインスタンスが生成され、最終的に React コンポーネントが完全に再レンダリングされます。

私の現在のソリューションはこれを軽視しています:

export default function finalReducer(state = new Map(), action) {
  state = state
    .update('a', a => aReducer(a, action))
    .update('b', b => bReducer(b, action))
    .update('c', c => cReducer(c, action));

  switch (action.type) {
    case HEAVY_ACTION_AFFECTING_A:
    case HEAVY_ACTION_AFFECTING_B:
    case HEAVY_ACTION_AFFECTING_C:
      return state.update('final', final => (final || new List()).mergeDeep(
        makeFinalState(state.get('a'), state.get('b'), state.get('c')));

    case LIGHT_ACTION_AFFECTING_C:
      const update = makeSmallIncrementalUpdate(state, action.payload);
      return state.update('final', final => (final || new List()).mergeDeep(update))
  }
}

export const finalSelector = state => state.final;

核となるアイデアは次のとおりです。

  • 何か大きなことが起こった場合 (つまり、サーバーから大量のデータを取得した場合)、派生状態全体を再構築します。
  • 何か小さなことが起こった場合 (つまり、ユーザーがアイテムを選択した場合) は、元のレデューサーとラッピングする親レデューサーの両方で、迅速な増分変更を行うだけです (一定の重複がありますが、一貫性と優れたパフォーマンスの両方を達成するために必要です)。

セレクター バージョンとの主な違いは、常に新しい状態を古い状態とマージすることです。Immutable.js ライブラリは、コンテンツが完全に同じ場合、古い Record インスタンスを新しい Record インスタンスに置き換えないほどスマートです。したがって、元のインスタンスが保持され、その結果、対応する純粋なコンポーネントは再レンダリングされません。

明らかに、ディープ マージはコストのかかる操作であるため、これは非常に大きなデータ セットでは機能しません。しかし、実際のところ、この種の操作は、React の再レンダリングや DOM 操作に比べて依然として高速です。したがって、このアプローチは、パフォーマンスとコードの読みやすさ/簡潔さの間の適切な妥協点になる可能性があります。

最後の注意: 個別に処理される軽いアクションがなければ、このアプローチは、純粋なコンポーネントの内部メソッドに置き換えることshallowEqualと本質的に同等です。deepEqualshouldComponentUpdate

于 2016-03-20T22:38:17.830 に答える