リアクティブプログラミング
データの変化を自動的に伝播させ、非同期データストリームを宣言的に処理するパラダイム
リアクティブプログラミングとは
リアクティブプログラミングは、データの変化を自動的に伝播させ、非同期データストリームを宣言的に処理するパラダイムである。RxJS、React の状態管理、Kotlin Flow が代表例。
扱う単位が「1 回の呼び出しで返る値」ではなく「時間軸に並んだ値の並び」になる点が要である。受け手は、値が届いたとき・並びが終わったとき・失敗したときの処理を先に登録しておき、送り手が届いた順に押し出す。失敗の通知が流れた時点でその並びは終わり、以降の値は届かない。この終わり方の扱いが、後述する落とし穴の大半を生む。
名前の似た「リアクティブシステム」とは層が違う。2014 年 9 月に公開された Reactive Manifesto v2.0 が挙げる応答性・耐障害性・弾力性・メッセージ駆動はシステム全体の性質についての主張で、コードの書き方を定めたものではない。宣言に沿った構成でも中身が命令的なことはあるし、その逆もある。
命令的 vs リアクティブ
命令的な書き方では、値が変わったときに何を作り直すかを人が書く。リアクティブな書き方では値どうしの関係だけを書いておき、作り直しの起動を仕組みに任せる。
// 命令的: 手動でデータの変化を追跡
let a = 1;
let b = 2;
let c = a + b; // 3
a = 10;
// c は 3 のまま (再計算されない)
// リアクティブ: データの変化が自動伝播
// React の例
const [a, setA] = useState(1);
const [b, setB] = useState(2);
const c = a + b; // a や b が変わると c も自動更新
「自動更新」の中身は方式で違う。React は状態を更新したコンポーネントの関数を丸ごと呼び直すので、中の式は関係の有無にかかわらず全部評価し直される。Vue が採る方式は、実行中の処理がどのプロパティを読んだかを Proxy とアクセサで記録しておき、書き換えられた値を読んでいた処理だけを再実行する。同じ宣言的な見た目でも再実行の粒度が違うため、重い計算をどこに置くかの判断も変わる。
RxJS の Observable
RxJS の Observable が運ぶ通知は、値が届く・並びが終わる・失敗するの 3 種類だけである。演算子を pipe でつなぐと、待つ・間引く・打ち切るといった時間の扱いまでコードの形として残る。単発の通知を配るだけの オブザーバーパターン との実務上の差はここにある。2026 年 8 月時点の最新安定版は 7.8 系で、次のメジャー版はベータ段階にある。
import { fromEvent, from, of, map, debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs';
// 検索ボックスの入力を宣言的に処理する
fromEvent(searchInput, 'input').pipe(
map(e => (e.target as HTMLInputElement).value),
debounceTime(300), // 入力が 300ms 止まるまで待つ
distinctUntilChanged(), // 直前と同じ語なら投げ直さない
switchMap(query => // 次の入力が来たら前の結果を捨てる
from(fetch(`/api/search?q=${encodeURIComponent(query)}`).then(r => r.json())).pipe(
catchError(() => of([])), // 失敗は内側で受ける
),
),
).subscribe(results => renderResults(results));
3 点が実務では効く。fetch は Response を返すだけなので json() まで通さないと購読側に本文が渡らない。クエリは URL に埋める前に符号化しないと、記号を含む語で壊れる。そして catchError を外側の末尾に置くと、1 回の通信失敗で並び自体が終わり、以降の入力に反応しなくなる。失敗の通知は打ち切りを意味するため、通信ごとの失敗は内側で受け止めて外側の購読を生かす。
React のリアクティブ性
React では状態を更新すると、その状態を持つコンポーネントの関数が呼び直され、前回との差分だけが画面に反映される。合計や税額のような派生値は別の状態として持たず、状態から計算し直す形にすると、更新漏れによる不整合が起きなくなる。
function PriceCalculator() {
const [quantity, setQuantity] = useState(1);
const [unitPrice] = useState(1000);
// 派生値は状態にせず、描画のたびに計算する
const total = quantity * unitPrice;
const tax = total * 0.1;
return (
<div>
<input type="number" min="1" value={quantity}
onChange={e => setQuantity(Number(e.target.value) || 1)} />
<p>小計: {total} 円 / 税込: {total + tax} 円</p>
</div>
);
}
リアクティブストリームの操作
演算子は、値そのものを扱うものと、時間や順序を扱うものに分かれる。命令的な書き方で状態変数とタイマーが増えていくのは後者の担当範囲であり、そこを宣言的に書けることが採用理由になる。
| 操作 | 説明 |
|---|---|
| map | 値を変換 |
| filter | 条件に合う値だけ通す |
| debounceTime | 一定時間待ってから最後の値を通す |
| switchMap | 次の値が来た時点で前の処理を打ち切って乗り換える |
| merge | 複数のストリームを合流 |
| retry | 失敗したら購読をやり直す (回数と間隔を指定できる) |
リアクティブ vs コールバック vs async/await
選び分けの基準は「値が何回届くか」と「途中で捨てる必要があるか」の 2 点である。
| 方式 | 適用 |
|---|---|
| コールバック | 単発のイベント。合成や解除は自分で書く |
| async/await | 1 回の非同期操作。値は 1 つで、打ち切りは別の仕組みに頼る |
| リアクティブ (Observable) | 連続するデータストリーム。合成・打ち切り・再試行を演算子で書ける |
落とし穴
何でもストリームにすると読み手の前提知識が増える。1 回取得して終わる処理は async/await のほうが短く、後から読む人も追いやすい。演算子を持ち出す価値が出るのは、間引き・乗り換え・再試行のどれかが要るときである。
switchMap が捨てるのは内側の購読であって、走り出した通信そのものではない。回線と API 側の負荷まで止めたいなら AbortController を渡して中断させる。
演算子の中に副作用を書くと、購読が 2 本になった時点で 2 回実行される。多くの Observable は購読ごとに処理を始める作りで、DOM 操作や送信を map の中に混ぜると原因の見えない二重実行になる。副作用は購読側か専用の演算子に寄せる。
購読の解除漏れは、リアクティブに限らず購読という仕組みそのものに付く問題である。解除の作法は オブザーバーパターン 側で扱う。
この記事は役に立ちましたか?
関連用語
オブザーバーパターン
オブジェクトの状態変化を複数の依存オブジェクトに自動通知するデザインパターン
ストリーム処理
データを受信しながらリアルタイムで処理する手法で、バッチ処理と対比される
非同期プログラミング
I/O 待ちの間に他の処理を進め、システムのスループットを向上させるプログラミング手法
React
コンポーネントベースの UI ライブラリで、宣言的な記法と仮想 DOM の差分計算で UI の更新を扱う
カナリアリリース
新バージョンを少数のユーザーに先行公開し、問題がないことを確認してから全体に展開するデプロイ手法
状態管理
フロントエンドアプリケーションのデータの流れと状態の変更を一元的に管理する手法