SolidJS コンポーネント間通信チート:Context・イベント・store の選択早見
SolidJS でアプリケーションを構築する際、コンポーネント間でデータをやり取りする方法が複数あって、どれを選べばよいのか迷ってしまうことはありませんか。 Context を使うべきか、カスタムイベントで済ませるべきか、それとも Store を導入すべきか。それぞれに得意な場面があり、適切に使い分けることでコードの可読性や保守性が大きく向上します。
本記事では、SolidJS における 3 つの主要なコンポーネント間通信手法(Context API、カスタムイベント、Store)の特徴と選択基準を、早見表と具体例を交えて解説していきます。
早見表:通信手法の選択基準
各手法の特徴を一目で把握できるよう、比較表を用意しました。
| # | 手法 | 適した用途 | スコープ | パフォーマンス | 学習コスト | 典型的な利用例 |
|---|---|---|---|---|---|---|
| 1 | Context API | ツリー内での依存性注入 | ローカル(Provider 配下) | ★★★★ | ★★★ | テーマ設定、認証情報、ルート設定 |
| 2 | カスタムイベント | 疎結合な通知・イベント駆動 | グローバル or ローカル | ★★★ | ★★ | ボタンクリック通知、モーダル制御 |
| 3 | Store (createStore) | グローバルな状態管理 | グローバル | ★★★★★ | ★★★★ | ユーザーデータ、カート情報、アプリ全体の状態 |
凡例: ★ が多いほど優れている、または高度
| # | 手法 | メリット | デメリット |
|---|---|---|---|
| 1 | Context API | ツリー構造に自然にフィット、型安全 | Provider のネスト地獄になる可能性 |
| 2 | カスタムイベント | コンポーネント間の結合度が低い、柔軟 | 型安全性が弱い、デバッグが難しい |
| 3 | Store | 細粒度のリアクティビティ、パフォーマンス最高 | 学習コストが高い、過度な利用で複雑化 |
背景
SolidJS のリアクティビティシステム
SolidJS は、細粒度のリアクティビティシステムを採用しているモダンな JavaScript フレームワークです。 React や Vue と異なり、仮想 DOM を使わず、リアクティブな値の変更を直接 DOM に反映させます。
このアーキテクチャにより、高速なレンダリングと最小限の再描画が実現されていますが、一方でコンポーネント間通信の手法も独自の特性を持ちます。
以下の図は、SolidJS アプリケーションにおけるデータフローの全体像を示しています。
mermaidflowchart TB
subgraph app["アプリケーション全体"]
root["ルートコンポーネント"]
subgraph context_scope["Context スコープ"]
provider["Context Provider"]
child1["子コンポーネントA"]
child2["子コンポーネントB"]
end
subgraph global["グローバル領域"]
store["Store<br/>(グローバル状態)"]
events["CustomEvent<br/>(イベントバス)"]
end
end
root --> provider
provider -->|useContext| child1
provider -->|useContext| child2
child1 -.->|dispatch| events
events -.->|listen| child2
child1 -->|setStore| store
child2 -->|読み取り| store
この図から分かるように、Context、イベント、Store はそれぞれ異なるスコープと通信経路を持っています。
各手法の基本的な位置づけ
SolidJS では以下の 3 つが主要なコンポーネント間通信手法として提供されています。
Context APIは、React と同様に依存性注入(DI)パターンを実現します。 親コンポーネントから子孫コンポーネントへデータを「注入」する形で、props のバケツリレーを避けられます。
カスタムイベントは、ブラウザ標準の CustomEvent を活用した疎結合な通信方法です。 イベント駆動アーキテクチャを採用したい場合に有効で、特定のコンポーネントに依存しない設計が可能になります。
Storeは、SolidJS 独自の強力な状態管理ソリューションです。 細粒度のリアクティビティを活かし、大規模なアプリケーション状態を効率的に管理できます。
課題
どの手法を選ぶべきか判断が難しい
開発者が直面する最大の課題は、「どの場面でどの手法を使うべきか」の判断基準が曖昧なことです。
例えば、認証情報をアプリ全体で共有したい場合、Context で実装するべきでしょうか、それとも Store を使うべきでしょうか。 また、モーダルの開閉をトリガーしたい場合、イベントと Context のどちらが適しているでしょうか。
以下は、よくある判断の迷いを図示したものです。
mermaidflowchart TD
start["コンポーネント間通信が必要"]
q1{"データは<br/>グローバルか?"}
q2{"頻繁に<br/>更新されるか?"}
q3{"ツリー構造に<br/>沿っているか?"}
q4{"イベント駆動<br/>の設計か?"}
ans_store["Store を選択"]
ans_context["Context を選択"]
ans_event["CustomEvent を選択"]
ans_props["Props で十分"]
start --> q1
q1 -->|はい| q2
q1 -->|いいえ| q3
q2 -->|はい| ans_store
q2 -->|いいえ| q4
q3 -->|はい| ans_context
q3 -->|いいえ| q4
q4 -->|はい| ans_event
q4 -->|いいえ| ans_props
この判断フローを参考にすることで、適切な手法を選びやすくなります。
誤った選択がもたらす問題
不適切な手法を選択すると、以下のような問題が発生します。
過度な Context 使用による問題として、Provider が深くネストし、コードの可読性が低下します。 また、Context の値が変更されると、その Context を利用するすべての子孫コンポーネントが再評価される可能性があり、パフォーマンスに影響を及ぼすことがあります。
カスタムイベントの乱用は、イベント名の管理が煩雑になり、どのコンポーネントがどのイベントをリスンしているのか追跡しづらくなります。 型安全性も失われがちで、実行時エラーのリスクが高まるでしょう。
Store の過剰導入では、本来ローカルで管理すべき状態までグローバル化してしまい、アプリケーション全体の複雑性が増大します。 小規模な機能にまで Store を使うと、かえってコードが冗長になってしまいます。
解決策
Context API:ツリー内での依存性注入
Context API は、コンポーネントツリー内での依存性注入に最適です。
Context API の特徴
Context は以下の特徴を持っています。
- スコープが Provider 配下に限定される
- 型安全性が高い(TypeScript との相性が良い)
- ツリー構造に沿った自然なデータフロー
Context API を選ぶべき場面
以下のような場面で Context を選択しましょう。
| # | 場面 | 理由 |
|---|---|---|
| 1 | テーマ設定(ダークモード/ライトモード) | アプリ全体で共有するが、変更頻度は低い |
| 2 | 認証情報(ユーザー情報、トークン) | セキュアに特定スコープで共有したい |
| 3 | ルーティング情報 | ページ単位でのコンテキストが必要 |
| 4 | 多言語対応(i18n) | 言語設定を子孫コンポーネントで参照 |
Context API の実装パターン
Context の基本的な実装方法を見ていきましょう。
カスタムイベント:疎結合なイベント駆動通信
カスタムイベントは、コンポーネント間の結合度を下げたい場合に有効です。
カスタムイベントの特徴
以下のような特性があります。
- 送信側と受信側が互いを知る必要がない
- グローバルにもローカルにも使える
- ブラウザ標準機能を活用
カスタムイベントを選ぶべき場面
イベント駆動が適している場面を整理します。
| # | 場面 | 理由 |
|---|---|---|
| 1 | モーダルやダイアログの制御 | 開閉イベントを疎結合に通知 |
| 2 | 通知・トースト表示 | 任意の場所から通知を発火 |
| 3 | アナリティクスイベント送信 | ビジネスロジックと分離したい |
| 4 | 画面遷移のトリガー | ルーティング制御を柔軟に |
カスタムイベントの実装パターン
イベントベースの通信を実装する方法を解説します。
Store:グローバルな状態管理
Store は、アプリケーション全体で共有する複雑な状態を管理するのに最適です。
Store の特徴
SolidJS の Store は非常に強力です。
- 細粒度のリアクティビティによる高いパフォーマンス
- ネストしたオブジェクトもリアクティブに扱える
- Immer のような記述が可能(produce ユーティリティ)
Store を選ぶべき場面
以下のケースで Store が威力を発揮します。
| # | 場面 | 理由 |
|---|---|---|
| 1 | ユーザーデータ(プロフィール、設定) | 複数画面で参照・更新が必要 |
| 2 | ショッピングカート | 商品追加・削除・数量変更が頻繁 |
| 3 | フォームの複雑な状態管理 | 多段フォームや検証状態の管理 |
| 4 | リアルタイムデータ(チャット、通知) | 頻繁な更新とリアクティブな反映 |
Store の実装パターン
Store を使った状態管理の実装を見ていきます。
具体例
それでは、各手法の具体的な実装コードを見ていきましょう。 実際のユースケースに基づいたサンプルコードを提示します。
Context API の実装例
テーマ設定を管理する Context の実装を段階的に説明します。
ステップ 1:Context の型定義
まず、Context で扱うデータの型を定義します。
typescript// theme-context.ts
// テーマの種類を定義
type Theme = 'light' | 'dark';
// Contextで提供する値の型
interface ThemeContextValue {
theme: () => Theme; // 現在のテーマを取得
toggleTheme: () => void; // テーマを切り替える関数
}
型を明確にすることで、TypeScript の恩恵を最大限受けられます。
ステップ 2:Context の作成
次に、createContext で Context オブジェクトを生成します。
typescriptimport { createContext } from 'solid-js';
// ThemeContextを作成(初期値はundefined)
export const ThemeContext =
createContext<ThemeContextValue>();
初期値を undefined にすることで、Provider の外で使用した際にエラーを検出できます。
ステップ 3:Provider コンポーネントの実装
Provider コンポーネントでは、テーマの状態とロジックを管理します。
typescriptimport { createSignal, JSX } from 'solid-js';
interface ThemeProviderProps {
children: JSX.Element;
}
export function ThemeProvider(props: ThemeProviderProps) {
// テーマの状態を管理するSignal
const [theme, setTheme] = createSignal<Theme>('light');
// テーマを切り替える関数
const toggleTheme = () => {
setTheme((prev) =>
prev === 'light' ? 'dark' : 'light'
);
};
// Contextに提供する値
const value: ThemeContextValue = {
theme,
toggleTheme,
};
return (
<ThemeContext.Provider value={value}>
{props.children}
</ThemeContext.Provider>
);
}
createSignal でリアクティブな状態を作成し、Provider で配下のコンポーネントに提供しています。
ステップ 4:カスタムフックの作成
Context を使いやすくするため、カスタムフックを用意します。
typescriptimport { useContext } from 'solid-js';
// ThemeContextを安全に取得するフック
export function useTheme() {
const context = useContext(ThemeContext);
// Providerの外で使われた場合はエラー
if (!context) {
throw new Error(
'useTheme must be used within ThemeProvider'
);
}
return context;
}
このフックにより、型安全かつ簡潔に Context を利用できます。
ステップ 5:コンポーネントでの利用
実際にコンポーネントで ThemeContext を使ってみましょう。
typescriptimport { Component } from 'solid-js';
import { useTheme } from './theme-context';
const ThemeToggleButton: Component = () => {
// カスタムフックでContextを取得
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
現在のテーマ: {theme()}
(クリックで切り替え)
</button>
);
};
useTheme フックを呼ぶだけで、簡単にテーマ情報にアクセスできます。
ステップ 6:アプリケーション全体への適用
最後に、ルートコンポーネントで Provider を配置します。
typescriptimport { Component } from 'solid-js';
import { ThemeProvider } from './theme-context';
import ThemeToggleButton from './ThemeToggleButton';
const App: Component = () => {
return (
<ThemeProvider>
<div>
<h1>SolidJS テーマ切り替えアプリ</h1>
<ThemeToggleButton />
</div>
</ThemeProvider>
);
};
ThemeProvider で囲んだ配下のすべてのコンポーネントで useTheme が利用可能になります。
カスタムイベントの実装例
モーダルダイアログの制御をカスタムイベントで実装してみましょう。
ステップ 1:イベント名の定義
イベント名を定数として定義し、タイポを防ぎます。
typescript// events.ts
// モーダル関連のイベント名を定数化
export const MODAL_EVENTS = {
OPEN: 'modal:open',
CLOSE: 'modal:close',
} as const;
// イベントのペイロード型
export interface ModalEventDetail {
modalId: string; // モーダルのID
data?: Record<string, unknown>; // 任意のデータ
}
定数化することで、IDE の補完も効き、保守性が向上します。
ステップ 2:イベント発火用のヘルパー関数
イベントをディスパッチするヘルパー関数を作成します。
typescript// modal-events.ts
export function openModal(
modalId: string,
data?: Record<string, unknown>
) {
// CustomEventを作成
const event = new CustomEvent(MODAL_EVENTS.OPEN, {
detail: { modalId, data },
});
// グローバルにディスパッチ
window.dispatchEvent(event);
}
export function closeModal(modalId: string) {
const event = new CustomEvent(MODAL_EVENTS.CLOSE, {
detail: { modalId },
});
window.dispatchEvent(event);
}
これらのヘルパー関数により、どこからでも簡単にモーダルを制御できます。
ステップ 3:イベントリスナーの実装
モーダルコンポーネント側でイベントをリッスンします。
typescriptimport {
createSignal,
onMount,
onCleanup,
Component,
} from 'solid-js';
interface ModalProps {
modalId: string;
}
const Modal: Component<ModalProps> = (props) => {
// モーダルの開閉状態
const [isOpen, setIsOpen] = createSignal(false);
// イベントハンドラー:モーダルを開く
const handleOpen = (event: Event) => {
const customEvent =
event as CustomEvent<ModalEventDetail>;
// 自分宛のイベントかチェック
if (customEvent.detail.modalId === props.modalId) {
setIsOpen(true);
}
};
// イベントハンドラー:モーダルを閉じる
const handleClose = (event: Event) => {
const customEvent =
event as CustomEvent<ModalEventDetail>;
if (customEvent.detail.modalId === props.modalId) {
setIsOpen(false);
}
};
return (
// JSXの実装は省略
<div>Modal Content</div>
);
};
CustomEvent の detail プロパティから必要な情報を取得しています。
ステップ 4:ライフサイクルでのイベント登録・解除
onMount と onCleanup でイベントリスナーを適切に管理します。
typescriptconst Modal: Component<ModalProps> = (props) => {
const [isOpen, setIsOpen] = createSignal(false);
// (前述のhandleOpen, handleCloseは省略)
onMount(() => {
// コンポーネントマウント時にリスナー登録
window.addEventListener(MODAL_EVENTS.OPEN, handleOpen);
window.addEventListener(
MODAL_EVENTS.CLOSE,
handleClose
);
});
onCleanup(() => {
// コンポーネント破棄時にリスナー解除
window.removeEventListener(
MODAL_EVENTS.OPEN,
handleOpen
);
window.removeEventListener(
MODAL_EVENTS.CLOSE,
handleClose
);
});
return (
<div class={isOpen() ? 'modal-open' : 'modal-closed'}>
{/* モーダルの中身 */}
</div>
);
};
onCleanup でリスナーを解除することで、メモリリークを防ぎます。
ステップ 5:イベント発火側の実装
別のコンポーネントからモーダルを開く例です。
typescriptimport { Component } from 'solid-js';
import { openModal } from './modal-events';
const TriggerButton: Component = () => {
// ボタンクリック時にモーダルを開く
const handleClick = () => {
openModal('user-settings', {
userId: 123,
tab: 'profile',
});
};
return <button onClick={handleClick}>設定を開く</button>;
};
openModal ヘルパーを使うだけで、モーダルコンポーネントと直接依存せずに制御できます。
Store の実装例
ショッピングカートを Store で管理する実装を見ていきます。
ステップ 1:Store の型定義
カートで扱うデータの型を定義します。
typescript// cart-store.ts
// 商品アイテムの型
export interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
// カート全体の型
export interface CartStore {
items: CartItem[];
totalPrice: number;
}
明確な型定義により、開発時のミスを防げます。
ステップ 2:Store の作成と初期化
createStore でリアクティブなストアを作成します。
typescriptimport { createStore } from 'solid-js/store';
// 初期状態
const initialState: CartStore = {
items: [],
totalPrice: 0,
};
// Storeの作成
export const [cartStore, setCartStore] =
createStore<CartStore>(initialState);
createStore は配列を返し、第 1 要素が読み取り専用の Store、第 2 要素が更新関数です。
ステップ 3:カート操作のアクション関数
商品追加・削除などのロジックを関数として実装します。
typescript// 商品を追加
export function addToCart(
item: Omit<CartItem, 'quantity'>
) {
// 既存の商品か確認
const existingIndex = cartStore.items.findIndex(
(i) => i.id === item.id
);
if (existingIndex >= 0) {
// 既存商品の数量を増やす
setCartStore(
'items',
existingIndex,
'quantity',
(q) => q + 1
);
} else {
// 新規商品を追加
setCartStore('items', (items) => [
...items,
{ ...item, quantity: 1 },
]);
}
// 合計金額を再計算
updateTotalPrice();
}
SolidJS の setStore は、パス指定で深い階層も簡単に更新できます。
ステップ 4:商品削除と数量変更
削除と数量変更のアクションも実装します。
typescript// 商品を削除
export function removeFromCart(itemId: string) {
setCartStore('items', (items) =>
items.filter((item) => item.id !== itemId)
);
updateTotalPrice();
}
// 数量を変更
export function updateQuantity(
itemId: string,
newQuantity: number
) {
if (newQuantity <= 0) {
// 数量が0以下なら削除
removeFromCart(itemId);
return;
}
const index = cartStore.items.findIndex(
(i) => i.id === itemId
);
if (index >= 0) {
setCartStore('items', index, 'quantity', newQuantity);
updateTotalPrice();
}
}
数量が 0 以下になったら自動的に削除するロジックを入れています。
ステップ 5:合計金額の計算
合計金額を計算する補助関数です。
typescript// 合計金額を更新
function updateTotalPrice() {
const total = cartStore.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
setCartStore('totalPrice', total);
}
商品の追加・削除・数量変更の度に呼び出され、常に最新の合計金額を保持します。
ステップ 6:コンポーネントでの利用
カート情報を表示するコンポーネントを作成します。
typescriptimport { Component, For } from 'solid-js';
import {
cartStore,
removeFromCart,
updateQuantity,
} from './cart-store';
const CartView: Component = () => {
return (
<div class='cart'>
<h2>ショッピングカート</h2>
{/* カート内の商品をリスト表示 */}
<For each={cartStore.items}>
{(item) => (
<div class='cart-item'>
<span>{item.name}</span>
<span>¥{item.price}</span>
{/* 数量変更 */}
<input
type='number'
value={item.quantity}
onInput={(e) => {
const value = parseInt(
e.currentTarget.value
);
updateQuantity(item.id, value);
}}
/>
{/* 削除ボタン */}
<button onClick={() => removeFromCart(item.id)}>
削除
</button>
</div>
)}
</For>
{/* 合計金額 */}
<div class='total'>合計: ¥{cartStore.totalPrice}</div>
</div>
);
};
cartStore を直接参照するだけで、リアクティブに UI が更新されます。
ステップ 7:別のコンポーネントからカートに追加
商品リストからカートに追加する例です。
typescriptimport { Component } from 'solid-js';
import { addToCart } from './cart-store';
interface ProductCardProps {
id: string;
name: string;
price: number;
}
const ProductCard: Component<ProductCardProps> = (
props
) => {
const handleAddToCart = () => {
addToCart({
id: props.id,
name: props.name,
price: props.price,
});
};
return (
<div class='product-card'>
<h3>{props.name}</h3>
<p>¥{props.price}</p>
<button onClick={handleAddToCart}>
カートに追加
</button>
</div>
);
};
どのコンポーネントからでも addToCart を呼ぶだけで、グローバルなカート状態が更新されます。
実装パターンの比較図
3 つの手法を実装した際の構造を図で比較してみましょう。
mermaidflowchart TB
subgraph context_pattern["Context パターン"]
cp1["ThemeProvider"]
cp2["Button Component"]
cp3["Header Component"]
cp1 -->|useContext| cp2
cp1 -->|useContext| cp3
end
subgraph event_pattern["Event パターン"]
ep1["Trigger Component"]
ep2["Modal Component"]
ep3["window (EventTarget)"]
ep1 -.->|dispatch| ep3
ep3 -.->|listen| ep2
end
subgraph store_pattern["Store パターン"]
sp1["Global Store"]
sp2["Product List"]
sp3["Cart View"]
sp2 -->|addToCart| sp1
sp1 -->|reactive| sp3
end
それぞれのデータフローの違いが明確になりますね。
まとめ
SolidJS でのコンポーネント間通信には、Context API、カスタムイベント、Store という 3 つの主要な手法があります。
Context APIは、コンポーネントツリー内での依存性注入に適しており、テーマ設定や認証情報など、変更頻度が低く、特定スコープで共有したいデータに最適です。 型安全性が高く、Provider の配下で自然なデータフローを実現できます。
カスタムイベントは、コンポーネント間を疎結合に保ちたい場合に有効で、モーダル制御や通知システムなど、イベント駆動のアーキテクチャに向いています。 ただし、型安全性が弱く、デバッグが難しくなる可能性があるため、使用は必要最小限に留めるべきでしょう。
Storeは、アプリケーション全体で共有する複雑な状態管理に威力を発揮します。 細粒度のリアクティビティにより高いパフォーマンスを実現し、ユーザーデータやショッピングカートなど、頻繁に更新されるグローバルな状態に最適です。
これら 3 つの手法を適切に使い分けることで、保守性が高く、パフォーマンスに優れた SolidJS アプリケーションを構築できます。 早見表を参考にしながら、それぞれの特性を理解し、プロジェクトの要件に最適な手法を選択していきましょう。
関連リンク
articleSolidJS コンポーネント間通信チート:Context・イベント・store の選択早見
articlesolidJS × SolidStart を Cloudflare Pages にデプロイ:Edge 最適化の手順
articleSolidJS のアニメーション比較:Motion One vs Popmotion vs CSS Transitions
articleSolidJS で無限ループが止まらない!createEffect/onCleanup の正しい書き方
articleSolidJS の Control Flow コンポーネント大全:Show/For/Switch/ErrorBoundary を使い分け
articleSolidJS 本番運用チェックリスト:CSP・SRI・Preload・エラーレポートの総点検
articleKubernetes で WebSocket:Ingress(NGINX/ALB) 設定とスティッキーセッションの実装手順
articleStorybook × Design Tokens 設計:Style Dictionary とテーマ切替の連携
articleWebRTC Simulcast 設計ベストプラクティス:レイヤ数・ターゲットビットレート・切替条件
articleSolidJS コンポーネント間通信チート:Context・イベント・store の選択早見
articleWebLLM 中心のクライアントサイド RAG 設計:IndexedDB とベクトル検索の組み立て
articleShell Script の set -e が招く事故を回避:pipefail・サブシェル・条件分岐の落とし穴
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来