第140章:練習:useState を useImmer に置き換えてみる
「配列やオブジェクトの state を更新するとき、...(スプレッド)だらけで頭こんがらがる…😵💫」
そんな子を一気にラクにしてくれるのが useImmer でしたね ✨
この章では、
「ふつうの
useStateで書いたコンポーネント」を 👉 そのままuseImmer版に書き換える
という、実戦っぽい練習をやっていきます 💪
🎯 この章のゴール
useStateで書いたコードを どこをどう直せばuseImmerになるか分かるdraftを使った書き換えスタイルに慣れる- 「ここは
useStateのまま」「ここはuseImmerにしたい」を自分で選べるようになる
1️⃣ まずはベースとなる useState 版 TODO リスト 📝
まず、よくある TODO リストを
「全部 useState だけで書いたバージョン」で用意します。
✅ 追加 ✅ 完了フラグの ON/OFF ✅ 削除
ができるシンプルなやつです。
※ すでに似た TODO を作っている場合は、 「自分のコードを
useImmerに置き換える練習」として見てください 💡
型定義&useState 版コンポーネント
// Todo の型
type Todo = {
id: number;
title: string;
done: boolean;
};
import { useState, type ChangeEvent } from "react";
const initialTodos: Todo[] = [
{ id: 1, title: "React の勉強をする", done: false },
{ id: 2, title: "ゼミのレポートを書く", done: true },
];
export function TodoListUseState() {
const [todos, setTodos] = useState<Todo[]>(initialTodos);
const [text, setText] = useState<string>("");
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
const handleAdd = () => {
const trimmed = text.trim();
if (!trimmed) return;
setTodos((prev) => [
...prev,
{
id: Date.now(),
title: trimmed,
done: false,
},
]);
setText("");
};
const handleToggle = (id: number) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
const handleDelete = (id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
return (
<div>
<h2>TODO リスト(useState 版)</h2>
<div>
<input value={text} onChange={handleChange} placeholder="やることを書く" />
<button onClick={handleAdd}>追加</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => handleToggle(todo.id)}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.title}
</span>
</label>
<button onClick={() => handleDelete(todo.id)}>削除</button>
</li>
))}
</ul>
</div>
);
}
ここでの「つらみ」ポイント 😢
handleAddでsetTodos(prev => [...prev, 新しいTodo])handleToggleでprev.map(...)&{ ...todo, done: !todo.done }handleDeleteでprev.filter(...)
全部 「新しい配列を自分で作る」 必要があって、 慣れないとちょっと読みづらいですよね 🌀
2️⃣ useState の更新の流れを図でイメージしてみる 🧠
useState のときのイメージを、軽く図解しておきます。
useImmer は、この「新しい配列を作る」部分を
代わりにやってくれる相棒でしたね 🧙♀️✨
3️⃣ useImmer 版に書き換えるためのステップ 👣
では、本題!
この TodoListUseState を useImmer 版に書き換えてみましょう。
やることは大きく 3 つだけ👇
useImmerをインポートするuseStateをuseImmerに変える- 更新関数の中身を
draft書き換えスタイルに変える
🪄 ステップ 1:useImmer をインポート
前の章で use-immer を入れている前提ですが、
もしまだなら一応コマンドはこれです 👇
npm install use-immer
コンポーネントで useImmer をインポートします。
import { useImmer } from "use-immer";
🪄 ステップ 2:useState を useImmer に置き換える
この行を:
const [todos, setTodos] = useState<Todo[]>(initialTodos);
こう変えます👇
const [todos, updateTodos] = useImmer<Todo[]>(initialTodos);
ポイント 💡
setTodosの名前を、updateTodosとかsetTodosDraftみたいに 「draft を更新するよ」感のある名前にすると読みやすいです- ジェネリクス
<Todo[]>は、initialTodosから推論もされるので 実は省略しても OK(慣れるまでは付けておくのもアリ)
🪄 ステップ 3:更新ロジックを draft スタイルに変える
ここが今日のメイン練習です ✍️
setTodos(prev => ...) になっているところを、
順番に updateTodos(draft => { ... }) に変えていきます。
3-1. 追加処理:push で OK に 🎉
もともと:
const handleAdd = () => {
const trimmed = text.trim();
if (!trimmed) return;
setTodos((prev) => [
...prev,
{
id: Date.now(),
title: trimmed,
done: false,
},
]);
setText("");
};
👉 useImmer 版:
const handleAdd = () => {
const trimmed = text.trim();
if (!trimmed) return;
updateTodos((draft) => {
draft.push({
id: Date.now(),
title: trimmed,
done: false,
});
});
setText("");
};
✨ ここでのポイント
draftは 「下書き用の配列」 だと思ってくださいdraft.push(...)のように、普通の配列みたいに書き換えて OK- Immer が裏で「新しい配列」をいい感じに作ってくれるので、 イミュータブル(元のを直接変えない)ルールはちゃんと守られてます ✅
3-2. 完了フラグの ON/OFF:map → find でスッキリ ✨
もともと:
const handleToggle = (id: number) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
};
👉 useImmer 版:
const handleToggle = (id: number) => {
updateTodos((draft) => {
const todo = draft.find((t) => t.id === id);
if (!todo) return;
// ここは「直接代入」して OK!(draft だから)
todo.done = !todo.done;
});
};
ここ、めちゃくちゃスッキリしませんか?🥹
- もう
{ ...todo, done: !todo.done }みたいに オブジェクトをコピーしなくていい - 「対象を探して」「そのまま書き換える」という 素直なストーリーのコードになります 💕
3-3. 削除:filter → splice にしてもいいし、filter のままでも OK
もともと:
const handleDelete = (id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
};
useImmer だと、2 パターン書き方があります。
① そのまま filter を返すパターン(少し上級)
updateTodos では、draft を返しても OK なので、
こんな書き方もできます 👇
const handleDelete = (id: number) => {
updateTodos((draft) => draft.filter((todo) => todo.id !== id));
};
「
draftを元に、新しい配列を return してもいいよ」という仕様です。
② splice で「その場で消す」パターン(初心者さん向け)
こっちの方が、「配列を直接いじってる感」が分かりやすいかもです。
const handleDelete = (id: number) => {
updateTodos((draft) => {
const index = draft.findIndex((t) => t.id === id);
if (index === -1) return;
draft.splice(index, 1);
});
};
👉 どっちでも OK なので、 自分が読みやすい方を選んでください ✨
4️⃣ フルコード:useImmer 版 TODO リスト 🎉
ここまでの変更を全部まとめると、こうなります。
import { useState, type ChangeEvent } from "react";
import { useImmer } from "use-immer";
type Todo = {
id: number;
title: string;
done: boolean;
};
const initialTodos: Todo[] = [
{ id: 1, title: "React の勉強をする", done: false },
{ id: 2, title: "ゼミのレポートを書く", done: true },
];
export function TodoListUseImmer() {
const [todos, updateTodos] = useImmer<Todo[]>(initialTodos);
const [text, setText] = useState<string>("");
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
const handleAdd = () => {
const trimmed = text.trim();
if (!trimmed) return;
updateTodos((draft) => {
draft.push({
id: Date.now(),
title: trimmed,
done: false,
});
});
setText("");
};
const handleToggle = (id: number) => {
updateTodos((draft) => {
const todo = draft.find((t) => t.id === id);
if (!todo) return;
todo.done = !todo.done;
});
};
const handleDelete = (id: number) => {
updateTodos((draft) => {
const index = draft.findIndex((t) => t.id === id);
if (index === -1) return;
draft.splice(index, 1);
});
};
return (
<div>
<h2>TODO リスト(useImmer 版)</h2>
<div>
<input value={text} onChange={handleChange} placeholder="やることを書く" />
<button onClick={handleAdd}>追加</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => handleToggle(todo.id)}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.title}
</span>
</label>
<button onClick={() => handleDelete(todo.id)}>削除</button>
</li>
))}
</ul>
</div>
);
}
5️⃣ useImmer のイメージ図 🧠💭
useImmer の流れも、図でイメージを固めておきましょう。
💡 大事な点
-
todos自体は絶対に直接書き換えない- ❌
todos.push(...) - ✅
updateTodos(draft => { draft.push(...) })
- ❌
-
draftの中でだけ「ミュータブルっぽく」書いて OK → 外から見るとちゃんとイミュータブル 🎀
6️⃣ ミニ練習ミッション 🚀
最後に、自分の手で書き換えてみる用ミッションを置いておきます ✍️
ミッション1:自分の useState コンポーネントを変えてみる
-
これまでの章で作った
- フォーム
- TODO
- カウンター(オブジェクト版)
など、配列 or オブジェクトを
useStateで持っているコンポーネントを1つ選ぶ
-
その state を
useImmerに置き換えてみる -
setXXXを全部updateXXX(draft => { ... })スタイルに書き換える
✅ ゴール:
draftを見るだけで「中身がイメージできる」ようになること
ミッション2:ネストしたオブジェクトで挑戦してみる 🧩
例えばこんな state を想像してみてください:
type Profile = {
name: string;
favorites: {
food: string;
color: string;
};
};
-
favorites.colorだけ変えたいとき、useStateだと{ ...prev, favorites: { ...prev.favorites, color: "pink" } }useImmerだとdraft.favorites.color = "pink";で OK 🎀
👉 実際にコンポーネントを作って、
useState 版 → useImmer 版 に変えてみてください 💪
ミッション3:どこまで useImmer にするか考える 🧠
- すべての state を
useImmerにする必要はありません ✋ - 配列やオブジェクトがややこしくなってきたら
useImmerを検討する くらいの気持ちで OK です。
自分の中で、
- 「これは単なる数値だから
useStateでいいや」 - 「これは配列だし、しょっちゅう更新するから
useImmerかな」
という ゆるいルール を決めてみてください 🧸
まとめ 🌈
-
useState→useImmerの変換は、基本的にuseStateの行をuseImmerに変える- 更新部分を
draft書き換えスタイルに変える だけで OK ✅
-
draftの中ではpushsplice- プロパティへの代入
=など、ふつうの JS の書き方で state を更新できる 🎉
-
でも
todos本体は絶対に直接書き換えない、これだけは守る!
次の React コーディングで、 「うわ、スプレッドだらけでしんど…😇」となったら、
✨「ここ、
useImmerに変えてみよっかな?」
ってぜひ思い出してあげてください 🪄💕