fetch-and-send-data について
Blazor 開発者向けに、データ取得・送信やコンポーネントの読み込み処理をまとめたスキルガイドです。レンダリングモード別のデータアクセスに加え、キャンセル、エラー、パラメーター変更時の処理方法を扱います。
主な機能
- 01レンダリングモード別のデータアクセス選択
- 02名前付き・型付き HttpClient の設定
- 03静的 SSR とプリレンダリングへの対応
- 04ErrorBoundary によるエラー管理
- 05パラメーター変更時のキャンセルと再読み込み
- 06HTTP によるデータ送信
活用シーン
- 01Blazor ページで外部 API のデータを読み込む
- 02静的 SSR ページにストリーミング読み込みを設定する
- 03ルート変更後の重複リクエストに対処する
- 04フォーム送信時の二重操作を防ぐ
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill fetch-and-send-dataインストール後、Claude Code で/fetch-and-send-dataを入力して読み込まれたか確認します。
詳細
データの取得と送信
ステップ 1 — AGENTS.md を読む
インタラクティビティモードとスコープを確認します。
| モード | データアクセス |
|---|---|
| なし(静的 SSR) | サーバー側:サービス/DbContext を注入します。読み込み中の UX には [StreamRendering] を使用します。 |
| Server | サーバー側:サービス/DbContext を注入します。??= と [PersistentState] を使ってプリレンダリングに対応します。 |
| WebAssembly | ブラウザー側:HttpClient のみ使用します。サーバーへの直接アクセスはできません。 |
| Auto | サーバーとブラウザーの両方。必ず API を経由します。 |
ステップ 2 — HttpClient を登録する
Server から外部 API を呼び出す場合、または WebAssembly/Auto を使用する場合に必要です。自分のデータベースにアクセスするサーバーコンポーネントでは、DbContext またはサービスを直接注入してください。
// Named client — requires Microsoft.Extensions.Http NuGet
builder.Services.AddHttpClient("CatalogAPI", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
});
// Typed client
builder.Services.AddHttpClient<CatalogClient>(client =>
client.BaseAddress = new Uri("https://api.example.com/"));
プリレンダリングを行う WebAssembly/Auto では、サーバーと .Client の Program.cs の両方に登録してください。
ステップ 3 — データを取得する
シンプルな読み込み
@page "/products"
@inject CatalogClient Catalog
@if (products is null)
{
<p>Loading…</p>
}
else
{
@foreach (var p in products)
{
<p>@p.Name — @p.Price.ToString("C")</p>
}
}
@code {
private Product[]? products;
protected override async Task OnInitializedAsync()
{
products = await Catalog.GetProductsAsync();
}
}
最もシンプルなケースでは、エラー処理は必要ありません。親またはレイアウトレベルでコンポーネントの使用箇所を <ErrorBoundary> で囲み、未処理の例外を捕捉してください。
静的 SSR — StreamRendering
[StreamRendering] がない場合、OnInitializedAsync が完了するまでユーザーには何も表示されません。
@attribute [StreamRendering]
これは静的 SSR にのみ適用されます。インタラクティブコンポーネントには影響しません。
プリレンダリングへの対応
プリレンダリングでは OnInitializedAsync が 2 回呼び出されます。重複を避けるには、次のようにします。
[PersistentState] private Product[]? products;
protected override async Task OnInitializedAsync()
{
products ??= await Catalog.GetProductsAsync();
}
詳細は support-prerendering スキルを参照してください。
ステップ 4 — エラーを処理する
デフォルトのエラー戦略として <ErrorBoundary> を使用します。コンポーネントごとに catch 処理を追加しなくても、すべてのコンポーネントで一貫したエラー体験を実現できます。レイアウトまたは親レベルでコンポーネントの使用箇所を囲んでください。
<ErrorBoundary>
<ChildContent>
<ProductList />
</ChildContent>
<ErrorContent>
<div class="alert alert-danger">Something went wrong. Please refresh.</div>
</ErrorContent>
</ErrorBoundary>
HttpRequestException などのキャンセル以外の例外は、自動的に ErrorBoundary に伝播するため、コンポーネントに catch ブロックを追加する必要はありません。
キャンセルは例外です
ComponentBase は、自身によるキャンセル(破棄、パラメーターの変更)と外部によるキャンセル(HttpClient のタイムアウト)の両方について、すべての OperationCanceledException を暗黙的に無視します。そのため、ErrorBoundary には通知されません。つまり、次のようになります。
- 自身によるキャンセル → 暗黙的に無視されます。これは正しい動作なので、対応は不要です。
- 外部によるキャンセル(タイムアウト) → こちらも暗黙的に無視されます。コンポーネントは読み込み中の状態で止まりますが、タイムアウトはまれなため、通常は問題ありません。
コンポーネント内にエラー処理を追加する場合
コンポーネントで ErrorBoundary では対応できない動作が必要な場合にのみ、catch ブロックを追加します。通常は再試行やタイムアウト専用メッセージが該当します。その場合も、必要なものだけを捕捉してください。
// Catch only external cancellation (timeouts) — everything else flows to ErrorBoundary
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "Request timed out for category {CategoryId}", CategoryId);
error = "The request timed out. Please try again.";
}
ErrorBoundary に処理を任せるのではなく、コンポーネント内で一般的なエラーを処理し、再試行ボタンを表示する必要がある場合は、次のようにします。
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "Request timed out for category {CategoryId}", CategoryId);
error = "The request timed out. Please try again.";
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to load products for category {CategoryId}", CategoryId);
error = "Unable to load products. Please try again.";
}
ルール
exception.Messageを表示しないでください — 個人情報、接続文字列、または内部情報が含まれる可能性があります。ユーザーにわかりやすいメッセージを固定で使用してください。- 必ず
ILoggerを通じてログを記録してください — 実際の例外はログのパイプラインに送られます。 - サービスでは
CancellationTokenを受け取るようにしてください — すべての非同期呼び出しに渡し、コンポーネントのキャンセル時に処理を停止できるようにします。
ステップ 5 — パラメーターに応じて再読み込みする
データが、変化するルートパラメーターまたはクエリパラメーター(例:/products/1 と /products/2 の間の移動)に依存する場合は、OnParametersSetAsync を使用します。データに影響しないパラメーターでは再読み込みをスキップするため、ガードを追加してください。
パターン:キャンセルして再読み込みし、古いデータを重ねて表示する
@page "/products/{CategoryId:int}"
@implements IAsyncDisposable
@inject ProductService ProductService
@inject ILogger<Products> Logger
@if (error is not null)
{
<div class="alert alert-danger">
<p>@error</p>
<button @onclick="LoadAsync">Retry</button>
</div>
}
else if (products is null)
{
<p>Loading…</p>
}
else
{
@if (isLoading)
{
<p><em>Refreshing…</em></p>
}
@foreach (var p in products)
{
<p>@p.Name — @p.Price.ToString("C")</p>
}
}
@code {
[Parameter] public int CategoryId { get; set; }
[SupplyParameterFromQuery] public string? ViewMode { get; set; } // UI-only
private CancellationTokenSource? cts;
private int? loadedCategoryId;
private List<Product>? products;
private bool isLoading;
private string? error;
protected override async Task OnParametersSetAsync()
{
if (CategoryId == loadedCategoryId)
{
return; // Only ViewMode changed — no reload
}
loadedCategoryId = CategoryId;
await LoadAsync();
}
private async Task LoadAsync()
{
if (cts is not null)
{
await cts.CancelAsync();
cts.Dispose();
}
cts = new CancellationTokenSource();
var cancellationToken = cts.Token; // Capture locally before await
error = null;
isLoading = true;
try
{
var result = await ProductService.GetByCategoryAsync(CategoryId, cancellationToken);
products = result;
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
Logger.LogWarning(ex, "Timed out loading category {CategoryId}", CategoryId);
error = "The request timed out. Please try again.";
}
finally
{
isLoading = false;
}
}
public async ValueTask DisposeAsync()
{
if (cts is not null)
{
await cts.CancelAsync();
cts.Dispose();
}
}
}
主なポイント:
- 追跡値でガードする:
loadedCategoryIdにより、UI パラメーターのみが変更された場合の再読み込みをスキップします。 - await の前にトークンをローカル変数に保存する — 並行するパラメーター変更によって CTS フィールドが置き換わる可能性があります。
- 後続の読み込みでは
productsを null にしない —isLoadingオーバーレイを表示し、既存データを表示したままにします。 IAsyncDisposableにより、ユーザーが画面を離れるときに保留中の処理をキャンセルします。
ステップ 6 — データを送信する
var response = await http.PostAsJsonAsync("products", newProduct);
response.EnsureSuccessStatusCode();
var response = await http.PutAsJsonAsync($"products/{id}", updated);
response.EnsureSuccessStatusCode();
var response = await http.DeleteAsync($"products/{id}");
response.EnsureSuccessStatusCode();
重複リクエストを防ぐため、保存中は送信ボタンを無効にします。保存中のインジケーターを表示してください。
ステップ 7 — プリレンダリングを使用する Auto モードまたは WebAssembly 向けのサービス抽象化
コンポーネントがサーバーとブラウザーの両方で実行される場合(Auto モード、またはプリレンダリングを使用する WebAssembly)は、抽象基底クラスを通じてデータアクセスを抽象化します。
public abstract class ProductServiceBase
{
public abstract Task<Product[]> GetAllAsync(CancellationToken ct = default);
}
// Server — direct database access
public class ServerProductService(AppDbContext db) : ProductServiceBase
{
public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
await db.Products.ToArrayAsync(ct);
}
// Client — calls API
public class ClientProductService(HttpClient http) : ProductServiceBase
{
public override async Task<Product[]> GetAllAsync(CancellationToken ct = default) =>
await http.GetFromJsonAsync<Product[]>("api/products", ct) ?? [];
}
各プロジェクトの Program.cs で、適切な実装を登録します。コンポーネントでは抽象基底クラスをインジェクションします。
やってはいけないこと
- コンストラクターで API を呼び出さないでください —
OnInitializedAsyncを使用します。 - データが変更されるパラメーターに依存する場合を除き、
OnParametersSetAsyncを使用しないでください。 初回の読み込みにはOnInitializedAsyncを使用します。 - WebAssembly/Auto モードのコンポーネントに
DbContextをインジェクションしないでください — ブラウザーにデータベースはありません。 HttpClientを使って自分のサーバーを呼び出さないでください — サービスを直接インジェクションします。- ユーザーに
exception.Messageを表示しないでください — 個人情報漏えいのリスクがあります。ログに記録し、一般的なメッセージを表示します。 - 自分でキャンセルする場合に
OperationCanceledExceptionをキャッチしないでください —ComponentBaseが処理します。