第139章:useImmer の紹介
データ更新をめっちゃ楽にする魔法🪄
1️⃣ この章でできるようになること
この章では、こんなことができるようになるのがゴールです✨
- 「イミュータブル(元のデータを直接いじらない)」のつらさを再確認する
- そのつらさを一気に解決してくれるライブラリ Immer を知る
- React 用のカスタムフック
useImmerの使い方を覚える useStateでは大変だった ネストしたオブジェクトや配列 を、サクッと更新できるようになる
React公式ドキュメントでも、オブジェクトや配列の更新がつらくなってきたら Immer を使うと楽になるよ〜という話が出てきます。(React)
2️⃣ おさらい:「イミュータブルしんどい問題」😇
React では、state は直接書き換えちゃダメ でしたよね。
state.user.name = '...';❌setState({ ...state, user: { ...state.user, name: '...' } })✅
例えば、ユーザー情報をこんなふうに持っていたとします:
type User = {
name: string;
profile: {
age: number;
city: string;
};
};
const [user, setUser] = useState<User>({
name: 'さくら',
profile: {
age: 20,
city: 'Tokyo',
},
});
city だけ変えたいとき、イミュータブルを守ろうとすると…
function handleChangeCity() {
setUser({
...user,
profile: {
...user.profile,
city: 'Osaka',
},
});
}
🌀 スプレッド(...)だらけで、ネストが増えるほど
「今どの階層コピーしてるんだっけ…?」ってなりがちです。
React docs でも、「オブジェクトや配列を更新するときはコピーして新しい値を渡してね」と説明されていますが、ネストが深いとどうしてもコードが長くなります。(React)
そこで登場するのが Immer & useImmer です💪
3️⃣ Immer & useImmer ってなに?🧊
🧊 Immer のざっくりイメージ
Immer は、
「ドラフト(下書き) に対しては自由にミュート(直接書き換え)していいよ。 最後に きれいなイミュータブルな新データ を作って返しておくからね〜」
という魔法のようなライブラリです🪄(tuckerblackwell.com)
React の公式ドキュメントでも、イミュータブルな更新を楽にするための方法の1つとして Immer が紹介されています。(React)
🪄 useImmer とは?
useImmer は、Immer チームが作っている React 用のカスタムフック です。
React 本体の機能ではなく、use-immer という外部ライブラリ から提供されます。(GitHub)
見た目はほぼ useState と同じなんですが、
更新関数に「ドラフト」を渡してくれる のがポイントです。
4️⃣ ざっくり仕組みイメージ(Mermaid図)🧠
useImmer が裏側で何をしているか、イメージ図で見てみましょう👇
- あなたは
draftを「普通にミュート」している感覚 で書ける - でも実際には Immer が、裏でちゃんとイミュータブルな新 state を作ってくれる
という流れです🌟
5️⃣ セットアップ:use-immer を入れよう 💿
ターミナルで、プロジェクトのフォルダに移動してから👇
npm install use-immer
これで useImmer フックが使えるようになります。(GitHub)
使うときは、コンポーネントのファイルでこう書きます:
import { useImmer } from 'use-immer';
6️⃣ useImmer の基本形を見てみよう ✨
まずは、シンプルなオブジェクトの例から。
🎀 例1:プロフィールを useImmer で管理
🔹 Before:useState 版(おさらい)
type Profile = {
name: string;
age: number;
city: string;
};
const [profile, setProfile] = useState<Profile>({
name: 'さくら',
age: 20,
city: 'Tokyo',
});
function handleBirthday() {
setProfile({
...profile,
age: profile.age + 1,
});
}
🔹 After:useImmer 版(ドラフトに直接書く🎉)
import { useImmer } from 'use-immer';
type Profile = {
name: string;
age: number;
city: string;
};
const [profile, updateProfile] = useImmer<Profile>({
name: 'さくら',
age: 20,
city: 'Tokyo',
});
function handleBirthday() {
updateProfile((draft) => {
draft.age = draft.age + 1;
});
}
function moveToOsaka() {
updateProfile((draft) => {
draft.city = 'Osaka';
});
}
☝ ポイント:
-
useImmer<Profile>(初期値)→useState<Profile>とほぼ同じ感覚で使える -
更新関数
updateProfileに 関数を渡す と、- その関数の引数
draftが「ドラフトオブジェクト」 draft.age++みたいに直接変更して OK
- その関数の引数
-
Immer が裏で「イミュータブルな新しい
profile」を作ってくれる
7️⃣ 配列の例:TODOリストを useImmer 化してみる ✅
次は、配列を扱うときのよくあるパターンです。
🔹 型定義
type Todo = {
id: number;
title: string;
done: boolean;
};
🔹 useImmer で TODO 配列を管理する
import { useImmer } from 'use-immer';
type Todo = {
id: number;
title: string;
done: boolean;
};
export function TodoListUseImmer() {
const [todos, updateTodos] = useImmer<Todo[]>([
{ id: 1, title: 'React を学ぶ', done: true },
{ id: 2, title: 'useImmer を試す', done: false },
]);
function handleAdd() {
updateTodos((draft) => {
const nextId =
draft.length === 0 ? 1 : draft[draft.length - 1].id + 1;
draft.push({
id: nextId,
title: `新しいタスク ${nextId}`,
done: false,
});
});
}
function handleToggle(id: number) {
updateTodos((draft) => {
const todo = draft.find((t) => t.id === id);
if (todo == null) {
return;
}
todo.done = !todo.done;
});
}
return (
<div>
<h2>useImmer TODO 📝</h2>
<button onClick={handleAdd}>追加</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => handleToggle(todo.id)}
/>
{todo.title}
</label>
</li>
))}
</ul>
</div>
);
}
ここでのポイントはこんな感じ👇
draft.push(...)して OK(普通の配列みたいに書ける)draft.find(...)で見つけたtodo.doneを直接反転して OK- でも実際は Immer が ちゃんと新しい配列を作って くれている
「...todos とか map とか filter を駆使して新配列を組み立てる」のに比べると、
だいぶメンタルが楽になります😌(Zenn)
8️⃣ 型の話:useImmer<StateType> の考え方 👓
useImmer もジェネリクスで型を指定できます。
const [profile, updateProfile] = useImmer<Profile>(/* 初期値 */);
const [todos, updateTodos] = useImmer<Todo[]>(/* 初期配列 */);
💡 型の付け方のコツ
-
オブジェクトや配列を管理するときは
useImmer<型>を素直に書く- そのほうが VS Code が型補完をガッツリ出してくれて安心
-
逆に単純な
numberやstringだけなら、 わざわざuseImmerを使わずuseStateのままで OK
Stack Overflow などでも、useImmer は「オブジェクトや配列を扱うときに便利なツールであって、必須ではないよ」という位置づけで説明されています。(Stack Overflow)
9️⃣ useImmer を使うとき・使わないときチェックリスト ✅❌
✅ 使うとハッピーなとき
-
state が ネストしたオブジェクト or 配列 で
...の嵐になって頭がこんがらがるとき
-
TODOリスト・フォーム入力・設定画面など
- 「ちょっと深めの階層」をよくいじる UI
-
「型もちゃんと守りながら楽に書きたい」とき
❌ 無理に使わなくていいとき
-
countみたいに、ただの数字や文字列 を ちょっと更新するだけ- →
useState<number>で十分
- →
-
ほんの少しのデータで、ネストも浅いとき
- → まずは素の
useStateで書いてみて、 「つらくなってきたらuseImmerに乗り換える」でOK
- → まずは素の
🔟 ミニ練習タイム ✍️(手を動かしてみよう)
実際に useImmer を使ってみる練習問題です。
答えはこの章では書かないので、VSCodeで手を動かしながら 試してみてください😉
📝 練習1:プロフィールカードを useImmer 化する
-
ProfileCard.tsxというコンポーネントを作る -
Profile型はこんな感じで定義:type Profile = {
name: string;
profile: {
age: number;
city: string;
hobby: string;
};
}; -
useImmer<Profile>で state を持つ -
ボタンを3つ作る
- 「1歳年をとる」(
ageを +1) - 「都市を Osaka に変える」(
cityを書き換え) - 「趣味を '旅行' に変える」
- 「1歳年をとる」(
-
それぞれのボタンの中では、
draftに直接代入して更新 してみる
✅ 練習2:TODO + タグ機能を useImmer で
-
TodoWithTag.tsxを作る -
Todo型を少しリッチにする:type Todo = {
id: number;
title: string;
done: boolean;
tags: string[];
}; -
やりたいこと:
-
「タグを追加」ボタン
- 対象の TODO の
tags配列に、新しいタグをpush
- 対象の TODO の
-
「完了/未完了」トグルボタン
doneをtrue/false切り替え
-
-
すべて
updateTodos((draft) => { ... })の中でdraft[index].tags.push('React')draft[index].done = !draft[index].doneみたいに書いてみる
🎯 まとめ:この章で押さえておきたいこと
-
Immer は「ドラフトに自由に書いていいよ、最終的なイミュータブルはこっちで作るね」っていうライブラリ🧊
-
useImmerはその React 版カスタムフックで、useStateと同じ顔をしているけど- 更新時に
draftを渡してくれるのがポイント
-
ネストしたオブジェクト・配列の更新が スッキリ & ミスりにくくなる ✨
-
ただし、なんでもかんでも
useImmerにする必要はなくて、- シンプルな値は
useState - 複雑・ネスト深めなら
useImmerくらいの感じで使い分けるとバランス良い👌
- シンプルな値は
次の 第140章 では、
実際に今まで useState で書いていたコードを useImmer に書き換えてみる実践編 に入っていきます💻🔥