
ReactでImmerを使う方法 | Immutable Statesのシンプルな使い方
こんにちわ。
最近Reduxを勉強してるんですが、Reduxの思想は、Immutabilityを徹底して、状態変化を追跡可能かつ予測可能な状態にすることです。
しかしながら、肝心なJavaScriptはmutableなので、Immutabilityは自分で実装する必要があります。
そこで出てくるのが、Immerで、これはCopy-on-Writeメカニズム(変更可能なリソースに対するコピー操作を実装するために使用される技術)に基づいて、
不変的な状態を開発するためのライブラリで、とても便利です。
Reduxだけではなく、Reactでもスプレッド構文というスリードット( … )を使わなくて済むんですよね。
ですので、今日はこのImmerに関して解説をしていきたいんですけれども、英語の記事でとても勉強になる記事を見つけたので、
それを参考に解説していきます。
ReactでImmerを使う方法 | Immutable Statesのシンプルな使い方
Reactでは、Immutable stateを使用することで、
変更前と変更後のステートツリーの比較を迅速かつ安価に行うことができます。
その結果、各コンポーネントは、コストのかかるDOM操作を行う前に、再レンダリングするかどうかを決定します。
しかし、すでにご存知かと思いますが
JavaScriptはmutableなので、自分たちでImmutabilityを実装しなければならないのです。
Reduxのような人気のある状態管理ライブラリも同じ哲学に従っています。
私たちがReduxを使用する際には、アプリケーション内での副作用を避けるために、状態を変更しないことが期待されます。
しかし、Immutabilityを手動で実装することは、エラーが発生しやすい大規模プロジェクトでは最良の選択肢ではないかもしれません。
幸いなことに、Immerのような特殊なJavaScriptライブラリがあり、これはステートツリーのImmutabilityをデザインによって強制するものです。
Immerとは何か、その仕組み
Immerは、Copy-on-Writeメカニズム(変更可能なリソースに対するコピー操作を実装するために使用される技術)に基づく、
不変的な状態を開発するために作られた小さなライブラリです。
Immerには、3つの主要な状態があります。
- Current State: 実際の状態データ
- Draft State: すべての変更はこの状態に適用される。
- Next State: この状態は、ドラフト状態への変異に基づいて生成されます。
JavaScriptのobject.assign()やSpread演算子を使ったShallow copyと比較すると、
Immerはパフォーマンス面でかなり優れています。
パフォーマンスの比較について詳しく知りたい方は、Immer vs Shallow copy vs Immutable Perf Testの記事を参照して、
ベンチマークを見てみてください。
また、Immerは、上記のベンチマーク結果を得るために書かなければならないコードの量を減らすことができ、
それがImmerが他のプロダクトから抜きん出ている理由の一つとなっています。
では、Immerの基本的な理解ができたので、
Immerが不変性のための最良のソリューションの1つとして認められている理由を見てみましょう。
なぜImmerはReactのState Immutabilityに適しているのか?
単純なステートを扱っていると、
Immerがコードを複雑にしているように感じるかもしれません。
しかし、より複雑なデータを扱うことになると、Immerは非常に便利になります。
それをよく理解するために、有名なReactのreducerの例を考えてみましょう。
export default (state = {}, action) => {
switch (action.type) {
case GET_ITEMS:
return {
...state,
...action.items.reduce((obj, item) => {
obj[item.id] = item
return obj
}, {})
}
default:
return state
}
}
上のコードは、
React-Reduxの典型的なreducerで、ES6のspread演算子を使って、
state treeオブジェクトのネストしたレベルに潜って値を更新しています。
Immerを使えば、上記のコードの複雑さを簡単に軽減することができます。
実際にImmerを使ってどのように複雑さを減らすことができるのか、例を挙げてみましょう。
import produce from "immer"
export default produce((draft, action) => {
switch (action.type) {
case GET_ITEMS:
action.items.forEach(item => {
draft[item.id] = item
})
}
}, {})
この例では、Immerは状態を拡散するためのコードを簡素化しています。
また、ES6のreduce関数の代わりにForEachループを使用することで、オブジェクトを変異させていることがわかります。
それでは、ImmerをReactで使用する別の例を見てみましょう。
import produce from "immer";
this.state={
id: 14,
email: "stewie@familyguy.com",
profile: {
name: "Stewie Griffin",
bio: "You know, the... the novel you've been working on",
age:1
}
}
changeBioAge = () => {
this.setState(prevState => ({
profile: {
...prevState.profile,
age: prevState.profile.age + 1
}
}))
}
このコードは、以下のように状態を変異させることでリファクタリングできます。
changeBioAge = () => {.
this.setState(
produce(draft => {
draft.profile.age += 1
})
)
}
ご覧のように、Immerはコードラインの数を減らし、コードの複雑さを劇的に減らしました。
Hooksと一緒に使えるか?
Immerのもう一つの大きな特徴は、React Hooksと連携できることです。
Immerはこの機能を実現するためにuse-immerという追加ライブラリを使用しています。
理解を深めるために、例を考えてみましょう。
const [state, setState] = useState({
id: 14,
email: "stewie@familyguy.com",
profile: {
name: "Stewie Griffin",
bio: "You know, the... the novel you've been working on",
age:1
}
});
function changeBio(newBio) {
setState(current => ({
...current,
profile: {
...current.profile,
bio: newBio
}
}));
}
useStateをuseImmer Hookに置き換えることで、
Hooksの例をさらにシンプルにすることができます。
また、コンポーネントの状態を変異させることで、
Reactコンポーネントを更新することができます。
import { useImmer } from 'use-immer';
const [state, setState] = useImmer({
id: 14,
email: "stewie@familyguy.com",
profile: {
name: "Stewie Griffin",
bio: "You know, the... the novel you've been working on",
age:1
}
});
function changeBio(newBio) {
setState(draft => {
draft.profile.bio = newBio;
});
}
また、Immerを使って、配列やセットを不変的なオブジェクトに変換することもできます。
Immerで作成したマップ、セットは変異したときにエラーを投げるので、開発者は変異の間違いに気づくことができます。
最も重要なことは、ImmerがReactに限定されないことです。普通のJavaScriptでもImmerを簡単に使うことができます。
Immutating以外にも、Immerはコードベースの複雑さを軽減することで、よく書かれた読みやすいコードベースを維持するのに役立ちます。
最終的な感想
Immerを使った経験から、
Reactと一緒に使うには最適なオプションだと思います。
コードを簡素化し、デザインによって不変性を管理するのに役立ちます。