第27章:並行状態(Parallel)で同時進行を扱う🧵🧵
(フォーム送信 × トースト表示を“並行”で作ってみるよ〜!💖)
1) 並行状態(Parallel)ってなに?🤔🧵

並行状態は、1つの親状態の中に「複数の領域(リージョン)」があって、全部が同時にアクティブになる状態のことだよ✨ しかも、親が並行状態のときは イベントが各リージョンに同時に届く(=各リージョンがそれぞれ反応できる)というのが大きな特徴!📣✨ (Stately)
イメージはこんな感じ👇
- 「送信の状態」🛰️(idle / submitting / success / error)
- 「トースト表示の状態」🍞(hidden / showing) この2つが、同時に走るのが並行状態🧵🧵
そして、並行状態の “いまの状態” は、だいたい オブジェクト(辞書)で表現されるよ(例:{ track: 'paused', volume: 'normal' } みたいに)✨ (Stately)
2) 何がうれしいの?🎁✨(if地獄を回避😵💫)
並行にしたいのに、1本の状態に押し込むと…👇
- 「送信中 & トースト表示中」
- 「送信成功 & トースト非表示」 みたいに 組み合わせが増えすぎるのがあるある😭💦
並行状態は、“別々に考えられるものは別々に持つ” から、仕様がスッキリするよ〜✨ (Stately)
3) 超重要:これ、ガチのマルチスレッドじゃないよ⚠️🧠
並行状態は **「論理的に同時」**ってだけで、だいたいの実装は 順番に評価されるよ🧘♀️ だから、スレッドの競合とかロック地獄を持ち込まずに「同時っぽさ」をモデリングできるのが強み✨ (The Complete Guide to State Machines)
4) 使いどころ✨ / 使わないほうがいいところ🙅♀️
使いどころ🧵🧵
- 関連はあるけど独立して進むもの 例:フォームの「送信」と、UIの「トースト表示」 (Stately)
- 「状態が増えすぎ問題」を止めたいとき🛑
使わないほうがいいとき🙅♀️
- 2つがガッチリ依存してて、ほぼ常に同時に変わる(=分けてもメリット薄)
- “完全に別物”で連携いらない → 並行より「別マシン」でもOK(XStateなら
invokeを使う判断もあり) (Stately)
5) 設計のコツ:リージョン分割の3ステップ🪄✨
Step 1:軸を見つける🔍
「これ、同じ状態の話?別の軸?」って考えるよ🧠
- 送信:成功/失敗/送信中
- トースト:出てる/消えてる → うん、別軸!並行にできそう🧵🧵✨
Step 2:リージョンごとに “小さな状態機械” を作る🧩
それぞれが独立して読めるくらいが理想👍
Step 3:連携は “イベント共有” でやる📣
リージョンAからリージョンBへ「直接ジャンプ」させるのは避けがち(依存が強くなるから) → 代わりに「同じイベントを両方が受けて、それぞれ動く」がキレイ✨ (Stately)
6) ハンズオン題材:フォーム送信 × トースト表示📨🍞✨
リージョンA:submission(送信)
- editing → submitting → success / error
リージョンB:toast(トースト)
- hidden ↔ showing
イベント案📣
- SUBMIT(送信押した)
- RESOLVE(成功した)
- REJECT(失敗した)
- TOAST_TIMEOUT(トースト消す)
ポイント:RESOLVE / REJECT を両リージョンで処理するよ✨
- submission:成功/失敗へ遷移
- toast:表示へ遷移🍞✨ (イベントは並行状態の全リージョンが受け取れるイメージ!) (Stately)
7) 実装A:学習用ミニ「並行マシン」TypeScript(ライブラリなし)🧪✨
考え方だけ掴む用だよ〜!(まずはここが超おすすめ💖)
type Effect =
| { type: "log"; message: string }
| { type: "timeout"; ms: number; event: AppEvent };
type SubmissionState = "editing" | "submitting" | "success" | "error";
type ToastState = "hidden" | "showing";
type AppState = {
submission: SubmissionState;
toast: ToastState;
// ついでに表示メッセージも持たせる(Contextっぽい役)
toastMessage: string | null;
errorMessage: string | null;
};
type AppEvent =
| { type: "SUBMIT" }
| { type: "RESOLVE" }
| { type: "REJECT"; message: string }
| { type: "TOAST_TIMEOUT" };
type RegionResult<S> = { state: S; patch?: Partial<AppState>; effects?: Effect[] };
function submissionRegion(state: AppState, event: AppEvent): RegionResult<SubmissionState> | null {
switch (state.submission) {
case "editing":
if (event.type === "SUBMIT") {
return {
state: "submitting",
effects: [{ type: "log", message: "送信開始!" }],
};
}
return null;
case "submitting":
if (event.type === "RESOLVE") return { state: "success" };
if (event.type === "REJECT") return { state: "error", patch: { errorMessage: event.message } };
return null;
default:
return null;
}
}
function toastRegion(state: AppState, event: AppEvent): RegionResult<ToastState> | null {
// 成功/失敗イベントを受けたらトースト出す(リージョン連携はイベントで!)
if (event.type === "RESOLVE") {
return {
state: "showing",
patch: { toastMessage: "送信できたよ〜!🎉", errorMessage: null },
effects: [{ type: "timeout", ms: 2000, event: { type: "TOAST_TIMEOUT" } }],
};
}
if (event.type === "REJECT") {
return {
state: "showing",
patch: { toastMessage: `失敗…😢 ${event.message}` },
effects: [{ type: "timeout", ms: 2500, event: { type: "TOAST_TIMEOUT" } }],
};
}
if (state.toast === "showing" && event.type === "TOAST_TIMEOUT") {
return { state: "hidden", patch: { toastMessage: null } };
}
return null;
}
// “並行”のキモ:同じイベントを両リージョンに流す
function step(state: AppState, event: AppEvent): { next: AppState; effects: Effect[] } {
const effects: Effect[] = [];
const sub = submissionRegion(state, event);
const t = toastRegion(state, event);
const next: AppState = {
...state,
...(sub?.patch ?? {}),
...(t?.patch ?? {}),
submission: sub?.state ?? state.submission,
toast: t?.state ?? state.toast,
};
if (sub?.effects) effects.push(...sub.effects);
if (t?.effects) effects.push(...t.effects);
return { next, effects };
}
// 初期状態
let s: AppState = {
submission: "editing",
toast: "hidden",
toastMessage: null,
errorMessage: null,
};
// 例:送信→失敗→(トースト出る)
({ next: s } = step(s, { type: "SUBMIT" }));
({ next: s } = step(s, { type: "REJECT", message: "ネットワークが不安定" }));
console.log(s);
ここでの学び🧠✨
- “状態は2本立て”でもOK(
submissionとtoastを同時に持つ) - “連携はイベントで”(REJECT を両方が処理)📣
8) 実装B:XStateの並行状態(Parallel)で書く🧵🧵✨
まず最新動向だけ一言:XStateは v5 ドキュメントが現行で、v4はメンテ対象外扱いだよ〜(公式側で明言)🧠 (Stately) (TypeScriptは公式DLページ上は “現在 5.9” が最新として案内されてるよ) (typescriptlang.org)
XState v5:parallel の形(超基本)
import { createMachine, createActor, assign } from "xstate";
type Ctx = {
toastMessage: string | null;
};
type Ev =
| { type: "SUBMIT" }
| { type: "RESOLVE" }
| { type: "REJECT"; message: string }
| { type: "TOAST_TIMEOUT" };
const machine = createMachine({
id: "form",
type: "parallel",
context: { toastMessage: null } as Ctx,
states: {
submission: {
initial: "editing",
states: {
editing: {
on: { SUBMIT: "submitting" },
},
submitting: {
on: {
RESOLVE: "success",
REJECT: "error",
},
},
success: {},
error: {},
},
},
toast: {
initial: "hidden",
states: {
hidden: {
on: {
RESOLVE: {
target: "showing",
actions: assign({
toastMessage: () => "送信できたよ〜!🎉",
}),
},
REJECT: {
target: "showing",
actions: assign({
toastMessage: ({ event }) => `失敗…😢 ${event.message}`,
}),
},
},
},
showing: {
on: {
TOAST_TIMEOUT: {
target: "hidden",
actions: assign({ toastMessage: () => null }),
},
},
},
},
},
},
});
const actor = createActor(machine);
actor.start();
actor.send({ type: "SUBMIT" });
actor.send({ type: "REJECT", message: "ネットワークが不安定" });
console.log(actor.getSnapshot().value); // { submission: 'error', toast: 'showing' } みたいになる
console.log(actor.getSnapshot().context); // toastMessage など
この書き方のポイント✨
type: 'parallel'を付けるとリージョンが並行になるよ 🧵🧵 (Stately)- イベントは各リージョンに届くので、RESOLVE / REJECT を両方で処理できる 📣✨ (Stately)
- 状態値はオブジェクトになる(
{ submission: ..., toast: ... }) (Stately)
9) 「全部終わったら次へ」もできるよ✅(onDone)🏁
並行状態の各リージョンが final になったら、親の onDone が動くよ〜✨ (Stately)
(例:初期化で “DB準備” と “設定ロード” を同時進行して、両方終わったら Ready へ!みたいなやつ🧵🧵)
10) よくある落とし穴😵💫💥(ここ超大事)
-
リージョン間を直接遷移させたくなる問題 → 依存が強くなって壊れやすい🥲 → “同じイベントを両方で処理”に寄せるのがおすすめ✨ (Stately)
-
並行=マルチスレッドだと思い込む → だいたいは順番に評価される「論理並行」だよ🧘♀️ (The Complete Guide to State Machines)
-
イベントが多すぎてカオス → イベント名を「誰のためのイベント?」で整理しよう(ユーザー操作/システム結果/タイマー)📣
11) ミニ課題🎓✨(手を動かすと一気に理解できるよ!)
課題A:リージョンをもう1本増やす🧵
network: online | offlineを追加- offline のとき SUBMIT したら、submission は submitting に行かず error にする(ガードっぽく)🛡️
課題B:トーストに “種類” を付ける🍞
- success / error で絵文字を変える(🎉 / 🚨)✨
12) AI活用プロンプト集🤖✨(そのままコピペOK)
- 「この仕様、並行に分けるなら “軸” は何?リージョン案を3つ出して🥺🧵」
- 「RESOLVE/REJECT を受けたとき、各リージョンがやるべきことを表にして📋✨」
- 「リージョン間の依存が強すぎないかレビューして。依存を減らすイベント設計も提案して🙏」 (Stately)
- 「TypeScriptで、リージョンごとの reducer を使った並行マシンの最小実装を作って(テストしやすく)🧪」
まとめ✅✨(チェックリスト付き)
- 並行状態=複数リージョンが同時にアクティブ 🧵🧵 (Stately)
- イベントは各リージョンに届くので、同じイベントで連携できる📣✨ (Stately)
- “同時”は 論理的(たいてい順番評価)🧘♀️ (The Complete Guide to State Machines)
- コツは「別軸は別リージョン」「連携はイベント」「直接遷移は避けがち」✨ (Stately)
次の章(第28章)は「エラーを状態にする」🚨で、さらに実務っぽく強くなるよ〜!💪💖