イマーでより良いレデューサー
公開: 2022-03-10React開発者は、状態を直接変更してはならないという原則をすでに理解している必要があります。 あなたはそれが何を意味するのか疑問に思うかもしれません(私たちのほとんどは私たちが始めたときにその混乱を持っていました)。
このチュートリアルはそれを正当化します。不変の状態とは何か、そしてその必要性を理解するでしょう。 また、Immerを使用して不変の状態を操作する方法と、それを使用する利点についても学習します。 この記事のコードは、このGithubリポジトリにあります。
JavaScriptの不変性とそれが重要な理由
Immer.jsは、Michel Weststrateによって作成された小さなJavaScriptライブラリであり、その使命は、「不変の状態をより便利な方法で操作できるようにすること」です。
しかし、Immerに飛び込む前に、JavaScriptの不変性と、それがReactアプリケーションで重要である理由について簡単に復習しましょう。
最新のECMAScript(別名JavaScript)標準は、9つの組み込みデータ型を定義しています。 これらの9つのタイプのうち、 primitive
値/タイプと呼ばれるものが6つあります。 これらの6つのプリミティブは、 undefined
、 number
、 string
、 boolean
、 bigint
、およびsymbol
です。 JavaScriptのtypeof
演算子を使用して簡単にチェックすると、これらのデータ型の型が明らかになります。
console.log(typeof 5) // number console.log(typeof 'name') // string console.log(typeof (1 < 2)) // boolean console.log(typeof undefined) // undefined console.log(typeof Symbol('js')) // symbol console.log(typeof BigInt(900719925474)) // bigint
primitive
は、オブジェクトではなく、メソッドを持たない値です。 現在の議論で最も重要なのは、プリミティブの値は、一度作成されると変更できないという事実です。 したがって、プリミティブはimmutable
であると言われます。
残りの3つのタイプは、 null
、 object
、およびfunction
です。 typeof
演算子を使用してタイプを確認することもできます。
console.log(typeof null) // object console.log(typeof [0, 1]) // object console.log(typeof {name: 'name'}) // object const f = () => ({}) console.log(typeof f) // function
これらのタイプはmutable
です。 つまり、値は作成後いつでも変更できます。
なぜ私が配列[0, 1]
をそこに持っているのか不思議に思うかもしれません。 そうですね、JavaScriptlandでは、配列は単に特殊なタイプのオブジェクトです。 null
と、それがundefined
とどのように違うのかについても疑問に思っている場合に備えて。 undefined
は、変数の値を設定していないことを意味しますが、 null
はオブジェクトの特殊なケースです。 何かがオブジェクトであるべきであることがわかっているが、オブジェクトがそこにない場合は、単にnull
を返します。
簡単な例で説明するために、ブラウザコンソールで以下のコードを実行してみてください。
console.log('aeiou'.match(/[x]/gi)) // null console.log('xyzabc'.match(/[x]/gi)) // [ 'x' ]
String.prototype.match
は、 object
タイプである配列を返す必要があります。 そのようなオブジェクトが見つからない場合は、 null
を返します。 undefined
を返すことは、ここでも意味がありません。
それで十分です。 不変性の説明に戻りましょう。
MDNドキュメントによると:
「オブジェクトを除くすべてのタイプは、不変の値(つまり、変更できない値)を定義します。」
このステートメントには、特殊なタイプのJavaScriptオブジェクトであるため、関数が含まれています。 ここで関数の定義を参照してください。
可変および不変のデータ型が実際に何を意味するかを簡単に見てみましょう。 ブラウザコンソールで以下のコードを実行してみてください。
let a = 5; let b = a console.log(`a: ${a}; b: ${b}`) // a: 5; b: 5 b = 7 console.log(`a: ${a}; b: ${b}`) // a: 5; b: 7
私たちの結果は、 b
がaから「派生」しa
いる場合でも、 b
の値を変更してもa
の値には影響しないことを示しています。 これは、JavaScriptエンジンがステートメントb = a
を実行すると、新しい別個のメモリ位置を作成し、そこに5
を配置し、その位置にb
を指すという事実から生じます。
オブジェクトはどうですか? 以下のコードを検討してください。
let c = { name: 'some name'} let d = c; console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"some name"}; d: {"name":"some name"} d.name = 'new name' console.log(`c: ${JSON.stringify(c)}; d: ${JSON.stringify(d)}`) // {"name":"new name"}; d: {"name":"new name"}
変数d
を介してnameプロパティを変更すると、 c
でも名前プロパティが変更されることがわかります。 これは、JavaScriptエンジンがステートメントc = { name: 'some name
'
}
を実行すると、JavaScriptエンジンがメモリ内にスペースを作成し、オブジェクトを内部に配置して、 c
を指すという事実から生じます。 次に、ステートメントd = c
を実行すると、JavaScriptエンジンはd
を同じ場所にポイントします。 新しいメモリ位置は作成されません。 したがって、 d
の項目への変更は、暗黙的にc
の項目に対する操作です。 あまり努力しなくても、なぜこれが問題になるのかがわかります。
Reactアプリケーションを開発していて、変数c
から読み取って、ユーザーの名前をsome name
として表示したいとします。 しかし、他のどこかで、オブジェクトd
を操作することによってコードにバグを導入しました。 これにより、ユーザーの名前がnew name
として表示されます。 c
とd
がプリミティブであれば、その問題は発生しません。 しかし、プリミティブは、典型的なReactアプリケーションが維持しなければならない種類の状態には単純すぎます。
これは、アプリケーションで不変の状態を維持することが重要である主な理由です。 Immutable.js READMEのこの短いセクションを読んで、他のいくつかの考慮事項を確認することをお勧めします。不変性の場合です。
Reactアプリケーションで不変性が必要な理由を理解したので、Immerがそのproduce
関数で問題にどのように取り組むかを見てみましょう。
イマーのproduce
機能
ImmerのコアAPIは非常に小さく、使用する主な関数はproduce
関数です。 produce
は、初期状態と、状態を変更する方法を定義するコールバックを受け取るだけです。 コールバック自体は、意図されたすべての更新を行う状態のドラフト(同一ですが、それでもコピー)コピーを受け取ります。 最後に、すべての変更が適用された新しい不変の状態をproduce
します。
この種の状態更新の一般的なパターンは次のとおりです。
// produce signature produce(state, callback) => nextState
これが実際にどのように機能するかを見てみましょう。
import produce from 'immer' const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], } // to add a new package const newPackage = { name: 'immer', installed: false } const nextState = produce(initState, draft => { draft.packages.push(newPackage) })
上記のコードでは、開始状態と、ミューテーションの発生方法を指定するコールバックを渡すだけです。 それはそれと同じくらい簡単です。 州の他の部分に触れる必要はありません。 initState
はそのままで、開始状態と新しい状態の間で触れなかった状態の部分を構造的に共有します。 私たちの州のそのような部分の1つは、 pets
の配列です。 produce
d nextState
は、変更した部分と変更しなかった部分を含む不変の状態ツリーです。
このシンプルでありながら有用な知識を武器に、 produce
がReactレデューサーを簡素化するのにどのように役立つかを見てみましょう。
イマーでレデューサーを書く
以下に定義された状態オブジェクトがあるとします。
const initState = { pets: ['dog', 'cat'], packages: [ { name: 'react', installed: true }, { name: 'redux', installed: true }, ], };
そして、新しいオブジェクトを追加し、次のステップで、 installed
ているキーをtrue
に設定しました。
const newPackage = { name: 'immer', installed: false };
JavaScriptオブジェクトと配列拡散構文を使用してこれを通常の方法で行う場合、状態リデューサーは次のようになります。
const updateReducer = (state = initState, action) => { switch (action.type) { case 'ADD_PACKAGE': return { ...state, packages: [...state.packages, action.package], }; case 'UPDATE_INSTALLED': return { ...state, packages: state.packages.map(pack => pack.name === action.name ? { ...pack, installed: action.installed } : pack ), }; default: return state; } };
これは不必要に冗長であり、この比較的単純な状態オブジェクトと間違えやすいことがわかります。 また、状態のあらゆる部分に触れる必要がありますが、これは不要です。 Immerを使用してこれを単純化する方法を見てみましょう。
const updateReducerWithProduce = (state = initState, action) => produce(state, draft => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'UPDATE_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
また、数行のコードで、レデューサーを大幅に簡素化しました。 また、デフォルトの場合、Immerは何もしなくてもドラフト状態を返すだけです。 ボイラープレートコードが少なくなり、状態の広がりがなくなることに注目してください。 Immerでは、更新したい状態の部分にのみ関心があります。 `UPDATE_INSTALLED`アクションのように、そのようなアイテムが見つからない場合は、何も触れずに先に進みます。 `produce`関数はカリー化にも役立ちます。 `produce`の最初の引数としてコールバックを渡すことは、カリー化に使用することを目的としています。 カレーの「プロデュース」のサインは//curried produce signature produce(callback) => (state) => nextState
カレーを使って以前の状態を更新する方法を見てみましょう。 私たちのカレー製品は次のようになります。 const curriedProduce = produce((draft, action) => { switch (action.type) { case 'ADD_PACKAGE': draft.packages.push(action.package); break; case 'SET_INSTALLED': { const package = draft.packages.filter(p => p.name === action.name)[0]; if (package) package.installed = action.installed; break; } default: break; } });
カレードプロデュース関数は、最初の引数として関数を受け入れ、次の状態を生成するための状態のみを必要とするカレードプロデュースを返します。 関数の最初の引数はドラフト状態です(これは、このカレードプロデュースを呼び出すときに渡される状態から派生します)。 次に、関数に渡したいすべての引数に従います。
この関数を使用するために今必要なのは、次の状態を生成する状態とそのようなアクションオブジェクトを渡すことだけです。
// add a new package to the starting state const nextState = curriedProduce(initState, { type: 'ADD_PACKAGE', package: newPackage, }); // update an item in the recently produced state const nextState2 = curriedProduce(nextState, { type: 'SET_INSTALLED', name: 'immer', installed: true, });
useReducer
フックを使用するReactアプリケーションでは、上記のように状態を明示的に渡す必要がないことに注意してください。これは、それを処理するためです。
最近のReactのすべてのように、Immerはhook
を取得しているのでしょうか? さて、あなたは良いニュースと一緒にいます。 Immerには、状態を操作するための2つのフックがありますuseImmer
フックとuseImmerReducer
フックです。 それらがどのように機能するか見てみましょう。
useImmer
およびuseImmerReducer
フックの使用
useImmer
フックの最も良い説明は、use-immerREADME自体にあります。
useImmer(initialState)
はuseState
と非常によく似ています。 この関数はタプルを返します。タプルの最初の値は現在の状態です。2番目の値はアップデーター関数です。これは、プロデューサーが終了して変更が行われるまで、draft
を自由に変更できるイマープロデューサー関数を受け入れます。不変で次の状態になります。
これらのフックを利用するには、メインのImmerライブラリに加えて、フックを個別にインストールする必要があります。
yarn add immer use-immer
コード用語では、 useImmer
フックは次のようになります
import React from "react"; import { useImmer } from "use-immer"; const initState = {} const [ data, updateData ] = useImmer(initState)
そして、それはそれと同じくらい簡単です。 あなたはそれがReactのuseStateであると言うことができますが、少しステロイドが含まれています。 更新機能の使い方はとても簡単です。 ドラフト状態を受け取り、以下のように好きなだけ変更できます。
// make changes to data updateData(draft => { // modify the draft as much as you want. })
Immerの作成者は、コードサンドボックスの例を提供しており、それがどのように機能するかを試してみることができます。
useImmerReducer
は、ReactのuseReducer
フックを使用したことがある場合も同様に簡単に使用できます。 同様の署名があります。 それがコード用語でどのように見えるかを見てみましょう。
import React from "react"; import { useImmerReducer } from "use-immer"; const initState = {} const reducer = (draft, action) => { switch(action.type) { default: break; } } const [data, dataDispatch] = useImmerReducer(reducer, initState);
レデューサーがdraft
状態を受け取り、必要なだけ変更できることがわかります。 実験するためのコードサンドボックスの例もここにあります。
そして、それはイマーフックを使用するのがいかに簡単かです。 ただし、プロジェクトでImmerを使用する必要がある理由がまだわからない場合は、Immerを使用するために私が見つけた最も重要な理由のいくつかを要約します。
イマーを使用する理由
状態管理ロジックを長期間作成したことがあれば、Immerが提供するシンプルさにすぐに感謝するでしょう。 しかし、Immerが提供するメリットはそれだけではありません。
Immerを使用すると、比較的単純なレデューサーで見たように、ボイラープレートコードの記述が少なくなります。 これにより、ディープアップデートも比較的簡単になります。
Immutable.jsなどのライブラリでは、不変性のメリットを享受するために新しいAPIを学習する必要があります。 ただし、Immerを使用すると、通常のJavaScript Objects
、 Arrays
、 Sets
、およびMaps
で同じことを実現できます。 学ぶべき新しいことは何もありません。
Immerは、デフォルトで構造共有も提供します。 これは単に、状態オブジェクトに変更を加えると、Immerが状態の変更されていない部分を新しい状態と前の状態の間で自動的に共有することを意味します。
Immerを使用すると、オブジェクトが自動的にフリーズすることもできます。つまり、 produced
た状態を変更することはできません。 たとえば、Immerを使い始めたとき、Immerのproduce関数によって返されるオブジェクトの配列にsort
メソッドを適用しようとしました。 アレイに変更を加えることができないというエラーがスローされました。 sort
を適用する前に、配列スライスメソッドを適用する必要がありました。 繰り返しになりますが、生成されたnextState
は不変の状態ツリーです。
Immerも強く型付けされており、gzipで圧縮するとわずか3KBと非常に小さくなります。
結論
状態の更新を管理することになると、Immerを使用するのは私にとって簡単です。 これは非常に軽量なライブラリであり、まったく新しいことを学ぼうとせずに、JavaScriptについて学んだすべてのことを使い続けることができます。 プロジェクトにインストールして、すぐに使い始めることをお勧めします。 既存のプロジェクトでそれを追加して使用し、レデューサーを段階的に更新できます。
また、MichaelWeststrateによるImmerの紹介ブログ投稿を読むことをお勧めします。 私が特に興味深いと思うのは、「Immerはどのように機能するのか」です。 Immerがプロキシなどの言語機能やコピーオンライトなどの概念をどのように活用するかを説明するセクション。
また、このブログ投稿をご覧になることをお勧めします:JavaScriptの不変性:著者のSteven deSalasが不変性を追求することのメリットについての彼の考えを提示する対照的な見方。
この投稿で学んだことで、すぐにImmerを使い始めることができることを願っています。
関連リソース
use-immer
、GitHub- Immer、GitHub
-
function
、MDN Webドキュメント、Mozilla -
proxy
、MDN Webドキュメント、Mozilla - オブジェクト(コンピューターサイエンス)、ウィキペディア
- 「JSでの不変性」、GitHubのOrji Chidi Matthew
- 「ECMAScriptのデータ型と値」、Ecma International
- JavaScript、Immutable.js、GitHubの不変コレクション
- 「不変性の場合」、Immutable.js、GitHub