React、Redux、Immutable.js:効率的なWebアプリケーションの材料

公開: 2022-03-11

React、Redux、Immutable.jsは現在最も人気のあるJavaScriptライブラリのひとつであり、フロントエンド開発に関しては急速に開発者の第一候補になりつつあります。 私が取り組んできたいくつかのReactおよびReduxプロジェクトで、Reactを使い始めた多くの開発者は、Reactと、その可能性を最大限に活用するための効率的なコードの記述方法を完全には理解していないことに気付きました。

このImmutable.jsチュートリアルでは、ReactとReduxを使用して簡単なアプリを作成し、Reactの最も一般的な誤用とそれらを回避する方法を特定します。

データ参照の問題

Reactはパフォーマンスがすべてです。 これは、非常にパフォーマンスが高く、新しいデータの変更に対応するためにDOMの最小限の部分のみを再レンダリングするようにゼロから構築されました。 Reactアプリは、ほとんどの場合、小さな単純な(またはステートレス関数)コンポーネントで構成されている必要があります。 それらは簡単に推論でき、それらのほとんどは、 falseを返すshouldComponentUpdate関数を持つことができます。

 shouldComponentUpdate(nextProps, nextState) { return false; }

パフォーマンスに関しては、最も重要なコンポーネントのライフサイクル関数はshouldComponentUpdateであり、可能であれば常にfalseを返す必要があります。 これにより、このコンポーネントが(最初のレンダリングを除いて)再レンダリングされないことが保証され、Reactアプリが非常に高速に感じられるようになります。

そうでない場合、私たちの目標は、古い小道具/状態と新しい小道具/状態の安価な同等性チェックを行い、データが変更されていない場合は再レンダリングをスキップすることです。

少し前に戻って、JavaScriptがさまざまなデータ型の等価性チェックを実行する方法を確認しましょう。

ブール値文字列整数などのプリミティブデータ型の等価性チェックは、常に実際の値で比較されるため、非常に簡単です。

 1 === 1 'string' === 'string' true === true

一方、オブジェクト配列関数などの複雑な型の等価性チェックは完全に異なります。 2つのオブジェクトが同じ参照(メモリ内の同じオブジェクトを指している)を持っている場合、それらは同じです。

 const obj1 = { prop: 'someValue' }; const obj2 = { prop: 'someValue' }; console.log(obj1 === obj2); // false

obj1とobj2は同じように見えますが、それらの参照は異なります。 それらは異なるため、 shouldComponentUpdate関数内で単純に比較すると、コンポーネントが不必要に再レンダリングされます。

注意すべき重要なことは、Reduxレデューサーからのデータは、正しく設定されていない場合、常に異なる参照で提供されるため、コンポーネントが毎回再レンダリングされることに注意してください。

これは、コンポーネントの再レンダリングを回避するための私たちの探求における中心的な問題です。

参照の処理

深くネストされたオブジェクトがあり、それを以前のバージョンと比較したい例を見てみましょう。 ネストされたオブジェクトの小道具を再帰的にループしてそれぞれを比較することもできますが、明らかにそれは非常にコストがかかり、問題外です。

それは私たちに唯一の解決策を残します、そしてそれは参照をチェックすることです、しかし新しい問題はすぐに現れます:

  • 何も変更されていない場合に参照を保持する
  • ネストされたオブジェクト/配列prop値のいずれかが変更された場合の参照の変更

きれいで、パフォーマンスを最適化した方法で実行したい場合、これは簡単な作業ではありません。 Facebookはずっと前にこの問題に気づき、Immutable.jsを呼び出して救助しました。

 import { Map } from 'immutable'; // transform object into immutable map let obj1 = Map({ prop: 'someValue' }); const obj2 = obj1; console.log(obj1 === obj2); // true obj1 = obj1.set('prop', 'someValue'); // set same old value console.log(obj1 === obj2); // true | does not break reference because nothing has changed obj1 = obj1.set('prop', 'someNewValue'); // set new value console.log(obj1 === obj2); // false | breaks reference

Immutable.js関数はいずれも、指定されたデータに対して直接ミューテーションを実行しません。 代わりに、データは内部で複製され、変更され、変更があった場合は新しい参照が返されます。 それ以外の場合は、初期参照を返します。 obj1 = obj1.set(...);のように、新しい参照を明示的に設定する必要があります。 。

React、Redux、Immutable.jsの例

これらのライブラリの能力を実証する最良の方法は、シンプルなアプリを作成することです。 そして、todoアプリよりも簡単なことは何ですか?

簡潔にするために、この記事では、これらの概念にとって重要なアプリの部分のみを説明します。 アプリコードのソースコード全体はGitHubにあります。

アプリを起動すると、 console.logの呼び出しが重要な領域に配置され、DOMの再レンダリングの量が最小限であることが明確に示されます。

他のtodoアプリと同様に、todoアイテムのリストを表示したいと思います。 ユーザーがToDoアイテムをクリックすると、完了のマークが付けられます。 また、新しいToDoを追加するために上部に小さな入力フィールドが必要であり、下部に3つのフィルターが必要です。これにより、ユーザーは次の項目を切り替えることができます。

  • 全て
  • 完了
  • アクティブ

Reduxレデューサー

Reduxアプリケーションのすべてのデータは単一のストアオブジェクト内に存在し、レデューサーは、ストアをより簡単に推論できる小さな部分に分割する便利な方法と見なすことができます。 レデューサーも機能しているので、さらに小さなパーツに分割することもできます。

レデューサーは2つの小さな部品で構成されます。

  • todoList
  • activeFilter
 // reducers/todos.js import * as types from 'constants/ActionTypes'; // we can look at List/Map as immutable representation of JS Array/Object import { List, Map } from 'immutable'; import { combineReducers } from 'redux'; function todoList(state = List(), action) { // default state is empty List() switch (action.type) { case types.ADD_TODO: return state.push(Map({ // Every switch/case must always return either immutable id: action.id, // or primitive (like in activeFilter) state data text: action.text, // We let Immutable decide if data has changed or not isCompleted: false, })); // other cases... default: return state; } } function activeFilter(state = 'all', action) { switch (action.type) { case types.CHANGE_FILTER: return action.filter; // This is primitive data so there's no need to worry default: return state; } } // combineReducers combines reducers into a single object // it lets us create any number or combination of reducers to fit our case export default combineReducers({ activeFilter, todoList, });

Reduxとの接続

Immutable.jsデータを使用してReduxレデューサーを設定したので、Reactコンポーネントに接続してデータを渡します。

 // components/App.js import { connect } from 'react-redux'; // ….component code const mapStateToProps = state => ({ activeFilter: state.todos.activeFilter, todoList: state.todos.todoList, }); export default connect(mapStateToProps)(App);

完璧な世界では、接続は最上位のルートコンポーネントでのみ実行する必要があり、mapStateToPropsでデータを抽出し、残りは子に小道具を渡す基本的なReactです。 大規模なアプリケーションでは、すべての接続を追跡するのが難しくなる傾向があるため、接続を最小限に抑えたいと考えています。

state.todosはReduxcombineReducers関数から返される通常のJavaScriptオブジェクトであることに注意することが非常に重要です(todosはレデューサーの名前です)が、state.todos.todoListは不変リストであり、そのようなリストにとどまることが重要です。 shouldComponentUpdateチェックに合格するまでフォームを作成します。

コンポーネントの再レンダリングの回避

深く掘り下げる前に、コンポーネントに提供する必要のあるデータのタイプを理解することが重要です。

  • あらゆる種類のプリミティブタイプ
  • 不変形式のみのオブジェクト/配列

これらのタイプのデータがあると、Reactコンポーネントに含まれる小道具を浅く比較できます。

次の例は、可能な限り簡単な方法で小道具を比較する方法を示しています。

 $ npm install react-pure-render
 import shallowEqual from 'react-pure-render/shallowEqual'; shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); }

関数shallowEqualは、1レベルの深さでのみ小道具/状態の差分をチェックします。 これは非常に高速に動作し、不変のデータと完全に相乗効果を発揮します。 すべてのコンポーネントでこのshouldComponentUpdateを記述しなければならないのは非常に不便ですが、幸いなことに簡単な解決策があります。

shouldComponentUpdateを特別な個別のコンポーネントに抽出します。

 // components/PureComponent.js import React from 'react'; import shallowEqual from 'react-pure-render/shallowEqual'; export default class PureComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState); } }

次に、このshouldComponentUpdateロジックが必要なコンポーネントを拡張します。

 // components/Todo.js export default class Todo extends PureComponent { // Component code }

これは、ほとんどの場合、コンポーネントの再レンダリングを回避するための非常にクリーンで効率的な方法です。後でアプリがより複雑になり、突然カスタムソリューションが必要になった場合は、簡単に変更できます。

関数を小道具として渡すときにPureComponentを使用すると、わずかな問題が発生します。 ES6クラスのReactは、これを関数に自動的にバインドしないため、手動で行う必要があります。 これは、次のいずれかを実行することで実現できます。

  • ES6矢印関数バインディングを使用します: <Component onClick={() => this.handleClick()} />
  • bindを使用: <Component onClick={this.handleClick.bind(this)} />

どちらのアプローチでも、毎回異なる参照がonClickに渡されるため、コンポーネントが再レンダリングされます。

この問題を回避するために、次のようにコンストラクターメソッドで関数を事前にバインドできます。

 constructor() { super(); this.handleClick = this.handleClick.bind(this); } // Then simply pass the function render() { return <Component onClick={this.handleClick} /> }

ほとんどの場合、複数の関数を事前にバインドしていることに気付いた場合は、小さなヘルパー関数をエクスポートして再利用できます。

 // utils/bind-functions.js export default function bindFunctions(functions) { functions.forEach(f => this[f] = this[f].bind(this)); } // some component constructor() { super(); bindFunctions.call(this, ['handleClick']); // Second argument is array of function names }

どのソリューションも機能しない場合は、いつでもshouldComponentUpdate条件を手動で記述できます。

コンポーネント内の不変データの処理

現在の不変のデータ設定では、再レンダリングが回避され、コンポーネントの小道具内に不変のデータが残されています。 この不変データを使用する方法はいくつかありますが、最も一般的な間違いは、不変toJS関数を使用してデータをすぐにプレーンJSに変換することです。

toJSを使用して不変データをプレーンJSに深く変換すると、予想どおり非常に低速であるため、再レンダリングを回避するという目的全体が無効になります。 では、不変のデータをどのように処理するのでしょうか。

そのまま使用する必要があるため、Immutable APIはさまざまな関数を提供し、Reactコンポーネント内で最も一般的使用されます。 Redux ReducerからのtodoListデータ構造は、不変形式のオブジェクトの配列であり、各オブジェクトは単一のtodoアイテムを表します。

 [{ id: 1, text: 'todo1', isCompleted: false, }, { id: 2, text: 'todo2', isCompleted: false, }]

Immutable.js APIは通常のJavaScriptと非常に似ているため、他のオブジェクトの配列と同じようにtodoListを使用します。 ほとんどの場合、マップ関数が最適です。

マップコールバック内でtodoを取得します。これは、まだ不変の形式のオブジェクトであり、 Todoコンポーネントで安全に渡すことができます。

 // components/TodoList.js render() { return ( // …. {todoList.map(todo => { return ( <Todo key={todo.get('id')} todo={todo}/> ); })} // …. ); }

次のような不変データに対して複数の連鎖反復を実行することを計画している場合:

 myMap.filter(somePred).sort(someComp)

…次に、最初にtoSeqを使用してSeqに変換し、反復後に次のような目的の形式に戻すことが非常に重要です。

 myMap.toSeq().filter(somePred).sort(someComp).toOrderedMap()

Immutable.jsは特定のデータを直接変更することはないため、常にそのデータの別のコピーを作成する必要があります。このような複数の反復を実行すると、非常にコストがかかる可能性があります。 Seqは、怠惰な不変のデータシーケンスです。つまり、中間コピーの作成をスキップしながら、タスクを実行するために実行する操作をできるだけ少なくします。 Seqは、このように使用するために構築されました。

Todoコンポーネント内で、 getまたはgetInを使用して小道具を取得します。

簡単ですね。

さて、私が気付いたのは、多くの場合、 get() 、特にgetIn()が多数あると、非常に読みにくくなる可能性があるということです。 そこで、パフォーマンスと読みやすさの間にスイートスポットを見つけることにしました。いくつかの簡単な実験の結果、Immutable.jsのtoObject関数とtoArray関数が非常にうまく機能することがわかりました。

これらの関数は、(1レベルの深さの)Immutable.jsオブジェクト/配列をプレーンなJavaScriptオブジェクト/配列に浅く変換します。 内部に深くネストされたデータがある場合、それらは不変の形式のままで、に渡す準備ができていますコンポーネントの子であり、まさにそれが私たちに必要なものです。

get()よりもわずかなマージンで遅くなりますが、見た目はかなりきれいになります。

 // components/Todo.js render() { const { id, text, isCompleted } = this.props.todo.toObject(); // ….. }

すべてを実際に見てみましょう

まだGitHubからコードのクローンを作成していない場合は、今がそれを実行する絶好の機会です。

 git clone https://github.com/rogic89/ToDo-react-redux-immutable.git cd ToDo-react-redux-immutable

サーバーの起動は次のように簡単です(Node.jsとNPMがインストールされていることを確認してください)。

 npm install npm start 

Immutable.jsの例:「Todoの入力」フィールド、5つのTOD(2番目と4番目の取り消し線)、すべて対完了対アクティブのラジオセレクター、および「すべて削除」ボタンを備えたTodoアプリ。

Webブラウザでhttp:// localhost:3000に移動します。 開発者コンソールを開いた状態で、いくつかのToDoアイテムを追加するときにログを監視し、それらに完了のマークを付けて、フィルターを変更します。

  • 5つのtodoアイテムを追加します
  • フィルタを「すべて」から「アクティブ」に変更してから「すべて」に戻します
    • ToDoの再レンダリングは不要で、変更をフィルター処理するだけです
  • 2つのtodoアイテムを完了としてマークします
    • 2つのToDoが再レンダリングされましたが、一度に1つしかレンダリングされませんでした
  • フィルタを「すべて」から「アクティブ」に変更してから「すべて」に戻します
    • 完了した2つのToDoアイテムのみがマウント/アンマウントされました
    • アクティブなものは再レンダリングされませんでした
  • リストの中央から1つのToDoアイテムを削除します
    • 削除されたToDoアイテムのみが影響を受け、他のアイテムは再レンダリングされませんでした

要約

React、Redux、Immutable.jsの相乗効果は、正しく使用すると、大規模なWebアプリケーションで頻繁に発生する多くのパフォーマンスの問題に対するいくつかの洗練されたソリューションを提供します。

Immutable.jsを使用すると、深い同等性チェックの非効率性に頼ることなくJavaScriptオブジェクト/配列の変更を検出できます。これにより、Reactは、必要のないときにコストのかかる再レンダリング操作を回避できます。 これは、Immutable.jsのパフォーマンスがほとんどのシナリオで良好になる傾向があることを意味します。

この記事が気に入って、将来のプロジェクトでReactの革新的なソリューションを構築するのに役立つことを願っています。

関連: ReactコンポーネントがUIテストを容易にする方法