{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/use-js-interop",
  "version": "1.0.1",
  "name": "use-js-interop",
  "description": "Add, review, or fix JavaScript interop in Blazor components. USE FOR: calling JavaScript from Blazor, calling .NET from JavaScript, collocated .razor.js modules, IJSRuntime, IJSObjectReference lifecycle, DotNetObjectReference, ElementReference, timing rules for when JS is available, IAsyncDisposable disposal of JS references, server-side JS interop safety. DO NOT USE FOR: general Blazor component authoring without JS interop needs (use author-component), forms (use collect-user-input).",
  "system_prompt_fragment": "# JS Interop in Blazor\n\n## 1. Collocated JS Modules\n\nAlways use collocated `.razor.js` files with `export` — never global `window.*` functions or `<script>` tags.\n\n```javascript\n// ChartPanel.razor.js — placed next to ChartPanel.razor\nexport function initialize(canvas, dotNetRef) { /* ... */ }\nexport function updateData(points) { /* ... */ }\nexport function dispose() { /* ... */ }\n```\n\nImport paths: same project = `\"./Components/ChartPanel.razor.js\"`, RCL = `\"./_content/{AssemblyName}/...\"`.\n\n## 2. Lifecycle Timing\n\n**All JS interop must happen in `OnAfterRenderAsync` or event handlers** — never in `OnInitialized`, `OnParametersSet`, or constructors. JS is not available during server prerendering.\n\nUse a typed interop wrapper (see Section 4) — never call `InvokeAsync`/`InvokeVoidAsync` with raw string literals:\n\n```csharp\nprivate ChartInterop? _chart;\n\nprotected override async Task OnAfterRenderAsync(bool firstRender)\n{\n    if (firstRender)\n    {\n        _chart = new ChartInterop(JS);\n        await _chart.InitializeAsync(_canvasRef);\n    }\n}\n```\n\n**Parameter changes**: set a flag in `OnParametersSet`, apply in `OnAfterRenderAsync`:\n\n```csharp\nprivate bool _dataChanged;\n\nprotected override void OnParametersSet() => _dataChanged = true;\n\nprotected override async Task OnAfterRenderAsync(bool firstRender)\n{\n    if (firstRender) { /* init */ }\n    else if (_dataChanged && _chart is not null)\n    {\n        _dataChanged = false;\n        await _chart.UpdateDataAsync(DataPoints);\n    }\n}\n```\n\n## 3. Batch Related Operations\n\nEach JS interop call crosses the .NET-to-JS boundary (and in Blazor Server, the SignalR circuit). Batching applies in **both directions** — .NET→JS and JS→.NET.\n\n### .NET → JS: merge consecutive calls\n\nIf the C# side makes two or more JS calls in a row, combine them into one JS function:\n\n```csharp\n// ❌ Two round-trips — theme and locale are always applied together\nawait _module.InvokeVoidAsync(\"applyTheme\", theme);\nawait _module.InvokeVoidAsync(\"applyLocale\", locale);\n\n// ❌ Result of one call feeds into another — both can stay in JS\nvar token = await _module.InvokeAsync<string>(\"createAccessToken\");\nawait _module.InvokeVoidAsync(\"storeToken\", token);\n```\n\n```javascript\n// ✅ One call applies both — no data dependency, no reason for two trips\nexport function applyPreferences(theme, locale) {\n    document.documentElement.dataset.theme = theme;\n    document.documentElement.lang = locale;\n}\n\n// ✅ Chain stays in JS — the token never needs to cross the boundary\nexport function createAndStoreToken() {\n    const token = crypto.randomUUID();\n    sessionStorage.setItem('access-token', token);\n    return token;\n}\n```\n\n### JS → .NET: batch callbacks\n\nWhen JS needs to send multiple pieces of data back to .NET, send them in a single `invokeMethodAsync` call rather than making separate callbacks:\n\n```javascript\n// ❌ Two .NET round-trips from JS\nawait dotNetRef.invokeMethodAsync(ON_VOLUME_CHANGED, volume);\nawait dotNetRef.invokeMethodAsync(ON_PLAYBACK_CHANGED, isPlaying);\n\n// ✅ One callback with all data\nawait dotNetRef.invokeMethodAsync(ON_PLAYER_STATE_CHANGED, { volume, isPlaying });\n```\n\n**Rule**: if two interop calls always happen together from either side, merge them into one function.\n\n## 4. Typed Interop Wrapper\n\nEncapsulate interop for a feature in a plain class that owns the module lifecycle:\n\n```csharp\npublic sealed class ChartInterop : IAsyncDisposable\n{\n    internal const string ModulePath = \"./Components/ChartPanel.razor.js\";\n    internal const string InitMethod = \"initialize\";\n    internal const string UpdateMethod = \"updateData\";\n    internal const string DisposeMethod = \"dispose\";\n\n    private readonly IJSRuntime _js;\n    private IJSObjectReference? _module;\n\n    public ChartInterop(IJSRuntime js) => _js = js;\n\n    private async ValueTask<IJSObjectReference> GetModuleAsync()\n        => _module ??= await _js.InvokeAsync<IJSObjectReference>(\"import\", ModulePath);\n\n    public async ValueTask InitializeAsync(ElementReference canvas)\n    {\n        var module = await GetModuleAsync();\n        await module.InvokeVoidAsync(InitMethod, canvas);\n    }\n\n    public async ValueTask UpdateDataAsync(IReadOnlyList<DataPoint> points)\n    {\n        var module = await GetModuleAsync();\n        await module.InvokeVoidAsync(UpdateMethod, points);\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        try\n        {\n            if (_module is not null)\n            {\n                await _module.InvokeVoidAsync(DisposeMethod);\n                await _module.DisposeAsync();\n            }\n        }\n        catch (JSDisconnectedException) { }\n    }\n}\n```\n\nThe component creates and uses the wrapper with no magic strings:\n\n```razor\n@inject IJSRuntime JS\n@implements IAsyncDisposable\n\n<canvas @ref=\"_canvasRef\" width=\"600\" height=\"400\"></canvas>\n\n@code {\n    private ElementReference _canvasRef;\n    private ChartInterop? _chart;\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (firstRender)\n        {\n            _chart = new ChartInterop(JS);\n            await _chart.InitializeAsync(_canvasRef);\n        }\n    }\n\n    async ValueTask IAsyncDisposable.DisposeAsync()\n    {\n        if (_chart is not null)\n            await _chart.DisposeAsync();\n    }\n}\n```\n\nPrefer a concrete class over interface + implementation for interop wrappers. For unit testing, substitute `IJSRuntime` directly (it is already an interface).\n\n## 5. DotNetObjectReference for JS-to-.NET Callbacks\n\n```csharp\n_dotNetRef = DotNetObjectReference.Create(this);\nawait _module.InvokeVoidAsync(\"initialize\", _dotNetRef);\n```\n\nOn the JS side, wrap the `dotNetRef` in a class. Use `async`/`await` with `try/catch` (not `.catch()`) to guard against circuit loss. Define .NET method name constants at the top:\n\n```javascript\nconst ON_CLIPBOARD_CHANGED = 'OnClipboardChanged';\n\nclass ClipboardMonitor {\n    #dotNetRef;\n    #abortController;\n\n    constructor(dotNetRef) {\n        this.#dotNetRef = dotNetRef;\n        this.#abortController = new AbortController();\n    }\n\n    start() {\n        document.addEventListener('copy', async () => {\n            try {\n                const text = await navigator.clipboard.readText();\n                await this.#dotNetRef.invokeMethodAsync(ON_CLIPBOARD_CHANGED, text);\n            } catch { /* circuit disconnected or clipboard denied */ }\n        }, { signal: this.#abortController.signal });\n    }\n\n    dispose() {\n        this.#abortController.abort();\n    }\n}\n\nlet monitor;\nexport function initialize(dotNetRef) {\n    monitor = new ClipboardMonitor(dotNetRef);\n    monitor.start();\n}\n\nexport function dispose() {\n    monitor?.dispose();\n}\n```\n\nRules:\n- `[JSInvokable]` methods **must be `public`** — private/internal silently fails at runtime\n- Wrap `StateHasChanged` in `InvokeAsync` inside `[JSInvokable]` callbacks:\n  ```csharp\n  [JSInvokable]\n  public async Task OnClipboardChanged(string text)\n  {\n      await InvokeAsync(() => { _lastClipboard = text; StateHasChanged(); });\n  }\n  ```\n- Always `try/catch` around `invokeMethodAsync` in JS — circuit loss throws\n- Use `const` for .NET method name strings in JS — prevents typo bugs that silently fail\n- Dispose `DotNetObjectReference` in `DisposeAsync`\n\n## 6. Disposal and Server Safety\n\nAlways implement `IAsyncDisposable`. Call JS cleanup first, then dispose references. Catch `JSDisconnectedException` for Blazor Server circuit loss:\n\n```csharp\npublic async ValueTask DisposeAsync()\n{\n    try\n    {\n        if (_module is not null)\n        {\n            await _module.InvokeVoidAsync(\"dispose\");\n            await _module.DisposeAsync();\n        }\n    }\n    catch (JSDisconnectedException) { }\n\n    _dotNetRef?.Dispose();\n}\n```\n\nNever use sync `IDisposable` for JS interop cleanup — `InvokeVoidAsync` returns `ValueTask` and must be awaited.\n\n## 7. ElementReference\n\nPass DOM elements via `@ref`, not string IDs:\n\n```razor\n<canvas @ref=\"_canvasRef\" width=\"600\" height=\"400\"></canvas>\n```\n\n```csharp\nawait _chart.InitializeAsync(_canvasRef);\n```\n\n## Checklist\n\n- [ ] JS is in collocated `.razor.js` with `export` — no `window.*` globals\n- [ ] All interop in `OnAfterRenderAsync` or event handlers — never during prerender\n- [ ] `IAsyncDisposable` catches `JSDisconnectedException`\n- [ ] `DotNetObjectReference` disposed in `DisposeAsync`; JS side has `try/catch` around `invokeMethodAsync`\n- [ ] `[JSInvokable]` methods are `public` and use `await InvokeAsync(StateHasChanged)`\n- [ ] `InvokeVoidAsync` used when no return value is needed\n- [ ] `ElementReference` instead of string IDs\n- [ ] Related operations batched into single interop calls (both .NET→JS and JS→.NET)\n\n## Common Mistakes Checklist\n\n| Mistake | Fix |\n|---------|-----|\n| Using JS for something achievable with CSS | Use CSS custom properties, `data-` attributes, pseudo-classes |\n| Many fine-grained interop calls | Batch into coarse functions — both .NET→JS and JS→.NET |\n| Component imports JS module directly | Encapsulate in a strongly typed interop class |\n| Magic strings for method names / module paths | Define `internal const` fields in the interop class |\n| Interface + implementation for interop wrapper | Use a plain class; mock `IJSRuntime` for tests instead |\n| JS calls in `OnInitializedAsync` | Move to `OnAfterRenderAsync(firstRender)` |\n| `InvokeAsync<object>` for void calls | Use `InvokeVoidAsync` |\n| `IDisposable` with fire-and-forget JS | Use `IAsyncDisposable` with `await` |\n| Global `window.*` JS functions | Use collocated `.razor.js` with `export` |\n| String element IDs passed to JS | Use `ElementReference` with `@ref` |\n| `[JSInvokable]` on private method | Must be `public` — silently fails otherwise |\n| `DotNetObjectReference` not disposed | Dispose in `DisposeAsync` — causes memory leak |\n| `StateHasChanged()` without `InvokeAsync` | Wrap in `await InvokeAsync(() => { StateHasChanged(); })` |\n| JS `invokeMethodAsync` without error handling | Wrap in `try/catch` — circuit loss throws |\n| Bare `dotNetRef` in JS event handlers | Wrap in a class with `#dotNetRef` private field |\n| Magic strings in JS `invokeMethodAsync` calls | Use `const` at module top — typos silently fail at runtime |\n| JS calls in `OnParametersSetAsync` | Track changes, apply in `OnAfterRenderAsync` with guard |\n| No null check before calling module | Check `module is not null` before use |",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/use-js-interop"
  ],
  "tags": [
    "dotnet-blazor",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-blazor/skills/use-js-interop/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-blazor/skills/use-js-interop/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}