2016-06-14 12 views
3

私はReactとReduxを使ってWebアプリケーションを構築していますが、私は状態の不変性に関する問題に直面しています。再帰関数(Redux)での状態変化の回避

私の状態は次のようになります -

{ 
    tasks: [ 
     { 
      id: 't1', 
      text: 'Task 1', 
      times: { 
       min: 0, 
       max: 0 
      }, 
      children: [ 
       { id: 't1c1', text: 'Task 1 child 1', times: { min: 2, max: 3 }, children: [] }, 
       { id: 't1c2', text: 'Task 1 child 2', times: { min: 3, max: 4 }, children: [] } 
       ... 
      ] 
     } 
     ... 
    ] 
} 

これは「子供」配列でゼロ以上の子タスクを持つ各タスクに「タスク」オブジェクトの階層を定義します。各タスクには、このタスクが実行する最小時間( "最小")と最大時間( "最大")(時間単位)を記録する「時間」オブジェクトもあります。

私は、タスク階層を横断し、1人以上の子供を持つすべてのタスクの合計「時間」値(最小値と最大値)を計算して設定するために使用される単純な再帰関数を作成しました。したがって、上記の例では、タスク "t1"は、計算後に最小時間5(2 + 3)と最大時間7(3 + 4)を持つ必要があります。この関数は、各タスクが任意の数の祖先を持つことができるので、本質的に再帰的である必要があります。

(以下はテストを行わずにメモリから書かれていたので、無視してください/任意のタイプミスを許す)

function taskTimes(tasks) 
{ 
    let result = { min: 0, max: 0 }; 

    if (tasks.length === 0) 
     return result; 

    tasks.forEach(task => { 
     if (task.children.length > 0) 
     { 
      let times = taskTimes(task.children); 
      task.times.min = times.min; // MUTATING! 
      task.times.max = times.max; // MUTATING! 
      result.min += times.min; 
      result.max += times.max; 
     } 
     else 
     { 
      result.min += task.times.min; 
      result.max += task.times.max; 
     } 
    }); 

    return result; 
} 

私が正しく作品を作成したが、私はそれを書いた前に、私は問題を抱えていることを知っていた機能:横断しながら、タスク階層、個々のタスクオブジェクト(具体的には「時間」オブジェクト)が変更されます。これは減速機で実行したいので問題です。したがって、私はこのアクションから新しい状態を作成したいのですが、それを変更するよりもむしろです。

私の質問は、状態の変異を避けるためにこのシステムを変更する最良の方法は何ですか?

Immutable.jsを使用して階層全体に不変性を適用しようとすると、これはおそらく私の次のステップです。もう1つの選択肢は、このデータをまったく保存するのではなく、必要に応じて各コンポーネントで計算することです。後者はおそらくよりクリーンな選択肢ですが、私はまだ他のプロジェクトでこれと似たようなことをする必要があると予測できるので、これにどのように取り組むべきかを知りたいのです。

この分野のベストプラクティスについてのアドバイスは大変ありがたいです。また、私が非常に明白な何かを見落としてしまったら、親切にしてください! ありがとうございます。

+2

私は最も一般的なベストプラクティスは、リレーショナルテーブルのようなデータ構造を平坦化することだと思います。これにより、内側のキーと配列を変更することなく、新しい状態を簡単に返すことができます。 Normalizrはこれを手助けする非常に普及したツールです。https://github.com/paularmstrong/normalizr – azium

+0

はい、これは間違いなく、特にデータがリンクされて正規化が必要な場合に適しています。おそらく、それぞれのエンティティ(タスク)を独自のキーを持つオブジェクトに入れておくと、コピーを簡単に作成できるようになるでしょう。私はそれをチェックする必要があります。私はnormalizrを見たことがなかったので、あまりにもリンクのおかげで。 – Polaris64

答えて

0

あなたの2番目の仮定は正しいです。可能な限り小さなデータを店舗に保存する必要があります。計算できるデータはその場で計算する必要があります。上記のような再帰的な計算のような重い計算を行っている場合は、店舗から派生したデータを計算するメモ型セレクターを作成するための小さなライブラリredux-reselectを見てください。

+1

私は同意します、そして、redux-reselectに向けて私を指摘してくれてありがとう。キャッシュされた結果を持つコンピューティングデータにはうってつけの解決策のように思えるので、私は間違いなく試してみます。これは私が最終的にプロジェクトで使う方法の一種だと確信しています。 – Polaris64

2

状態を操作しないようにするには、結果を計算する際に操作しても問題がないコピーを作成するには、迅速な&ダーティな回避策が必要です。 子タスクのすべての時間をその関数で更新するのが簡単な方法の場合は、状態の(深い)コピーを作成し、操作して返します。例えば

減速の抜粋:まず

... 
switch(action.type) { 
    case actions.RECALCULATE_TASK_COUNT: 
    var nextState = $.extend({}, state); 
    taskTimes(nextState); 
    return nextState; 
    ... 

} 
... 

function taskTimes(tasks) 
{ 
    let result = { min: 0, max: 0 }; 

    if (tasks.length === 0) 
     return result; 

    tasks.forEach(task => { 
     if (task.children.length > 0) 
     { 
      let times = taskTimes(task.children); 
      task.times.min = times.min; // MUTATING! 
      task.times.max = times.max; // MUTATING! 
      result.min += times.min; 
      result.max += times.max; 
     } 
     else 
     { 
      result.min += task.times.min; 
      result.max += task.times.max; 
     } 
    }); 

    return result; 
} 
+1

ありがとう、はい、再帰関数でインプレースを更新する前に配列のディープコピーを作成することを考えました。一連の浅いコピーを作成して同じことを試みましたが、最初は成功しませんでした。 jQuery(とlodashのmerge())メソッドが深いコピー機能を提供することを指摘してくれてありがとう。 – Polaris64

0

、この問題の適切な解決策に向けて行く答えを提供するためのPcriulanのおかげで、理論的に可能な解決策を提供するためのDETONに感謝問題。

私が作成した最終的な解決策を共有すると思いました。 Pcriulan氏が指摘したように、実際のアプリケーションでこれを解決するには、これはおそらく最良の方法ではありませんが、ここでは理論的な問題を解決するためにこのソリューションを示したかったのです。

私の解決策は、再帰関数を修正して、減速器自体と同様に動作するようにすることです。具体的には、新規または変更されていないタスクオブジェクトのタスク配列(および子配列)のコピーを返します彼らの属性が変更されたかどうかについて。したがって、変更されたオブジェクトのみの完全コピーが提供されるため、比較的効率的でなければなりません。私の単体テストでは、状態が関数によって変更されていないことも示されているため、初期の問題は解決されます。

更新機能は次のとおりです。 -

function getTaskTime(tasks) 
{ 
    let result = { min: 0, max: 0, tasks: [] }; 

    if (tasks.length === 0) 
     return result; 

    // Build a new tasks array 
    result.tasks = tasks.map(task => { 

     if (task.children.length > 0) 
     { 
      // Get times and new task array for task.children 
      const child_result = getTaskTime(task.children); 

      // Add child times to current totals 
      result.min += child_result.min; 
      result.max += child_result.max; 

      // Return new task object with new child array and modified times 
      return Object.assign({}, task, { 
       time: { min: child_result.min, max: child_result.max }, 
       children: child_result.tasks 
      }); 
     } 
     else 
     { 
      // If task has no children, simply add times to current totals and return 
      // the unmodified task object 
      result.min += task.time.min; 
      result.max += task.time.max; 
      return task; 
     } 

    }); 

    return result; 
} 

うまくいけば、これは、似たような状況に直面して他人を助けるだろう。何か改善が必要な場合、または私が逃した問題があれば、お気軽にコメントしてください。