SolidJS の Control Flow コンポーネント大全:Show/For/Switch/ErrorBoundary を使い分け
SolidJS でアプリケーションを開発していると、条件分岐やリストレンダリング、エラー処理など、UI の制御フローをどう書くべきか迷うことがありますよね。React や Vue とは異なり、SolidJS には専用の Control Flow コンポーネントが用意されています。
この記事では、SolidJS の 4 つの主要な Control Flow コンポーネント(<Show>、<For>、<Switch>、<ErrorBoundary>)の使い方と使い分けを、実践的なコード例とともに詳しく解説します。これらを正しく使うことで、パフォーマンスに優れた、メンテナンスしやすいコードが書けるようになるでしょう。
背景
SolidJS におけるリアクティブシステムの特徴
SolidJS は、きめ細かいリアクティビティ(Fine-grained Reactivity)を採用しているフレームワークです。これにより、仮想 DOM を使わずに、変更があった部分だけを効率的に更新できます。
しかし、この仕組みを最大限に活かすには、通常の JavaScript の制御構文(if文やmapメソッドなど)をそのまま使うだけでは不十分なのです。
なぜ専用の Control Flow コンポーネントが必要なのか
JavaScript の通常の制御構文は、SolidJS のリアクティブシステムと連携できません。例えば、以下のようなコードを見てみましょう。
typescript// ❌ これは期待通りに動作しません
function BadExample() {
const [show, setShow] = createSignal(false);
// if文はリアクティブではない
if (show()) {
return <div>表示されます</div>;
}
return <div>非表示です</div>;
}
上記のコードでは、showの値が変わっても、再レンダリングされません。SolidJS のコンポーネントは一度しか実行されないため、if文も初回しか評価されないのです。
SolidJS のリアクティブシステムと制御フローの関係を図で示すと、以下のようになります。
mermaidflowchart TB
signal["Signal の変更"] -->|検知| reactive["リアクティブ<br/>コンテキスト"]
reactive -->|更新| controlFlow["Control Flow<br/>コンポーネント"]
controlFlow -->|最小限の更新| dom["DOM"]
jsControl["JavaScript<br/>制御構文 (if/map)"] -.->|非リアクティブ| noUpdate["再評価されない"]
style controlFlow fill:#4ade80
style jsControl fill:#f87171
この図から分かるように、Control Flow コンポーネントを使うことで、Signal の変更が適切に DOM の更新に反映されます。
Control Flow コンポーネントの役割
SolidJS の Control Flow コンポーネントは、リアクティブシステムと統合された制御フローを提供します。これにより、以下のメリットが得られるでしょう。
- 自動的な再評価: Signal の値が変わると、自動的に再評価される
- 最適なパフォーマンス: 必要な部分だけを更新する
- 型安全性: TypeScript との相性が良い
課題
React や Vue からの移行時の混乱
React や Vue に慣れた開発者が SolidJS を使い始めると、以下のような課題に直面します。
条件分岐の書き方が分からない
React では三項演算子や&&演算子を使って条件分岐を書きますが、SolidJS では<Show>コンポーネントを使うのが推奨されます。
tsx// React の書き方
{
isLoggedIn ? <Dashboard /> : <Login />;
}
// SolidJS の書き方(推奨)
<Show when={isLoggedIn()} fallback={<Login />}>
<Dashboard />
</Show>;
リストレンダリングの最適化
React のmapメソッドと SolidJS の<For>コンポーネントでは、内部の動作が大きく異なります。
tsx// React の書き方
{
items.map((item) => <Item data={item} key={item.id} />);
}
// SolidJS の書き方
<For each={items()}>{(item) => <Item data={item} />}</For>;
パフォーマンスの問題
Control Flow コンポーネントを使わないと、以下のようなパフォーマンス問題が発生します。
| # | 問題 | 影響 |
|---|---|---|
| 1 | 不要な再レンダリング | CPU リソースの無駄遣い |
| 2 | DOM 操作の増加 | UI の反応速度低下 |
| 3 | メモリリークのリスク | アプリケーションの不安定化 |
どのコンポーネントを使うべきか分からない
4 つの Control Flow コンポーネントがあり、それぞれ適切な使い分けが必要です。誤った選択は、コードの複雑化やパフォーマンス低下につながるでしょう。
以下の図は、Control Flow コンポーネントの選択フローを示しています。
mermaidflowchart TD
start["制御フローが<br/>必要"] --> question1{エラー処理?}
question1 -->|はい| errorBoundary["ErrorBoundary<br/>を使用"]
question1 -->|いいえ| question2{リスト表示?}
question2 -->|はい| forComponent["For または Index<br/>を使用"]
question2 -->|いいえ| question3{複数条件の<br/>分岐?}
question3 -->|はい| switchComponent["Switch/Match<br/>を使用"]
question3 -->|いいえ| showComponent["Show<br/>を使用"]
style errorBoundary fill:#fbbf24
style forComponent fill:#60a5fa
style switchComponent fill:#a78bfa
style showComponent fill:#34d399
解決策
Show コンポーネント:条件分岐の基本
<Show>コンポーネントは、条件に基づいて UI を表示・非表示にするための最も基本的な Control Flow です。
基本的な使い方
typescriptimport { createSignal } from 'solid-js';
import { Show } from 'solid-js/web';
次に、Signal を作成して、条件に応じてコンテンツを表示します。
tsxfunction UserGreeting() {
const [isLoggedIn, setIsLoggedIn] = createSignal(false);
return (
<div>
<Show when={isLoggedIn()}>
<p>ようこそ、ユーザーさん!</p>
</Show>
</div>
);
}
このコードでは、isLoggedInがtrueのときだけ、挨拶メッセージが表示されます。whenプロパティに渡した条件が真の場合に、子要素がレンダリングされるのです。
fallback プロパティで else 分岐を実現
fallbackプロパティを使うと、条件が偽の場合に表示するコンテンツを指定できます。
tsxfunction LoginStatus() {
const [isLoggedIn, setIsLoggedIn] = createSignal(false);
return (
<Show
when={isLoggedIn()}
fallback={<p>ログインしてください</p>}
>
<p>ログイン中です</p>
</Show>
);
}
これにより、if-else文と同等の動作を実現できます。fallbackがあることで、コードの意図が明確になりますね。
値の型絞り込み機能
<Show>の強力な機能の一つが、TypeScript の型絞り込み(Type Narrowing)です。whenの条件が真の場合、子要素内で値が非 null として扱われます。
typescriptinterface User {
name: string;
email: string;
}
次に、<Show>を使ってユーザー情報を安全に表示します。
tsxfunction UserProfile() {
const [user, setUser] = createSignal<User | null>(null);
return (
<Show when={user()}>
{(userData) => (
<div>
{/* userData は User 型として扱われる */}
<h2>{userData().name}</h2>
<p>{userData().email}</p>
</div>
)}
</Show>
);
}
このコードでは、userData()はUser型として扱われるため、nullチェックが不要になります。型安全性が向上し、開発体験が大きく改善されるでしょう。
keyed オプションで参照の比較
デフォルトでは、<Show>は値の真偽のみを評価しますが、keyedオプションを使うと、参照の変更も検知できます。
tsxfunction ProductDisplay() {
const [product, setProduct] = createSignal({
id: 1,
name: '商品A',
});
return (
<Show when={product()} keyed>
{(prod) => (
<div>
{/* product オブジェクトの参照が変わるたびに再作成される */}
<h3>{prod.name}</h3>
</div>
)}
</Show>
);
}
keyedを指定すると、オブジェクトの参照が変わるたびに、子要素が再作成されます。これは、アニメーションやトランジションを適用したい場合に便利です。
For コンポーネント:リストレンダリングの最適化
<For>コンポーネントは、配列をレンダリングするための最適化された Control Flow です。
基本的な使い方
typescriptimport { createSignal } from 'solid-js';
import { For } from 'solid-js/web';
interface Todo {
id: number;
text: string;
completed: boolean;
}
次に、Todo リストをレンダリングします。
tsxfunction TodoList() {
const [todos, setTodos] = createSignal<Todo[]>([
{ id: 1, text: '買い物', completed: false },
{ id: 2, text: '掃除', completed: true },
{ id: 3, text: '勉強', completed: false },
]);
return (
<ul>
<For each={todos()}>
{(todo) => (
<li>
{todo.text} {todo.completed ? '✓' : ''}
</li>
)}
</For>
</ul>
);
}
このコードでは、eachプロパティに配列を渡し、子要素の関数で各アイテムをレンダリングします。
For コンポーネントの最適化の仕組み
<For>は、各アイテムを参照によって追跡します。配列が更新されたとき、変更されたアイテムのみが再レンダリングされるのです。
以下の図は、<For>の更新メカニズムを示しています。
mermaidflowchart LR
oldArray["旧配列<br/>[A, B, C]"] -->|比較| diffEngine["差分検出<br/>エンジン"]
newArray["新配列<br/>[A, D, C]"] -->|比較| diffEngine
diffEngine -->|更新| itemB["Item B<br/>を削除"]
diffEngine -->|追加| itemD["Item D<br/>を追加"]
diffEngine -->|保持| itemAC["Item A, C<br/>は再利用"]
style itemB fill:#f87171
style itemD fill:#4ade80
style itemAC fill:#60a5fa
インデックスへのアクセス
<For>の子要素関数は、第 2 引数としてインデックスを受け取れます。
tsxfunction NumberedList() {
const [items, setItems] = createSignal([
'りんご',
'バナナ',
'オレンジ',
]);
return (
<ol>
<For each={items()}>
{(item, index) => (
<li>
{index() + 1}. {item}
</li>
)}
</For>
</ol>
);
}
注意点として、index()は関数呼び出しが必要です。これは、インデックスが Signal として扱われるためです。
fallback プロパティで空配列に対応
配列が空の場合に表示するコンテンツを、fallbackプロパティで指定できます。
tsxfunction SearchResults() {
const [results, setResults] = createSignal<string[]>([]);
return (
<For
each={results()}
fallback={<p>検索結果がありません</p>}
>
{(result) => <div>{result}</div>}
</For>
);
}
このように、空配列の場合の UI を簡潔に定義できます。ユーザー体験の向上につながるでしょう。
Index コンポーネント:インデックスベースのレンダリング
<Index>は、<For>の姉妹コンポーネントです。アイテムではなく、インデックスを基準に最適化されています。
For と Index の違い
| # | 特徴 | For | Index |
|---|---|---|---|
| 1 | 追跡対象 | アイテムの参照 | インデックス位置 |
| 2 | 適した用途 | オブジェクトの配列 | プリミティブ値の配列 |
| 3 | 更新時の動作 | アイテムが移動しても再利用 | インデックスが変わると再作成 |
基本的な使い方
typescriptimport { createSignal } from 'solid-js';
import { Index } from 'solid-js/web';
次に、数値の配列をレンダリングします。
tsxfunction NumberList() {
const [numbers, setNumbers] = createSignal([
1, 2, 3, 4, 5,
]);
return (
<ul>
<Index each={numbers()}>
{(num, index) => (
<li>
位置 {index}: 値 {num()}
</li>
)}
</Index>
</ul>
);
}
<Index>では、第 1 引数が値(Signal)、第 2 引数がインデックス(数値)となります。これは<For>と逆なので注意が必要です。
いつ Index を使うべきか
プリミティブ値(文字列、数値、真偽値)の配列を扱う場合、<Index>が適しています。
tsxfunction TagList() {
const [tags, setTags] = createSignal([
'TypeScript',
'SolidJS',
'Vite',
]);
return (
<div>
<Index each={tags()}>
{(tag) => <span class='tag'>{tag()}</span>}
</Index>
</div>
);
}
プリミティブ値は参照による比較ができないため、<For>よりも<Index>の方が効率的なのです。
Switch/Match コンポーネント:複数条件の分岐
<Switch>と<Match>は、複数の条件分岐を扱うための Control Flow です。JavaScript のswitch文に相当します。
基本的な使い方
typescriptimport { createSignal } from 'solid-js';
import { Switch, Match } from 'solid-js/web';
type Status = 'loading' | 'success' | 'error';
次に、ステータスに応じて異なる UI を表示します。
tsxfunction DataDisplay() {
const [status, setStatus] =
createSignal<Status>('loading');
return (
<Switch>
<Match when={status() === 'loading'}>
<p>読み込み中...</p>
</Match>
<Match when={status() === 'success'}>
<p>データの読み込みに成功しました</p>
</Match>
<Match when={status() === 'error'}>
<p>エラーが発生しました</p>
</Match>
</Switch>
);
}
<Switch>内の<Match>は、上から順に評価され、最初に真となった<Match>のみがレンダリングされます。
fallback で default ケースを実装
どの<Match>にも該当しない場合のデフォルト表示を、fallbackプロパティで指定できます。
tsxfunction UserRole() {
const [role, setRole] = createSignal<string>('guest');
return (
<Switch fallback={<p>不明な役割です</p>}>
<Match when={role() === 'admin'}>
<p>管理者権限があります</p>
</Match>
<Match when={role() === 'user'}>
<p>一般ユーザーです</p>
</Match>
<Match when={role() === 'guest'}>
<p>ゲストユーザーです</p>
</Match>
</Switch>
);
}
これにより、予期しない値に対しても適切な UI を表示できます。
型安全な Switch の活用
TypeScript のリテラル型と組み合わせると、型安全な分岐を実現できます。
typescripttype PageState =
| { type: 'home' }
| { type: 'profile'; userId: string }
| { type: 'settings'; tab: string };
次に、ページの状態に応じてコンポーネントを切り替えます。
tsxfunction PageRenderer() {
const [page, setPage] = createSignal<PageState>({
type: 'home',
});
return (
<Switch>
<Match when={page().type === 'home'}>
<HomePage />
</Match>
<Match when={page().type === 'profile'}>
{() => {
const p = page() as Extract<
PageState,
{ type: 'profile' }
>;
return <ProfilePage userId={p.userId} />;
}}
</Match>
<Match when={page().type === 'settings'}>
{() => {
const p = page() as Extract<
PageState,
{ type: 'settings' }
>;
return <SettingsPage tab={p.tab} />;
}}
</Match>
</Switch>
);
}
このように、判別可能なユニオン型を使うことで、型安全性を保ちながら複雑な状態管理ができるでしょう。
ErrorBoundary コンポーネント:エラーハンドリング
<ErrorBoundary>は、子コンポーネントで発生したエラーをキャッチして、代替 UI を表示するための Control Flow です。
基本的な使い方
typescriptimport { ErrorBoundary } from 'solid-js';
import { createSignal } from 'solid-js';
次に、エラーが発生する可能性のあるコンポーネントをラップします。
tsxfunction App() {
return (
<ErrorBoundary
fallback={(err) => <div>エラー: {err.message}</div>}
>
<RiskyComponent />
</ErrorBoundary>
);
}
fallbackプロパティには、エラーオブジェクトを受け取る関数を渡します。この関数が返す JSX が、エラー発生時に表示されるのです。
エラーリセット機能の実装
エラーから回復するために、リセット機能を実装できます。
tsxfunction AppWithReset() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<div>
<p>エラーが発生しました: {err.message}</p>
<button onClick={reset}>再試行</button>
</div>
)}
>
<DataFetcher />
</ErrorBoundary>
);
}
fallbackの第 2 引数であるreset関数を呼び出すと、エラー状態がクリアされ、子コンポーネントが再レンダリングされます。
ネストした ErrorBoundary で段階的なエラー処理
複数の<ErrorBoundary>をネストすることで、階層的なエラー処理が可能です。
tsxfunction ComplexApp() {
return (
<ErrorBoundary
fallback={(err) => <AppCrashScreen error={err} />}
>
<Header />
<ErrorBoundary
fallback={(err) => (
<SidebarErrorDisplay error={err} />
)}
>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary
fallback={(err) => (
<ContentErrorDisplay error={err} />
)}
>
<MainContent />
</ErrorBoundary>
</ErrorBoundary>
);
}
この構造により、各セクションで発生したエラーを個別に処理できます。アプリ全体がクラッシュすることを防げるでしょう。
以下の図は、ネストした ErrorBoundary のエラー伝播を示しています。
mermaidflowchart TB
root["Root ErrorBoundary"] --> header["Header"]
root --> sidebar["Sidebar ErrorBoundary"]
root --> content["Content ErrorBoundary"]
sidebar --> sidebarComponent["Sidebar Component"]
content --> contentComponent["Content Component"]
contentComponent -.->|エラー発生| contentError["Content Error UI<br/>を表示"]
sidebarComponent -.->|エラー発生| sidebarError["Sidebar Error UI<br/>を表示"]
header -.->|エラー発生| rootError["App Crash Screen<br/>を表示"]
style contentError fill:#f87171
style sidebarError fill:#fbbf24
style rootError fill:#dc2626
非同期エラーのキャッチ
<ErrorBoundary>は、Suspense と組み合わせることで、非同期処理のエラーもキャッチできます。
typescriptimport { createResource } from 'solid-js';
async function fetchUser(
id: string
): Promise<{ name: string }> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}
次に、リソースを使ってデータを取得します。
tsxfunction UserProfile(props: { userId: string }) {
const [user] = createResource(
() => props.userId,
fetchUser
);
return (
<ErrorBoundary
fallback={(err) => (
<div>ユーザー情報の取得に失敗: {err.message}</div>
)}
>
<Suspense fallback={<p>読み込み中...</p>}>
<div>
<h2>{user()?.name}</h2>
</div>
</Suspense>
</ErrorBoundary>
);
}
このパターンにより、データ取得中のローディング状態とエラー状態の両方を適切に処理できます。
具体例
実践例 1:ユーザーダッシュボードの実装
ユーザーの認証状態、データ取得、エラーハンドリングを組み合わせた実践的な例を見てみましょう。
型定義
typescriptinterface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
interface DashboardData {
stats: {
views: number;
likes: number;
comments: number;
};
recentActivities: Activity[];
}
interface Activity {
id: string;
type: string;
timestamp: Date;
description: string;
}
データ取得関数
typescriptasync function fetchDashboardData(
userId: string
): Promise<DashboardData> {
const response = await fetch(`/api/dashboard/${userId}`);
if (!response.ok) {
throw new Error(
`Error ${response.status}: ${response.statusText}`
);
}
return response.json();
}
ダッシュボードコンポーネント
tsximport {
createSignal,
createResource,
Show,
For,
Switch,
Match,
ErrorBoundary,
} from 'solid-js';
function Dashboard() {
const [user, setUser] = createSignal<User | null>(null);
const [dashboardData] = createResource(
() => user()?.id,
fetchDashboardData
);
return (
<div class='dashboard'>
<Show
when={user()}
fallback={<LoginPrompt onLogin={setUser} />}
>
{(currentUser) => (
<ErrorBoundary
fallback={(err, reset) => (
<DashboardError error={err} onRetry={reset} />
)}
>
<DashboardHeader user={currentUser()} />
<DashboardContent
data={dashboardData()}
role={currentUser().role}
/>
</ErrorBoundary>
)}
</Show>
</div>
);
}
このコードでは、<Show>で認証チェック、<ErrorBoundary>でエラーハンドリングを行っています。
ダッシュボードコンテンツ
tsxfunction DashboardContent(props: {
data: DashboardData | undefined;
role: User['role'];
}) {
return (
<div class='content'>
<Show when={props.data}>
{(data) => (
<>
<StatsDisplay stats={data().stats} />
<Switch>
<Match when={props.role === 'admin'}>
<AdminPanel data={data()} />
</Match>
<Match when={props.role === 'user'}>
<UserPanel data={data()} />
</Match>
<Match when={props.role === 'guest'}>
<GuestPanel />
</Match>
</Switch>
<RecentActivities
activities={data().recentActivities}
/>
</>
)}
</Show>
</div>
);
}
<Switch>を使って、ユーザーの役割に応じて異なるパネルを表示しています。
最近のアクティビティ表示
tsxfunction RecentActivities(props: {
activities: Activity[];
}) {
return (
<section class='activities'>
<h3>最近のアクティビティ</h3>
<For
each={props.activities}
fallback={<p>アクティビティがありません</p>}
>
{(activity) => (
<div class='activity-item'>
<span class='activity-type'>
{activity.type}
</span>
<p>{activity.description}</p>
<time>
{new Date(
activity.timestamp
).toLocaleString()}
</time>
</div>
)}
</For>
</section>
);
}
<For>のfallbackにより、アクティビティが空の場合にも適切なメッセージを表示できます。
実践例 2:動的フォームビルダー
複数の Control Flow を組み合わせて、動的なフォームを構築する例です。
フォーム定義の型
typescripttype FieldType =
| 'text'
| 'number'
| 'email'
| 'select'
| 'checkbox';
interface FormField {
id: string;
type: FieldType;
label: string;
required: boolean;
options?: string[]; // select用
validation?: (value: any) => string | null;
}
interface FormConfig {
title: string;
fields: FormField[];
submitUrl: string;
}
フォームビルダーコンポーネント
tsximport { createSignal, For, Switch, Match, Show } from "solid-js";
function DynamicFormBuilder(props: { config: FormConfig }) {
const [formData, setFormData] = createSignal<Record<string, any>>({});
const [errors, setErrors] = createSignal<Record<string, string>>({});
const handleSubmit = async (e: Event) => {
e.preventDefault();
// バリデーション処理
const newErrors: Record<string, string> = {};
For each={props.config.fields}>
{(field) => {
const value = formData()[field.id];
if (field.required && !value) {
newErrors[field.id] = `${field.label}は必須です`;
} else if (field.validation && value) {
const error = field.validation(value);
if (error) newErrors[field.id] = error;
}
}}
</For>
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// 送信処理
await submitForm(props.config.submitUrl, formData());
};
return (
<form onSubmit={handleSubmit}>
<h2>{props.config.title}</h2>
<For each={props.config.fields}>
{(field) => (
<FormField
field={field}
value={formData()[field.id]}
error={errors()[field.id]}
onChange={(value) =>
setFormData({ ...formData(), [field.id]: value })
}
/>
)}
</For>
<button type="submit">送信</button>
</form>
);
}
個別フィールドコンポーネント
tsxfunction FormField(props: {
field: FormField;
value: any;
error?: string;
onChange: (value: any) => void;
}) {
return (
<div class='form-field'>
<label>
{props.field.label}
<Show when={props.field.required}>
<span class='required'>*</span>
</Show>
</label>
<Switch>
<Match
when={
props.field.type === 'text' ||
props.field.type === 'email'
}
>
<input
type={props.field.type}
value={props.value || ''}
onInput={(e) =>
props.onChange(e.currentTarget.value)
}
/>
</Match>
<Match when={props.field.type === 'number'}>
<input
type='number'
value={props.value || ''}
onInput={(e) =>
props.onChange(Number(e.currentTarget.value))
}
/>
</Match>
<Match when={props.field.type === 'select'}>
<select
value={props.value || ''}
onChange={(e) =>
props.onChange(e.currentTarget.value)
}
>
<option value=''>選択してください</option>
<For each={props.field.options || []}>
{(option) => (
<option value={option}>{option}</option>
)}
</For>
</select>
</Match>
<Match when={props.field.type === 'checkbox'}>
<input
type='checkbox'
checked={props.value || false}
onChange={(e) =>
props.onChange(e.currentTarget.checked)
}
/>
</Match>
</Switch>
<Show when={props.error}>
<span class='error-message'>{props.error}</span>
</Show>
</div>
);
}
このコードでは、<Switch>でフィールドタイプに応じた入力要素を動的に切り替えています。<Show>でエラーメッセージと必須マークの表示を制御しているのです。
フォーム送信処理
typescriptasync function submitForm(
url: string,
data: Record<string, any>
): Promise<void> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(
`Error ${response.status}: フォームの送信に失敗しました`
);
}
}
実践例 3:リアルタイム通知システム
WebSocket を使ったリアルタイム通知の実装例です。
通知の型定義
typescripttype NotificationType =
| 'info'
| 'success'
| 'warning'
| 'error';
interface Notification {
id: string;
type: NotificationType;
message: string;
timestamp: Date;
read: boolean;
}
通知コンポーネント
tsximport {
createSignal,
For,
Show,
Switch,
Match,
onCleanup,
} from 'solid-js';
function NotificationCenter() {
const [notifications, setNotifications] = createSignal<
Notification[]
>([]);
const [isOpen, setIsOpen] = createSignal(false);
// WebSocket接続
const ws = new WebSocket(
'ws://localhost:8080/notifications'
);
ws.onmessage = (event) => {
const notification: Notification = JSON.parse(
event.data
);
setNotifications([notification, ...notifications()]);
};
onCleanup(() => ws.close());
const unreadCount = () =>
notifications().filter((n) => !n.read).length;
const markAsRead = (id: string) => {
setNotifications(
notifications().map((n) =>
n.id === id ? { ...n, read: true } : n
)
);
};
return (
<div class='notification-center'>
<button
class='notification-button'
onClick={() => setIsOpen(!isOpen())}
>
通知
<Show when={unreadCount() > 0}>
<span class='badge'>{unreadCount()}</span>
</Show>
</button>
<Show when={isOpen()}>
<div class='notification-panel'>
<For
each={notifications()}
fallback={<p class='empty'>通知はありません</p>}
>
{(notification) => (
<NotificationItem
notification={notification}
onRead={() => markAsRead(notification.id)}
/>
)}
</For>
</div>
</Show>
</div>
);
}
個別通知アイテム
tsxfunction NotificationItem(props: {
notification: Notification;
onRead: () => void;
}) {
const getIcon = () => {
switch (props.notification.type) {
case 'info':
return 'ℹ️';
case 'success':
return '✓';
case 'warning':
return '⚠️';
case 'error':
return '✗';
}
};
return (
<div
class={`notification-item ${props.notification.type}`}
classList={{ unread: !props.notification.read }}
onClick={props.onRead}
>
<Switch>
<Match when={props.notification.type === 'info'}>
<div class='notification-content info-style'>
<span class='icon'>{getIcon()}</span>
<p>{props.notification.message}</p>
</div>
</Match>
<Match when={props.notification.type === 'success'}>
<div class='notification-content success-style'>
<span class='icon'>{getIcon()}</span>
<p>{props.notification.message}</p>
</div>
</Match>
<Match when={props.notification.type === 'warning'}>
<div class='notification-content warning-style'>
<span class='icon'>{getIcon()}</span>
<p>{props.notification.message}</p>
</div>
</Match>
<Match when={props.notification.type === 'error'}>
<div class='notification-content error-style'>
<span class='icon'>{getIcon()}</span>
<p>{props.notification.message}</p>
</div>
</Match>
</Switch>
<time class='timestamp'>
{formatTimestamp(props.notification.timestamp)}
</time>
<Show when={!props.notification.read}>
<span class='unread-indicator'>●</span>
</Show>
</div>
);
}
タイムスタンプのフォーマット
typescriptfunction formatTimestamp(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'たった今';
if (minutes < 60) return `${minutes}分前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}時間前`;
const days = Math.floor(hours / 24);
return `${days}日前`;
}
この実装では、<For>で通知リストを表示し、<Switch>で通知タイプに応じたスタイリングを行い、<Show>で未読バッジを制御しています。
以下の図は、通知システムのデータフローを示しています。
mermaidsequenceDiagram
participant WS as WebSocket Server
participant NC as NotificationCenter
participant UI as UI Component
participant User as ユーザー
WS->>NC: 新規通知を送信
NC->>NC: notifications() を更新
NC->>UI: For コンポーネントが再描画
UI->>User: 通知を表示 & バッジ更新
User->>UI: 通知をクリック
UI->>NC: markAsRead() 実行
NC->>NC: read フラグを更新
NC->>UI: Show コンポーネントが再描画
UI->>User: バッジが更新される
まとめ
SolidJS の Control Flow コンポーネントは、リアクティブシステムと統合された強力な制御フローを提供します。以下のポイントを押さえることで、効率的で保守性の高いコードが書けるでしょう。
各コンポーネントの使い分け
| # | コンポーネント | 用途 | 主な特徴 |
|---|---|---|---|
| 1 | Show | 単純な条件分岐 | 型絞り込み、fallback、keyed オプション |
| 2 | For | オブジェクトの配列 | 参照による追跡、高速な差分更新 |
| 3 | Index | プリミティブの配列 | インデックスベースの追跡 |
| 4 | Switch/Match | 複数条件の分岐 | 最初の真の条件のみ評価 |
| 5 | ErrorBoundary | エラーハンドリング | エラーの局所化、リセット機能 |
パフォーマンスのベストプラクティス
Control Flow コンポーネントを使うことで、以下のパフォーマンス最適化が自動的に適用されます。
- 最小限の再レンダリング: 変更があった部分だけを更新
- 効率的な差分検出:
<For>は参照ベースで変更を検知 - メモリ効率: 不要になった DOM ノードを適切に破棄
TypeScript との相性
SolidJS の Control Flow コンポーネントは、TypeScript の型システムと相性が良く、以下のメリットがあります。
- 型絞り込み:
<Show>の子要素内で型が自動的に絞り込まれる - 型安全な分岐:
<Switch>と判別可能なユニオン型の組み合わせ - コンパイル時のエラー検出: 型の不一致を事前に発見できる
エラーハンドリングの重要性
<ErrorBoundary>を適切に配置することで、堅牢なアプリケーションを構築できます。以下のポイントを意識しましょう。
- 階層的な配置: アプリ全体、セクション、コンポーネント単位で配置
- リセット機能: ユーザーがエラーから回復できる手段を提供
- 非同期処理: Suspense と組み合わせて非同期エラーもキャッチ
SolidJS の Control Flow コンポーネントをマスターすることで、React や Vue とは異なる、SolidJS 独自の開発スタイルが身に付きます。最初は慣れないかもしれませんが、リアクティビティの仕組みを理解すれば、より効率的で保守性の高いコードが書けるようになるでしょう。
この記事で紹介したパターンを実際のプロジェクトで試してみて、SolidJS の真価を体験してみてください。
関連リンク
articleSolidJS の Control Flow コンポーネント大全:Show/For/Switch/ErrorBoundary を使い分け
articleSolidJS 本番運用チェックリスト:CSP・SRI・Preload・エラーレポートの総点検
articleSolidJS クリーンアーキテクチャ実践:UI・状態・副作用を厳密に分離する
articleSolidJS フック相当 API 速見表:createSignal/createMemo/createEffect… 一覧
articleSolidJS を macOS + yarn で最速構築:ESLint・Prettier・TSconfig の鉄板レシピ
articleSolidJS × TanStack Query vs createResource:データ取得手段の実測比較
articleSvelte のコンパイル出力を読み解く:仮想 DOM なしで速い理由
articleTauri で Markdown エディタを作る:ライブプレビュー・拡張プラグイン対応
articleStorybook で“仕様が生きる”開発:ドキュメント駆動 UI の実践ロードマップ
articleshadcn/ui で B2B SaaS ダッシュボードを組む:権限別 UI と監査ログの見せ方
articleSolidJS の Control Flow コンポーネント大全:Show/For/Switch/ErrorBoundary を使い分け
articleRemix で管理画面テンプレ:表・フィルタ・CSV エクスポートの鉄板構成
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来