第70章:練習:カウンターアプリを useReducer で作り直す (Actionの型もちゃんと定義して!)
「型のついた State + Action + reducer」の流れを、手を動かしながら体に覚えさせちゃいましょう 💪
1️⃣ この章のゴール
この章が終わるころには…
useStateで作ったカウンターを 👉useReducerで書き直せるようになるStateの型とActionの型を 👉 自分で定義できるようになるdispatch({ type: "..." })の流れが 👉 なんとなくじゃなくて「ちゃんと分かってる!」状態になる
という感じを目指します 🎯
2️⃣ ファイルを用意しよう ✍️
プロジェクトはすでに Vite+React+TS でできている前提で進めます。
srcフォルダの中にCounterWithReducer.tsxというファイルを作る- いったん、最小構成だけ書いておきます。
// src/CounterWithReducer.tsx
import { useReducer } from "react";
export function CounterWithReducer() {
return (
<div>
<h2>useReducer カウンター</h2>
<p>ここにカウンターを作るよ!</p>
</div>
);
}
次に、App.tsx から呼び出せるようにします。
// src/App.tsx
import { CounterWithReducer } from "./CounterWithReducer";
export function App() {
return (
<div>
<h1>第70章:useReducer カウンター 💫</h1>
<CounterWithReducer />
</div>
);
}
npm run dev をして、ブラウザに
「useReducer カウンター」と表示されていれば準備OKです ✅
3️⃣ State と Action の「型」を決める 🧩
この章の主役は State と Action の型づけです!
まずは「カウンターがどんな状態を持っているか?」を言葉で整理してみましょう。
- 今回の State はとってもシンプル 👉 「数字がひとつ」だけ
なので、CounterState の型はこんな感じになります。
type CounterState = {
count: number;
};
次に、どんな「操作(Action)」ができるかを決めます。
今回はこの3つにしましょう ✨
- 1 増やす(
increment) - 1 減らす(
decrement) - 0 にリセットする(
reset)
それを型にすると、こうなります👇
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
ポイント 💡
typeプロパティが「どのアクションか」を表す「ラベル」|でつないで「どれか1つ」という形にしている(ユニオン型)
この2つを CounterWithReducer の上あたりに書きます。
// src/CounterWithReducer.tsx
import { useReducer } from "react";
type CounterState = {
count: number;
};
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
export function CounterWithReducer() {
// ここにあとで useReducer を書くよ!
return (
<div>
<h2>useReducer カウンター</h2>
<p>ここにカウンターを作るよ!</p>
</div>
);
}
4️⃣ reducer 関数を書く 🔁
次は、State と Action を受け取って
「次の State を返す」だけの関数 を作ります。
イメージはこんな感じ 👇
「今の状態」と「指示書(Action)」を渡すと、 「じゃあ新しい状態はこうだね!」って返してくれる係。
コードで書くとこうなります。
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "increment":
return {
count: state.count + 1,
};
case "decrement":
return {
count: state.count - 1,
};
case "reset":
return {
count: 0,
};
default:
// ここには基本的に来ないはずだけど、型的に必要な保険
return state;
}
}
ここで大事なポイント ✨
- 必ず新しいオブジェクトを返す
state.count++みたいに、元のstateを直接いじらない! switch (action.type)の中で 「どの Action ならどう変えるか」 を全部書く
この counterReducer 関数も、さっきの型定義のすぐ下に書いておきます。
5️⃣ useReducer でコンポーネント完成させる 🌟
いよいよ useReducer の出番です!
- 最初の状態(初期値)を用意
useReducerでstateとdispatchを受け取る- ボタンから
dispatch({ type: "..." })を呼ぶ
という流れになります。
完成形のコンポーネントをいったんドン!と置きますね 💥
// src/CounterWithReducer.tsx
import { useReducer } from "react";
type CounterState = {
count: number;
};
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "increment":
return {
count: state.count + 1,
};
case "decrement":
return {
count: state.count - 1,
};
case "reset":
return {
count: 0,
};
default:
return state;
}
}
const initialState: CounterState = {
count: 0,
};
export function CounterWithReducer() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
<div style={{ border: "1px solid #ccc", padding: "16px", borderRadius: "8px" }}>
<h2>useReducer カウンター 🧮</h2>
<p style={{ fontSize: "24px", fontWeight: "bold" }}>{state.count}</p>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={() => dispatch({ type: "decrement" })}>−1</button>
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "reset" })}>リセット</button>
</div>
</div>
);
}
コードの中身を分解してみる 🪄
✅ initialState
const initialState: CounterState = {
count: 0,
};
- 最初の状態をひとまとめにしておく
- 型はちゃんと
CounterStateと一致させる
✅ useReducer のところ
const [state, dispatch] = useReducer(counterReducer, initialState);
state👉 今の状態({ count: number })dispatch👉 「この Action を実行して〜」とお願いする関数
✅ ボタンから Action を飛ばす
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
onClickの中でdispatchを呼んでいる- 渡しているのは
CounterActionのどれか1つ (type: "increment"だから OK ✅)
VS Code で dispatch にカーソルを当てると、
dispatch(action: CounterAction): void みたいな型が出てくるはずです 👀
「Action の型ちゃんと効いてるんだ〜」と確認してみてください ✨
6️⃣ データの流れを図でイメージしよう 🎨
useReducer の流れは、ざっくりいうとこんな感じです。
- ボタンをクリック
dispatch({ type: "..." })が呼ばれるcounterReducer(state, action)が呼ばれる- 新しい
stateが返ってくる - React が画面を描き直す
Mermaid で図にすると、こう 👇
「イベント → dispatch → reducer → 新しい state → 再描画」
この流れをなんとなく頭に置いておくと、
もう useReducer は怖くなくなります 🥰
7️⃣ ちょこっとアレンジ練習 ✏️💕
ここからは自分の手でコードを変えてみるミッションです!
🔁 練習1:2ずつ増えるボタンを追加
やってみてほしいこと:
- Action に
type: "add2"を追加する counterReducerに"add2"のケースを追加してcount: state.count + 2にする- ボタンを1つ増やして、
dispatch({ type: "add2" })を呼ぶ
👉 ヒント:Action の型はこんな感じで増やせます。
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" }
| { type: "add2" };
🌈 練習2:マイナスにならないようにしてみる
count が 0 以下にならないようにしてみましょう。
"decrement"のところでstate.count - 1が 0 未満になりそうなら、0を返すようにしてみる
8️⃣ まとめ:useReducer の「型」が守ってくれる 🛡️
この章でやったことを整理すると…
Stateの型(CounterState)を定義したActionの型(CounterAction)を ラベル付きのユニオン型として定義したcounterReducer(state, action)の中でswitch (action.type)で分岐して、新しい state を返したuseReducer+dispatchで 「指示書(Action)」を投げて状態を変えるスタイル に慣れた
useState よりちょっと手順は増えますが、そのぶん
- 状態の変化が一本の道にまとまる
- Action の型で「ありえない操作」を防げる
- アプリが大きくなっても整理しやすい
というメリットがあります ✨
次にもっと複雑な画面を作るときも、 このカウンターのコードをテンプレートとしてコピペして ちょっとずつ書き換えていけば OK です 🥳
おつかれさまでした〜!💐
useReducer、だいぶ仲良くなれたはずです 💘