use-js-interop について
Blazor 開発者向けに、JavaScript 相互運用の呼び出し時期、モジュールのライフサイクル、バッチ処理、コールバックの安全性をまとめたスキルです。コンポーネント隣接モジュールと型付きラッパーで、.NET と JavaScript 間の呼び出しを管理します。
主な機能
- 01隣接する .razor.js モジュールの使用
- 02レンダリング後またはイベント内での JS 呼び出し
- 03関連する相互運用呼び出しの一括化
- 04型付きクラスによるモジュールライフサイクルの管理
- 05コールバック、切断、非同期クリーンアップへの対応
活用シーン
- 01Blazor コンポーネントからブラウザー側 JavaScript を利用
- 02Blazor Server の SignalR 相互運用往復の削減
- 03JS コールバック、コンポーネント状態更新、後片付けの管理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill use-js-interopインストール後、Claude Code で/use-js-interopを入力して読み込まれたか確認します。
詳細
Blazor での JS 相互運用
1. 同じ場所に配置する JS モジュール
export を使う同じ場所に配置した .razor.js ファイルを必ず使用してください。グローバルな window.* 関数や <script> タグは使用しないでください。
// ChartPanel.razor.js — placed next to ChartPanel.razor
export function initialize(canvas, dotNetRef) { /* ... */ }
export function updateData(points) { /* ... */ }
export function dispose() { /* ... */ }
インポートパス:同じプロジェクトの場合は "./Components/ChartPanel.razor.js"、RCL の場合は "./_content/{AssemblyName}/..." です。
2. ライフサイクルのタイミング
すべての JS 相互運用は OnAfterRenderAsync またはイベントハンドラー内で行う必要があります。OnInitialized、OnParametersSet、コンストラクター内では行わないでください。サーバーの事前レンダリング中は JS を使用できません。
型付きの相互運用ラッパーを使用してください(セクション 4 を参照)。生の文字列リテラルを使って InvokeAsync/InvokeVoidAsync を呼び出さないでください。
private ChartInterop? _chart;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_chart = new ChartInterop(JS);
await _chart.InitializeAsync(_canvasRef);
}
}
パラメーターの変更:OnParametersSet でフラグを設定し、OnAfterRenderAsync で反映します。
private bool _dataChanged;
protected override void OnParametersSet() => _dataChanged = true;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) { /* init */ }
else if (_dataChanged && _chart is not null)
{
_dataChanged = false;
await _chart.UpdateDataAsync(DataPoints);
}
}
3. 関連する操作をまとめる
JS 相互運用の呼び出しごとに、.NET と JS の境界(Blazor Server では SignalR 回線も)をまたぎます。バッチ処理は両方向、つまり .NET→JS と JS→.NET のどちらにも適用されます。
.NET → JS:連続する呼び出しをまとめる
C# 側で JS 呼び出しを 2 回以上連続して行う場合は、1 つの JS 関数にまとめます。
// ❌ Two round-trips — theme and locale are always applied together
await _module.InvokeVoidAsync("applyTheme", theme);
await _module.InvokeVoidAsync("applyLocale", locale);
// ❌ Result of one call feeds into another — both can stay in JS
var token = await _module.InvokeAsync<string>("createAccessToken");
await _module.InvokeVoidAsync("storeToken", token);
// ✅ One call applies both — no data dependency, no reason for two trips
export function applyPreferences(theme, locale) {
document.documentElement.dataset.theme = theme;
document.documentElement.lang = locale;
}
// ✅ Chain stays in JS — the token never needs to cross the boundary
export function createAndStoreToken() {
const token = crypto.randomUUID();
sessionStorage.setItem('access-token', token);
return token;
}
JS → .NET:コールバックをまとめる
JS から複数のデータを .NET に返す場合は、コールバックを個別に実行するのではなく、1 回の invokeMethodAsync 呼び出しでまとめて送信します。
// ❌ Two .NET round-trips from JS
await dotNetRef.invokeMethodAsync(ON_VOLUME_CHANGED, volume);
await dotNetRef.invokeMethodAsync(ON_PLAYBACK_CHANGED, isPlaying);
// ✅ One callback with all data
await dotNetRef.invokeMethodAsync(ON_PLAYER_STATE_CHANGED, { volume, isPlaying });
ルール:どちらかの側で 2 つの相互運用呼び出しが常に一緒に行われる場合は、1 つの関数にまとめてください。
4. 型付き相互運用ラッパー
機能ごとの相互運用処理を、モジュールのライフサイクルを管理するプレーンなクラスにカプセル化します。
public sealed class ChartInterop : IAsyncDisposable
{
internal const string ModulePath = "./Components/ChartPanel.razor.js";
internal const string InitMethod = "initialize";
internal const string UpdateMethod = "updateData";
internal const string DisposeMethod = "dispose";
private readonly IJSRuntime _js;
private IJSObjectReference? _module;
public ChartInterop(IJSRuntime js) => _js = js;
private async ValueTask<IJSObjectReference> GetModuleAsync()
=> _module ??= await _js.InvokeAsync<IJSObjectReference>("import", ModulePath);
public async ValueTask InitializeAsync(ElementReference canvas)
{
var module = await GetModuleAsync();
await module.InvokeVoidAsync(InitMethod, canvas);
}
public async ValueTask UpdateDataAsync(IReadOnlyList<DataPoint> points)
{
var module = await GetModuleAsync();
await module.InvokeVoidAsync(UpdateMethod, points);
}
public async ValueTask DisposeAsync()
{
try
{
if (_module is not null)
{
await _module.InvokeVoidAsync(DisposeMethod);
await _module.DisposeAsync();
}
}
catch (JSDisconnectedException) { }
}
}
コンポーネントは、マジック文字列を使わずにラッパーを作成して使用します。
@inject IJSRuntime JS
@implements IAsyncDisposable
<canvas @ref="_canvasRef" width="600" height="400"></canvas>
@code {
private ElementReference _canvasRef;
private ChartInterop? _chart;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_chart = new ChartInterop(JS);
await _chart.InitializeAsync(_canvasRef);
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (_chart is not null)
await _chart.DisposeAsync();
}
}
相互運用ラッパーには、インターフェイスと実装の組み合わせより、具象クラスを使用してください。単体テストでは、IJSRuntime を直接差し替えます(これはすでにインターフェイスです)。
5. JS から .NET へのコールバックに使用する DotNetObjectReference
_dotNetRef = DotNetObjectReference.Create(this);
await _module.InvokeVoidAsync("initialize", _dotNetRef);
JS 側では、dotNetRef をクラスでラップします。回線切断に備えて、.catch() ではなく try/catch と async/await を使用します。.NET メソッド名の定数は先頭で定義します。
const ON_CLIPBOARD_CHANGED = 'OnClipboardChanged';
class ClipboardMonitor {
#dotNetRef;
#abortController;
constructor(dotNetRef) {
this.#dotNetRef = dotNetRef;
this.#abortController = new AbortController();
}
start() {
document.addEventListener('copy', async () => {
try {
const text = await navigator.clipboard.readText();
await this.#dotNetRef.invokeMethodAsync(ON_CLIPBOARD_CHANGED, text);
} catch { /* circuit disconnected or clipboard denied */ }
}, { signal: this.#abortController.signal });
}
dispose() {
this.#abortController.abort();
}
}
let monitor;
export function initialize(dotNetRef) {
monitor = new ClipboardMonitor(dotNetRef);
monitor.start();
}
export function dispose() {
monitor?.dispose();
}
ルール:
[JSInvokable]メソッドは**publicである必要があります**。private/internalにすると、実行時にエラーなく失敗します[JSInvokable]コールバック内では、StateHasChangedをInvokeAsyncでラップします。[JSInvokable] public async Task OnClipboardChanged(string text) { await InvokeAsync(() => { _lastClipboard = text; StateHasChanged(); }); }- JS の
invokeMethodAsyncの呼び出しは、必ずtry/catchで囲みます。回線が切断されると例外が発生します - JS の .NET メソッド名文字列には
constを使用します。これにより、気づかれないまま失敗するタイプミスを防げます DisposeAsyncでDotNetObjectReferenceを破棄します
6. 破棄とサーバーの安全性
必ず IAsyncDisposable を実装します。最初に JS のクリーンアップを呼び出し、その後で参照を破棄します。Blazor Server の回線切断に備えて、JSDisconnectedException をキャッチします。
public async ValueTask DisposeAsync()
{
try
{
if (_module is not null)
{
await _module.InvokeVoidAsync("dispose");
await _module.DisposeAsync();
}
}
catch (JSDisconnectedException) { }
_dotNetRef?.Dispose();
}
JS 相互運用のクリーンアップに同期的な IDisposable は使用しないでください。InvokeVoidAsync は ValueTask を返すため、await する必要があります。
7. ElementReference
文字列 ID ではなく、@ref で DOM 要素を渡します。
<canvas @ref="_canvasRef" width="600" height="400"></canvas>
await _chart.InitializeAsync(_canvasRef);
チェックリスト
- JS は
exportを使う同じ場所に配置した.razor.jsに記述し、window.*グローバルは使用しない - すべての相互運用は
OnAfterRenderAsyncまたはイベントハンドラー内で行い、事前レンダリング中は実行しない -
IAsyncDisposableでJSDisconnectedExceptionをキャッチする -
DotNetObjectReferenceはDisposeAsyncで破棄し、JS 側ではinvokeMethodAsyncをtry/catchで囲む -
[JSInvokable]メソッドはpublicにし、await InvokeAsync(StateHasChanged)を使用する - 戻り値が不要な場合は
InvokeVoidAsyncを使用する - 文字列 ID の代わりに
ElementReferenceを使用する - 関連する操作は 1 回の相互運用呼び出しにまとめる(.NET→JS と JS→.NET の両方)
よくある間違いのチェックリスト
| 間違い | 修正方法 |
|---|---|
| CSSで実現できることにJSを使う | CSSカスタムプロパティ、data-属性、疑似クラスを使用します |
| 細分化された相互運用呼び出しが多い | .NET→JS、JS→.NETの両方で、まとめて粗粒度の関数にします |
| コンポーネントがJSモジュールを直接インポートする | 型安全な相互運用クラスにカプセル化します |
| メソッド名/モジュールパスにマジック文字列を使う | 相互運用クラス内でinternal constフィールドを定義します |
| 相互運用ラッパーにインターフェイスと実装を用意する | 通常のクラスを使い、代わりにテストではIJSRuntimeをモックします |
OnInitializedAsync内でJSを呼び出す | OnAfterRenderAsync(firstRender)に移します |
戻り値のない呼び出しにInvokeAsync<object>を使う | InvokeVoidAsyncを使用します |
JSを呼び出さずに破棄するIDisposable | awaitを使うIAsyncDisposableを使用します |
グローバルなwindow.* JS関数 | exportを使う、コンポーネントと同じ場所に配置した.razor.jsを使用します |
| 文字列の要素IDをJSに渡す | @refでElementReferenceを使用します |
プライベートメソッドに[JSInvokable]を付ける | publicである必要があります。そうでない場合、警告なく失敗します |
DotNetObjectReferenceを破棄しない | DisposeAsyncで破棄します。破棄しないとメモリリークが発生します |
InvokeAsyncを使わずにStateHasChanged()を呼び出す | await InvokeAsync(() => { StateHasChanged(); })で囲みます |
エラー処理なしでJSのinvokeMethodAsyncを呼び出す | try/catchで囲みます。回線の切断時に例外が発生します |
JSのイベントハンドラーでdotNetRefをそのまま使う | #dotNetRefプライベートフィールドを持つクラスで囲みます |
JSのinvokeMethodAsync呼び出しでマジック文字列を使う | モジュールの先頭でconstを使います。誤記があっても実行時に警告なく失敗します |
OnParametersSetAsync内でJSを呼び出す | 変更を追跡し、ガードを設けてOnAfterRenderAsyncで適用します |
| モジュールを呼び出す前にnullチェックを行わない | 使用前にmodule is not nullを確認します |