T-CREATOR

SolidJS の Control Flow コンポーネント大全:Show/For/Switch/ErrorBoundary を使い分け

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 リソースの無駄遣い
2DOM 操作の増加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>
  );
}

このコードでは、isLoggedIntrueのときだけ、挨拶メッセージが表示されます。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 の違い

#特徴ForIndex
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 コンポーネントは、リアクティブシステムと統合された強力な制御フローを提供します。以下のポイントを押さえることで、効率的で保守性の高いコードが書けるでしょう。

各コンポーネントの使い分け

#コンポーネント用途主な特徴
1Show単純な条件分岐型絞り込み、fallback、keyed オプション
2Forオブジェクトの配列参照による追跡、高速な差分更新
3Indexプリミティブの配列インデックスベースの追跡
4Switch/Match複数条件の分岐最初の真の条件のみ評価
5ErrorBoundaryエラーハンドリングエラーの局所化、リセット機能

パフォーマンスのベストプラクティス

Control Flow コンポーネントを使うことで、以下のパフォーマンス最適化が自動的に適用されます。

  • 最小限の再レンダリング: 変更があった部分だけを更新
  • 効率的な差分検出: <For>は参照ベースで変更を検知
  • メモリ効率: 不要になった DOM ノードを適切に破棄

TypeScript との相性

SolidJS の Control Flow コンポーネントは、TypeScript の型システムと相性が良く、以下のメリットがあります。

  • 型絞り込み: <Show>の子要素内で型が自動的に絞り込まれる
  • 型安全な分岐: <Switch>と判別可能なユニオン型の組み合わせ
  • コンパイル時のエラー検出: 型の不一致を事前に発見できる

エラーハンドリングの重要性

<ErrorBoundary>を適切に配置することで、堅牢なアプリケーションを構築できます。以下のポイントを意識しましょう。

  • 階層的な配置: アプリ全体、セクション、コンポーネント単位で配置
  • リセット機能: ユーザーがエラーから回復できる手段を提供
  • 非同期処理: Suspense と組み合わせて非同期エラーもキャッチ

SolidJS の Control Flow コンポーネントをマスターすることで、React や Vue とは異なる、SolidJS 独自の開発スタイルが身に付きます。最初は慣れないかもしれませんが、リアクティビティの仕組みを理解すれば、より効率的で保守性の高いコードが書けるようになるでしょう。

この記事で紹介したパターンを実際のプロジェクトで試してみて、SolidJS の真価を体験してみてください。

関連リンク