【実務・中級編】 RecoilのAtomとSelectorの概念 – React実践ガイド

お疲れ。最近、チームのコードレビューをしていて「またここでStateのバグ踏んでるな…」って思うことが増えたんだよね。

親コンポーネントから子、孫へとプロパティをバケツリレーさせたり、無理やりContext APIを細分化してパフォーマンスチューニングに消耗したり……。君もそんな「状態管理の沼」にハマって夜な夜なリファクタリングしていませんか?

今回は、そんなフロントエンドの構造的疲労を鮮やかに解決してくれるRecoilの「Atom」と「Selector」について、現場のリアルな泥臭い知見を交えながら徹底的に解説していくよ。
公式ドキュメントには書いていない、ブラウザの裏側の動きや実践的な設計のコツまで叩き込むから、コーヒーでも飲みながらじっくり読んでくれ。

—

なぜ今、Recoilなのか?(現場のリアルな課題感)

Reactを触り始めて半年、一通りの機能を作れるようになった中級エンジニアが必ずぶ I/O(Input/Output)の壁、それが「コンポーネントを跨いだ状態共有と再レンダリング地獄」だ。

例えば、ECサイトのヘッダーにある「カートアイコンのアイテム数」と「商品一覧ページのカート追加ボタン」、そして「ドロワーのカート内訳」。これらをすべて親の`useState`で管理しようとするとどうなるか?
Stateを吊り上げる(Lifting State Up)たびに、関係のない親コンポーネントまで巻き添えで再レンダリングされ、ブラウザのメインスレッドは悲鳴を上げる。かといって、Context APIを導入すると、値が1つ変わっただけでそのContextを購読している全コンポーネントが強制再描画される。「あれ、パフォーマンス悪くなってない…?」というお決のパターンだ。

そこで登場するのが、Facebook(現Meta)が開発した状態管理ライブラリ Recoil だ。
Recoilは、Reactのコンポーネントツリーから独立した「データフローグラフ」を構築し、必要なコンポーネントにピンポイントで状態を届けてくれる。その心臓部が Atom と Selector なんだ。

—

1. Atom(アトム):状態の最小単位

Atomとは何か?

Atomは、いわば「状態の断片(ピース)」だ。
`useState`がコンポーネントのなかに閉じ込められたプライベートな状態だとすれば、Atomはグローバルに存在し、どこからでもアクセスできるパブリックな状態の置き場所だと言える。

特徴的なのは、Atomを更新すると、そのAtomを「購読(subscribe)している」コンポーネントだけがピンポイントで再レンダリングされるという点。React標準のContextのように、無関係なコンポーネントまで巻き込むことがない。これがパフォーマンス上有利な最大の理由だ。

実務で使えるAtomのコード例

まずは、ユーザーの認証状態やUIのダークモードなど、シンプルなデータを保持するAtomの書き方を見てみよう。

import { atom } from ‘recoil’;

// ユーザーのテーマ設定(ダークモードかどうか)を保持するAtom
// keyは一意(ユニーク)である必要があるため、コンポーネント名やファイル名をプレフィックスにするのが現場の暗黙の了解
export const themeState = atom<'light' | 'dark'>({
key: ‘themeState’,
default: ‘light’, // 初期値
});

// ショッピングカートに追加された商品のIDリストを保持するAtom
export const cartItemIdsState = atom({
key: ‘cartItemIdsState’,
default: [],
});

チーフからのアドバイス:
`key`文字列の重複はRecoilが即座にエラーを吐く原因になる。大規模開発では、`[ファイル名]/[一意の識別子]` の命名規則をチーム内で必ずドキュメント化しておこう。

—

2. Selector(セレクター):純粋関数による派生状態の計算

Selectorとは何か?

Atomだけでも状態管理はできる。しかし、実務では「Atom AとAtom Bのデータを加工して新しいデータを作りたい」「APIから非同期でデータを取得しつつ、特定の条件でフィルタリングしたい」という要件が必ず出てくる。

ここで登場するのが Selector だ。
Selectorは、「Atomや他のSelectorから派生した、計算済みの状態」を返す。
イメージとしては、Excelの「セル(Atom)」と、数式や関数を入れた「計算セル(Selector)」の関係に近い。

ここで重要なのは、Selectorの本質が「純粋関数(Pure Function)」であるということ。同じ入力を受け取れば、必ず同じ出力を返し、副作用(外部の変数を書き換えるなど)を持たない。これにより、ReactのConcurrent Modeやメモ化(キャッシュ)の恩恵を最大限に受けることができる。

実務で使えるSelectorのコード例

先ほどのカートの例を使って、Atom(商品IDのリスト)から、「カート内の総アイテム数」や「合計金額」を動的に計算するSelectorを作ってみよう。

import { atom, selector } from ‘recoil’;
import { cartItemIdsState } from ‘./cartAtoms’; // 先ほどのAtom

// 仮の商品マスターデータ(本来はAPIや別のAtomから取得する想定)
const productDatabase: Record = {
‘item-1’: { name: ‘超高速TypeScript入門’, price: 3200 },
‘item-2’: { name: ‘Reactアーキテクチャ実践’, price: 4000 },
};

// 1. カート内のアイテムIDから、実際の商品の詳細オブジェクトの配列を返す派生Selector
export const cartItemsSelector = selector({
key: ‘cartItemsSelector’,
get: ({ get }) => {
// get関数を使って、他のAtomやSelectorを「購読」する
const ids = get(cartItemIdsState);

// IDの配列を、商品オブジェクトの配列に変換
return ids.map((id) => productDatabase[id]).filter(Boolean);
},
});

// 2. さらにそこから「合計金額」を算出する、Selector依存のSelector(チェーン可能!)
export const cartTotalPriceSelector = selector({
key: ‘cartTotalPriceSelector’,
get: ({ get }) => {
// cartItemsSelectorをさらに購読する
const items = get(cartItemsSelector);

// 合計金額を計算
return items.reduce((total, item) => total + item.price, 0);
},
});

このコードの美しいところは、`cartTotalPriceSelector` は 「`cartItemsSelector` が変わり、さらにその元の `cartItemIdsState` が変わったときだけ」 再計算されるという点だ。途中の計算結果はReact(Recoil)が自動でメモ化してくれるため、無駄なCPUサイクルを一切消費しない。

—

3. ブラウザの裏側で何が起きているか?(データフローグラフの仕組み)

ここで、ブラウザのランタイムやJavaScriptのメモリ構造の観点から、Recoilが裏側でどう動いているのかを少し深掘りしよう。

Recoilは、メモリ上に「Directed Acyclic Graph(有向非巡環グラフ:DAG)」というデータ構造を構築している。
コンポーネントが `useRecoilValue(cartTotalPriceSelector)` を呼び出すと、Recoilの内部ストア(Store)に対して「このコンポーネントはこのSelectorを購読する」という依存関係(Edge)が登録される。

1. ユーザーがボタンを押し、`setCartItemIds(newIds)` が実行される。
2. Atom (`cartItemIdsState`) の値が書き換わる。
3. グラフを辿って、直接・間接的に依存している Selector (`cartItemsSelector` → `cartTotalPriceSelector`) に「ダーティフラグ(値が古くなったという印)」が伝播する。
4. 実際にその値を必要としている(現在画面にマウントされている)コンポーネントに対してのみ、Reactの再レンダリング要求(`scheduleUpdate`)が飛ぶ。

従来の `useState` によるバケツリレーや、雑に作られたContext APIが「ドミノ倒し式」に全コンポーネントを再描画させていたのに対し、Recoilは「必要な配線だけに電流を流すスマートグリッド」のような挙動をする。これが、中規模〜大規模アプリケーションで絶大なパフォーマンスを発揮する理由だ。

—

4. 現場ですぐに使える!コンポーネントでの実装例

理屈はこれくらいにして、実際にReactのコンポーネントでAtomとSelectorをどう呼び出すのか、スッキリとした実用的なコードを見てみよう。

import React from ‘react’;
import { useRecoilValue, useSetRecoilState } from ‘recoil’;
import { cartItemIdsState, cartItemsSelector, cartTotalPriceSelector } from ‘./cartAtoms’;

export const CartWidget: React.FC = () => {
// 1. 読み取り専用でSelectorから合計金額を取得する
const totalPrice = useRecoilValue(cartTotalPriceSelector);

// 2. アイテム数だけを取得するために、カートアイテムのSelectorを購読
const items = useRecoilValue(cartItemsSelector);

// 3. 状態を更新する関数だけを取得する(useSetRecoilStateを使うことで、値の変更による不要な再レンダリングを防ぐ!)
const setCartItemIds = useSetRecoilState(cartItemIdsState);

const handleAddToCart = (itemId: string) => {
// 既存の配列に新しいIDを追加するイミュータブルな更新
setCartItemIds((prevIds) => […prevIds, itemId]);
};

const handleClearCart = () => {
setCartItemIds([]); // カートを空にする
};

return (

ショッピングカート

現在のアイテム数: {items.length} 点

合計金額: ¥{totalPrice.toLocaleString()}

    {items.map((item, index) => (

  • {item.name} – ¥{item.price}
  • ))}


);
};

チーフからの実践的Tips:`useRecoilState` より使い分けろ!

よくやりがちなアンチパターンとして、すべてのコンポーネントで `useRecoilState(atom)` を使って、値の読み取りも書き込みも両方引っ張ってくるコードを見る。
値しか使わない(表示するだけ)のコンポーネントで `useRecoilState` を使うと、親切心で書いたつもりが「状態が更新されたら問答無用で再レンダリングされるリスナー」になってしまう。

  • 読むだけなら `useRecoilValue`
  • 書くだけ(更新するだけ)なら `useSetRecoilState`
  • 両方必要なら `useRecoilState`

この3つを適切に使い分けることが、シニアとジュニアを分ける最初の分水嶺だ。

—

まとめ:状態管理は「シンプル」に保て

今回はRecoilの基本中の基本である Atom と Selector の概念、そしてブラウザの裏側の動きと実務でのベストプラクティスを解説した。

状態管理ライブラリは、一歩間違えると「何がどこで変更されているか分からない魔境」を作り出してしまう諸刃の剣だ。しかし、Atomで「真実のデータ(Single Source of Truth)」を定義し、Selectorで「そこから派生する美しく純粋な計算結果」を導き出すという設計思想をチームで共有できれば、コードベースは驚くほど見通しがよく、堅牢なものに生まれ変わる。

明日のコードレビューでは、無駄なバケツリレーや、無駄な再レンダリングを生んでいるコンポーネントがないか、ぜひこのAtomとSelectorの視点で見直してみてほしい。
それじゃあ、今日も最高のコードを書こうぜ!

コメント

タイトルとURLをコピーしました