SolidJS のサスペンス&非同期 UI 制御

モダンなWebアプリケーション開発において、非同期処理は欠かすことのできない要素となっています。ユーザーがサイトを訪れた瞬間から、データの取得、API通信、リアルタイム更新など、多くの処理が裏側で実行されているのです。
SolidJSは、このような非同期処理を効率的かつ直感的に扱うために、強力なサスペンス機能を提供しています。従来のフレームワークでは複雑になりがちなローディング状態の管理やエラーハンドリングを、SolidJSではシンプルで読みやすいコードで実現できるのです。
本記事では、SolidJSにおけるサスペンスの仕組みから実践的な活用方法まで、詳しく解説いたします。
背景
従来の非同期 UI 制御の課題
従来のWebアプリケーション開発では、非同期処理による UI 制御に多くの課題がありました。データの取得中にローディング画面を表示し、エラーが発生した場合は適切なメッセージを表示する。一見シンプルに思えるこの処理が、実際のコードでは予想以上に複雑になってしまうのです。
以下のような図で、従来の問題点を整理してみましょう。
mermaidflowchart TD
start[コンポーネント開始] --> loading[loading状態を管理]
loading --> api[API呼び出し]
api --> success{成功?}
success -->|Yes| data[データを状態に保存]
success -->|No| error[エラー状態を設定]
data --> render[UIレンダリング]
error --> errorUI[エラーUI表示]
loading --> loadingUI[ローディングUI表示]
このフローからわかるように、開発者は手動で複数の状態を管理し、それぞれに対応するUIを用意する必要がありました。
SolidJS のリアクティブシステムの特徴
SolidJSは、ファイングレインドリアクティビティという独自のアプローチを採用しています。これは、値の変更を検知したときに、その値に依存する部分のみを効率的に更新するシステムです。
この仕組みにより、SolidJSでは以下のような利点が生まれます。
- 高いパフォーマンス: 必要な部分のみが更新されるため、無駄な再レンダリングが発生しません
- シンプルな状態管理: Signal を使った直感的な状態管理が可能です
- 予測可能な動作: データの流れが明確で、デバッグしやすいコードを書けます
サスペンスが解決する問題
SolidJSのサスペンス機能は、非同期処理における複雑な状態管理を大幅に簡素化します。開発者が手動で管理していたローディング状態、エラー状態、データ状態を、フレームワーク側が自動的に処理してくれるのです。
サスペンスによる問題解決を図で表現すると、次のようになります。
mermaidflowchart LR
component[コンポーネント] --> suspense[Suspense境界]
suspense --> resource[createResource]
resource --> api[API呼び出し]
api --> auto[自動状態管理]
auto --> ui[適切なUI表示]
この仕組みにより、開発者はビジネスロジックに集中でき、UIの状態管理はフレームワークに任せることができます。
課題
非同期データ読み込み中のローディング状態管理
Webアプリケーションにおいて、データの読み込み中にユーザーに適切なフィードバックを提供することは、良いユーザーエクスペリエンスを実現するために不可欠です。しかし、従来のアプローチでは、以下のような課題がありました。
手動状態管理の複雑性
開発者は、loading
、error
、data
といった複数の状態を同時に管理する必要がありました。これらの状態は相互に関連しており、一つの状態が変わると他の状態も適切に更新する必要があります。
typescript// 従来のアプローチでの状態管理例
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal(null);
const [data, setData] = createSignal(null);
// データ取得処理
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const result = await api.getData();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
この例からわかるように、シンプルなデータ取得でも多くのコードが必要になってしまいます。
状態の不整合リスク
複数の状態を手動で管理する場合、状態間の不整合が発生するリスクがあります。例えば、loading
が false
になったにも関わらず、data
と error
の両方が設定されてしまうような状況です。
エラーハンドリングの複雑性
非同期処理では、ネットワークエラー、APIエラー、タイムアウトなど、様々な種類のエラーが発生する可能性があります。これらのエラーを適切に処理し、ユーザーにわかりやすいメッセージを表示することは、従来のアプローチでは困難でした。
エラーの種類と対応
エラーの種類 | 発生要因 | 対応方法 |
---|---|---|
ネットワークエラー | 接続障害、タイムアウト | 再試行ボタンの表示 |
APIエラー | サーバー側のエラー | エラーメッセージの表示 |
認証エラー | トークンの期限切れ | ログイン画面への遷移 |
データ形式エラー | 予期しないレスポンス | 開発者向けログ出力 |
これらの多様なエラーに対して、それぞれ異なる UI と処理ロジックを実装する必要がありました。
ユーザーエクスペリエンスの向上
優れたユーザーエクスペリエンスを提供するためには、以下の要素が重要です。
即座のフィードバック
ユーザーがアクションを起こした瞬間から、何らかのフィードバックを提供する必要があります。データの読み込みが始まったことを示すローディングアニメーション、進捗状況の表示など、ユーザーが待っていることを明確に伝えることが大切です。
段階的な情報表示
大量のデータを一度に表示するのではなく、段階的に情報を表示することで、ユーザーの認知負荷を軽減できます。しかし、この段階的な表示を実装するには、複雑な状態管理が必要になります。
解決策
Suspense コンポーネントの仕組み
SolidJSのSuspenseコンポーネントは、非同期処理を宣言的に扱うための強力な仕組みです。従来の命令的なアプローチとは異なり、「何を表示するか」に焦点を当てた書き方ができます。
Suspenseの基本的な動作原理を図で示します。
mermaidstateDiagram-v2
[*] --> Pending: リソース読み込み開始
Pending --> Ready: データ取得完了
Pending --> Error: エラー発生
Ready --> Pending: データ再取得
Error --> Pending: リトライ
Error --> Ready: 直接データ取得成功
Suspenseコンポーネントは、子コンポーネントで非同期処理が実行されている間、フォールバックUIを自動的に表示します。
typescriptimport { Suspense } from "solid-js";
// 基本的なSuspenseの使用例
function App() {
return (
<Suspense fallback={<div>読み込み中...</div>}>
<DataComponent />
</Suspense>
);
}
この例では、DataComponent
で非同期処理が実行されている間、「読み込み中...」のメッセージが表示されます。
Suspenseの特徴
- 自動的な状態管理: ローディング状態を手動で管理する必要がありません
- 宣言的な記述: UIの構造が明確で理解しやすいコードになります
- ネスト可能: 複数のSuspense境界を組み合わせて、細かい制御が可能です
createResource による非同期データ管理
createResource
は、SolidJSで非同期データを扱うための専用の API です。この関数を使用することで、非同期処理の状態管理が大幅に簡素化されます。
typescriptimport { createResource } from "solid-js";
// ユーザーデータを取得するリソースの作成
const [userData] = createResource(async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('ユーザーデータの取得に失敗しました');
}
return response.json();
});
createResource
の第一引数には、非同期処理を行う関数を渡します。この関数が Promise を返すと、SolidJSが自動的に状態を管理してくれます。
createResourceの返り値
createResource
は、以下の要素を含む配列を返します。
typescript// リソースの基本的な使用方法
const [data, { mutate, refetch }] = createResource(fetchFunction);
// dataの状態確認
function DataDisplay() {
return (
<div>
{data.loading && <p>読み込み中...</p>}
{data.error && <p>エラー: {data.error.message}</p>}
{data() && <p>データ: {JSON.stringify(data())}</p>}
</div>
);
}
パラメータ付きリソース
createResource
は、外部の値に依存するリソースも簡単に作成できます。
typescriptimport { createSignal, createResource } from "solid-js";
// ユーザーIDに基づいてデータを取得
const [userId, setUserId] = createSignal(1);
const [userProfile] = createResource(userId, async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
この例では、userId
の値が変更されるたびに、新しいデータが自動的に取得されます。
ErrorBoundary との組み合わせ
非同期処理では、エラーハンドリングが重要な要素です。SolidJSでは、ErrorBoundary
コンポーネントを使用して、エラーを適切に処理できます。
typescriptimport { ErrorBoundary } from "solid-js";
// エラーハンドリングを含む完全な例
function App() {
return (
<ErrorBoundary fallback={(error) =>
<div>エラーが発生しました: {error.message}</div>
}>
<Suspense fallback={<div>読み込み中...</div>}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}
エラーバウンダリの階層構造
エラーバウンダリとサスペンスを組み合わせることで、堅牢なアプリケーションを構築できます。
mermaidflowchart TD
errorBoundary[ErrorBoundary] --> suspense[Suspense]
suspense --> component[DataComponent]
component --> error{エラー発生?}
error -->|Yes| catch[ErrorBoundaryがキャッチ]
error -->|No| data[データ表示]
catch --> errorUI[エラーUI表示]
この構造により、エラーが発生した場合でもアプリケーション全体がクラッシュすることを防げます。
カスタムエラーハンドリング
より詳細なエラー処理が必要な場合は、カスタムのエラーハンドラーを実装できます。
typescript// カスタムエラーハンドラーの実装
function CustomErrorBoundary(props) {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div class="error-container">
<h2>何らかのエラーが発生しました</h2>
<p>{error.message}</p>
<button onClick={reset}>再試行</button>
</div>
)}
>
{props.children}
</ErrorBoundary>
);
}
このカスタムエラーバウンダリでは、エラーメッセージの表示と再試行機能を提供しています。
具体例
基本的なサスペンス実装
まずは、シンプルなデータ取得を例にして、SolidJSでのサスペンス実装を見てみましょう。
ユーザー一覧を表示するコンポーネントを作成します。まず、必要なインポートから始めます。
typescriptimport { createResource, Suspense, ErrorBoundary, For } from "solid-js";
import { Component } from "solid-js";
次に、ユーザーデータを取得する関数を定義します。
typescript// ユーザーデータを取得するAPI関数
async function fetchUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
// エラーハンドリング
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json();
}
リソースを作成し、コンポーネント内で使用します。
typescript// ユーザー一覧コンポーネント
const UserList: Component = () => {
// createResourceでデータを管理
const [users] = createResource(fetchUsers);
return (
<div class="user-list">
<h2>ユーザー一覧</h2>
<For each={users()}>
{(user) => (
<div class="user-card">
<h3>{user.name}</h3>
<p>Email: {user.email}</p>
<p>Company: {user.company.name}</p>
</div>
)}
</For>
</div>
);
};
最後に、SuspenseとErrorBoundaryでラップしたメインコンポーネントを作成します。
typescript// メインアプリケーションコンポーネント
const App: Component = () => {
return (
<div class="app">
<ErrorBoundary
fallback={(error) => (
<div class="error-message">
<h2>データの取得に失敗しました</h2>
<p>{error.message}</p>
<button onClick={() => window.location.reload()}>
ページを再読み込み
</button>
</div>
)}
>
<Suspense
fallback={
<div class="loading">
<p>ユーザーデータを読み込み中...</p>
<div class="spinner"></div>
</div>
}
>
<UserList />
</Suspense>
</ErrorBoundary>
</div>
);
};
この実装により、以下の流れが自動的に処理されます。
- コンポーネントがマウントされると、
fetchUsers
が実行される - データ取得中は「読み込み中...」メッセージが表示される
- データ取得が完了すると、ユーザー一覧が表示される
- エラーが発生した場合は、エラーメッセージと再読み込みボタンが表示される
複数のリソースを扱う場合
実際のアプリケーションでは、複数の異なるデータソースから情報を取得することがよくあります。SolidJSでは、複数のリソースを効率的に管理できます。
ユーザー情報と投稿一覧を同時に取得する例を見てみましょう。
typescriptimport { createResource, createSignal, Suspense } from "solid-js";
// ユーザー詳細ページのコンポーネント
const UserDetailPage: Component = () => {
// 現在表示するユーザーのID
const [userId, setUserId] = createSignal(1);
// ユーザー情報のリソース
const [userInfo] = createResource(userId, async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
});
// ユーザーの投稿一覧のリソース
const [userPosts] = createResource(userId, async (id) => {
const response = await fetch(`/api/users/${id}/posts`);
return response.json();
});
return (
<div class="user-detail">
<UserSelector userId={userId()} onUserChange={setUserId} />
{/* ユーザー情報セクション */}
<Suspense fallback={<div>ユーザー情報を読み込み中...</div>}>
<UserInfoSection user={userInfo()} />
</Suspense>
{/* 投稿一覧セクション */}
<Suspense fallback={<div>投稿を読み込み中...</div>}>
<UserPostsSection posts={userPosts()} />
</Suspense>
</div>
);
};
ユーザー情報表示コンポーネントを定義します。
typescript// ユーザー情報表示コンポーネント
const UserInfoSection: Component<{user: any}> = (props) => {
return (
<div class="user-info">
<h2>{props.user.name}</h2>
<div class="user-details">
<p><strong>Email:</strong> {props.user.email}</p>
<p><strong>Phone:</strong> {props.user.phone}</p>
<p><strong>Website:</strong> {props.user.website}</p>
<div class="address">
<h3>住所</h3>
<p>{props.user.address.street}, {props.user.address.city}</p>
</div>
</div>
</div>
);
};
投稿一覧表示コンポーネントを定義します。
typescript// 投稿一覧表示コンポーネント
const UserPostsSection: Component<{posts: any[]}> = (props) => {
return (
<div class="user-posts">
<h3>投稿一覧</h3>
<div class="posts-grid">
<For each={props.posts}>
{(post) => (
<article class="post-card">
<h4>{post.title}</h4>
<p>{post.body}</p>
</article>
)}
</For>
</div>
</div>
);
};
この実装では、ユーザー情報と投稿一覧が独立して読み込まれます。一方のデータ取得が完了すれば、そちらの UI が先に表示され、もう一方のデータは引き続き読み込み状態を維持します。
ネストしたサスペンス構造
より複雑なアプリケーションでは、サスペンス境界をネストして、きめ細かい制御を行うことができます。
ダッシュボード画面を例に、段階的なデータ読み込みを実装してみましょう。
mermaidflowchart TD
dashboard[ダッシュボード] --> userInfo[ユーザー情報]
dashboard --> stats[統計情報]
dashboard --> notifications[通知]
userInfo --> profile[プロフィール]
userInfo --> settings[設定]
stats --> sales[売上データ]
stats --> analytics[分析データ]
この構造に対応するコンポーネント実装は以下のようになります。
typescript// メインダッシュボードコンポーネント
const Dashboard: Component = () => {
return (
<div class="dashboard">
<header class="dashboard-header">
<h1>ダッシュボード</h1>
</header>
<div class="dashboard-content">
{/* 最優先で表示するユーザー情報 */}
<Suspense fallback={<UserInfoSkeleton />}>
<UserInfoWidget />
</Suspense>
{/* 統計情報セクション */}
<div class="stats-section">
<Suspense fallback={<StatsSkeleton />}>
<StatsWidget />
</Suspense>
</div>
{/* 通知セクション */}
<aside class="notifications-section">
<Suspense fallback={<NotificationsSkeleton />}>
<NotificationsWidget />
</Suspense>
</aside>
</div>
</div>
);
};
統計情報ウィジェットでは、さらに詳細なネスト構造を実装します。
typescript// 統計情報ウィジェット(ネストしたサスペンス)
const StatsWidget: Component = () => {
return (
<div class="stats-widget">
<h2>統計情報</h2>
<div class="stats-grid">
{/* 売上データ */}
<div class="stats-card">
<Suspense fallback={<div class="loading-card">売上データ読み込み中...</div>}>
<SalesChart />
</Suspense>
</div>
{/* ユーザー分析 */}
<div class="stats-card">
<Suspense fallback={<div class="loading-card">分析データ読み込み中...</div>}>
<UserAnalytics />
</Suspense>
</div>
{/* パフォーマンス指標 */}
<div class="stats-card">
<Suspense fallback={<div class="loading-card">指標データ読み込み中...</div>}>
<PerformanceMetrics />
</Suspense>
</div>
</div>
</div>
);
};
各チャートコンポーネントは、独自のデータソースを持ちます。
typescript// 売上チャートコンポーネント
const SalesChart: Component = () => {
const [salesData] = createResource(async () => {
// 売上データの取得(重い処理)
const response = await fetch('/api/analytics/sales?period=30d');
return response.json();
});
return (
<div class="sales-chart">
<h3>売上推移(30日間)</h3>
<ChartComponent data={salesData()} />
</div>
);
};
// ユーザー分析コンポーネント
const UserAnalytics: Component = () => {
const [analyticsData] = createResource(async () => {
// 分析データの取得
const response = await fetch('/api/analytics/users');
return response.json();
});
return (
<div class="user-analytics">
<h3>ユーザー分析</h3>
<AnalyticsChart data={analyticsData()} />
</div>
);
};
このネスト構造により、以下のような利点が得られます。
段階的な表示
- ユーザー情報が最初に表示される
- 統計情報の各カードが順次表示される
- 重いデータ処理があっても、他の部分は正常に動作する
細かいフィードバック
- 各セクションごとに適切なローディング表示
- エラーが発生しても影響範囲を限定
- ユーザーは利用可能な情報からすぐに作業を開始できる
まとめ
SolidJS サスペンスの利点
SolidJSのサスペンス機能は、現代的なWebアプリケーション開発において多くの利点をもたらします。最も重要な利点は、開発者の負担軽減です。従来は手動で管理していた複雑な非同期状態を、フレームワークが自動的に処理してくれるため、開発者はビジネスロジックに集中できます。
パフォーマンスの向上も見逃せない利点です。SolidJSのファイングレインドリアクティビティにより、必要な部分のみが更新されるため、大規模なアプリケーションでも高いパフォーマンスを維持できます。また、複数のリソースを並列で取得できるため、ユーザーは最初に利用可能になったデータからすぐに作業を開始できるのです。
宣言的なコード記述により、コードの可読性と保守性が大幅に向上します。UIの構造が明確になり、新しいチームメンバーでも理解しやすいコードベースを構築できます。
エラーハンドリングの統一も重要な利点です。ErrorBoundaryとの組み合わせにより、アプリケーション全体で一貫したエラー処理を実現できます。
実装時の注意点
SolidJSサスペンスを効果的に活用するために、以下の点にご注意ください。
適切な粒度でのサスペンス境界設定
サスペンス境界を細かく設定しすぎると、ローディング表示が頻繁に切り替わり、ユーザーエクスペリエンスが悪化する可能性があります。一方で、境界が大きすぎると、一部のデータ取得が遅れただけで全体の表示が遅くなってしまいます。
ユーザーの利用パターンを考慮して、適切な境界を設定することが重要です。
エラーハンドリングの階層化
ErrorBoundaryは階層的に配置し、エラーの種類に応じて適切なレベルで処理することを推奨します。ページレベル、セクションレベル、コンポーネントレベルでそれぞれ異なる対応を用意することで、ユーザーに分かりやすいエラー表示を提供できます。
リソースの依存関係管理
複数のリソースが相互に依存している場合は、依存関係を明確に定義することが重要です。createResource
の第一引数に依存する値を指定することで、適切なタイミングでデータが再取得されます。
メモリリークの防止
長時間実行されるアプリケーションでは、不要になったリソースを適切にクリーンアップすることが重要です。コンポーネントがアンマウントされる際に、実行中の非同期処理をキャンセルする仕組みを検討してください。
SolidJSのサスペンス機能を適切に活用することで、ユーザーにとって快適で、開発者にとって保守しやすいアプリケーションを構築できます。本記事で紹介した実装例を参考に、皆様のプロジェクトでもぜひ活用してみてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来