[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6DZDF0G4P0YNXRG79MNXWZN":3,"skill-same:01M6DZDF0G4P0YNXRG79MNXWZN":72},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":48,"commitSha":49,"skillDirectory":50,"forksCount":51,"repositoryHtmlUrl":52,"repositoryHomepageUrl":53,"ownerHtmlUrl":54,"ownerBlogUrl":55,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M6DZDF0G4P0YNXRG79MNXWZN","use-js-interop",null,"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).","managedcode\u002Fdotnet-skills","managedcode","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F78253589?v=4","01M6DYDJR3EG8KB5G4PC65XCTT","Managed Code","01M6DYDJX0ZT432N04NYJW89DN",512,"MIT","https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fexternal-sources\u002Fupstreams\u002Fdotnet-skills\u002Fplugins\u002Fdotnet-blazor\u002Fskills\u002Fuse-js-interop\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"use js interop","js","use","modules","when","author","for","timing","server-side","forms","calling","do","references","general","review","fix","Rules","lifecycle","server","collect","blazor","component","components","javascript","2026-11-10 10:12:30",false,"---\nlicense: MIT\nname: use-js-interop\ndescription: >\n  Add, review, or fix JavaScript interop in Blazor components.\n  USE FOR: calling JavaScript from Blazor, calling .NET from JavaScript,\n  collocated .razor.js modules, IJSRuntime, IJSObjectReference lifecycle,\n  DotNetObjectReference, ElementReference, timing rules for when JS is available,\n  IAsyncDisposable disposal of JS references, server-side JS interop safety.\n  DO NOT USE FOR: general Blazor component authoring without JS interop needs\n  (use author-component), forms (use collect-user-input).\n---\n\n# JS Interop in Blazor\n\n## 1. Collocated JS Modules\n\nAlways use collocated `.razor.js` files with `export` — never global `window.*` functions or `\u003Cscript>` tags.\n\n```javascript\n\u002F\u002F ChartPanel.razor.js — placed next to ChartPanel.razor\nexport function initialize(canvas, dotNetRef) { \u002F* ... *\u002F }\nexport function updateData(points) { \u002F* ... *\u002F }\nexport function dispose() { \u002F* ... *\u002F }\n```\n\nImport paths: same project = `\".\u002FComponents\u002FChartPanel.razor.js\"`, RCL = `\".\u002F_content\u002F{AssemblyName}\u002F...\"`.\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`\u002F`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) { \u002F* init *\u002F }\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\u002F\u002F ❌ Two round-trips — theme and locale are always applied together\nawait _module.InvokeVoidAsync(\"applyTheme\", theme);\nawait _module.InvokeVoidAsync(\"applyLocale\", locale);\n\n\u002F\u002F ❌ Result of one call feeds into another — both can stay in JS\nvar token = await _module.InvokeAsync\u003Cstring>(\"createAccessToken\");\nawait _module.InvokeVoidAsync(\"storeToken\", token);\n```\n\n```javascript\n\u002F\u002F ✅ 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\u002F\u002F ✅ 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\u002F\u002F ❌ Two .NET round-trips from JS\nawait dotNetRef.invokeMethodAsync(ON_VOLUME_CHANGED, volume);\nawait dotNetRef.invokeMethodAsync(ON_PLAYBACK_CHANGED, isPlaying);\n\n\u002F\u002F ✅ 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 = \".\u002FComponents\u002FChartPanel.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\u003CIJSObjectReference> GetModuleAsync()\n        => _module ??= await _js.InvokeAsync\u003CIJSObjectReference>(\"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\u003CDataPoint> 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\u003Ccanvas @ref=\"_canvasRef\" width=\"600\" height=\"400\">\u003C\u002Fcanvas>\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`\u002F`await` with `try\u002Fcatch` (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 { \u002F* circuit disconnected or clipboard denied *\u002F }\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\u002Finternal 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\u002Fcatch` 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\u003Ccanvas @ref=\"_canvasRef\" width=\"600\" height=\"400\">\u003C\u002Fcanvas>\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\u002Fcatch` 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 \u002F 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\u003Cobject>` 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\u002Fcatch` — 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 |\n","# JS Interop in Blazor\n\n## 1. Collocated JS Modules\n\nAlways use collocated `.razor.js` files with `export` — never global `window.*` functions or `\u003Cscript>` tags.\n\n```javascript\n\u002F\u002F ChartPanel.razor.js — placed next to ChartPanel.razor\nexport function initialize(canvas, dotNetRef) { \u002F* ... *\u002F }\nexport function updateData(points) { \u002F* ... *\u002F }\nexport function dispose() { \u002F* ... *\u002F }\n```\n\nImport paths: same project = `\".\u002FComponents\u002FChartPanel.razor.js\"`, RCL = `\".\u002F_content\u002F{AssemblyName}\u002F...\"`.\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`\u002F`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) { \u002F* init *\u002F }\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\u002F\u002F ❌ Two round-trips — theme and locale are always applied together\nawait _module.InvokeVoidAsync(\"applyTheme\", theme);\nawait _module.InvokeVoidAsync(\"applyLocale\", locale);\n\n\u002F\u002F ❌ Result of one call feeds into another — both can stay in JS\nvar token = await _module.InvokeAsync\u003Cstring>(\"createAccessToken\");\nawait _module.InvokeVoidAsync(\"storeToken\", token);\n```\n\n```javascript\n\u002F\u002F ✅ 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\u002F\u002F ✅ 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\u002F\u002F ❌ Two .NET round-trips from JS\nawait dotNetRef.invokeMethodAsync(ON_VOLUME_CHANGED, volume);\nawait dotNetRef.invokeMethodAsync(ON_PLAYBACK_CHANGED, isPlaying);\n\n\u002F\u002F ✅ 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 = \".\u002FComponents\u002FChartPanel.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\u003CIJSObjectReference> GetModuleAsync()\n        => _module ??= await _js.InvokeAsync\u003CIJSObjectReference>(\"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\u003CDataPoint> 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\u003Ccanvas @ref=\"_canvasRef\" width=\"600\" height=\"400\">\u003C\u002Fcanvas>\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`\u002F`await` with `try\u002Fcatch` (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 { \u002F* circuit disconnected or clipboard denied *\u002F }\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\u002Finternal 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\u002Fcatch` 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\u003Ccanvas @ref=\"_canvasRef\" width=\"600\" height=\"400\">\u003C\u002Fcanvas>\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\u002Fcatch` 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 \u002F 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\u003Cobject>` 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\u002Fcatch` — 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 |\n",{"name":5,"license":15,"description":7},"main","497cc9aba79a931ac6fb8d36a48cc22fe3d6d74b","external-sources\u002Fupstreams\u002Fdotnet-skills\u002Fplugins\u002Fdotnet-blazor\u002Fskills\u002Fuse-js-interop",41,"https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills","https:\u002F\u002Fskills.managed-code.com","https:\u002F\u002Fgithub.com\u002Fmanagedcode","https:\u002F\u002Fwww.managed-code.com","Open Source Community for .NET Developers: Reliable, Actively Maintained, and Community-Driven. Made in Ukraine 🇺🇦 with ❤️","2026-11-11 19:34:28",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add managedcode\u002Fdotnet-skills --skill use-js-interop",{"tool":64,"command":65},"bunx","bunx skills add managedcode\u002Fdotnet-skills --skill use-js-interop",{"tool":67,"command":68},"pnpm","pnpm dlx skills add managedcode\u002Fdotnet-skills --skill use-js-interop","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fexternal-sources\u002Fupstreams\u002Fdotnet-skills\u002Fplugins\u002Fdotnet-blazor\u002Fskills\u002Fuse-js-interop\u002FSKILL.md",true,[],[73,110,134,162,172,199],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":89,"publishedAt":43},"01M6DYQE1Y9FHAQHEDTVFQ3447","xtwitter-scraper","Use XTwitterScraper when a .NET application needs typed access to Xquik's X automation REST API for tweet search, user lookup, posting workflows, extraction jobs, monitors, webhooks, or giveaway draws. USE FOR: integrating the XTwitterScraper NuGet package; reviewing Xquik API client usage; modeling API-key configuration, pagination, binary exports, raw responses, retries, and webhook\u002Fevent flows in .NET. DO NOT USE FOR: unrelated stacks; direct browser automation; generic social-media strategy; or code that does not call Xquik. INVOKES: inspect the repository context, edit targeted files, and run relevant build, test, lint, or validation commands when changes are made.","https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fcatalog\u002FLibraries\u002FXTwitterScraper\u002Fskills\u002Fxtwitter-scraper\u002FSKILL.md",[79,84],{"categoryNo":80,"categoryName":81,"taxonomyType":82,"taxonomyTypeDesc":83},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":85,"categoryName":86,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109],"xtwitter scraper","browser automation","workflows","run","code","browser","search","repository","api","Media","api client","access","edit","unrelated","webhooks","files","social","commands","modeling","webhook",{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":122,"publishedAt":43},"01M6DYQ9S4PE1H6G8MCH9T21HR","sep","Use Sep for high-performance separated-value parsing and writing in .NET, including delimiter inference, explicit parser\u002Fwriter options, and low-allocation row\u002Fcolumn workflows. USE FOR: delimited data needs are performance-sensitive and allocation-aware; project needs explicit control over separator inference, escaping, trimming, and header behavior;. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this specific guidance. INVOKES: inspect the repository context, edit targeted files, and run relevant build, test, lint, or validation commands when changes are made.","https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fcatalog\u002FLibraries\u002FSep\u002Fskills\u002Fsep\u002FSKILL.md",[116,119],{"categoryNo":117,"categoryName":118,"taxonomyType":82,"taxonomyTypeDesc":83},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":120,"categoryName":121,"taxonomyType":87,"taxonomyTypeDesc":88},"01JBSEEDCAT000000000000036","内容创作者",[112,123,124,92,125,93,97,126,127,128,102,129,103,105,30,130,131,132,107,133],"writing","performance","tasks","data","parsing","parser","context","writer","options","project","high-performance",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":140,"publishedAt":43},"01M6DZS2DNEPXCGGF6SSCW6G5V","webgpu-threejs-tsl","Comprehensive guide for developing WebGPU-enabled Three.js applications using TSL (Three.js Shading Language). Covers WebGPU renderer setup, TSL syntax and node materials, compute shaders, post-processing effects, and WGSL integration. Use this skill when working with Three.js WebGPU, TSL shaders, node materials, or GPU compute in Three.js.","https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fexternal-sources\u002Fupstreams\u002Fwebgpu-claude-skill\u002Fskills\u002Fwebgpu-threejs-tsl\u002FSKILL.md",[],[141,142,143,144,145,146,147,148,20,149,150,151,21,152,153,154,155,156,157,158,25,159,160,161,23],"webgpu","threejs","webgpu threejs tsl","setup","shaders","gpu","integration","applications","node","syntax","guide","language","three","compute","post","processing","with","renderer","materials","wgsl","effects",{"skillNo":163,"name":164,"nameZh":6,"description":165,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":166,"categories":167,"keywords":168,"publishedAt":43},"01M6DZS0BQXW2B56A289XF1X76","roslynk","Use Roslynk for semantic inspection and edits of *.cs, *.razor and *.cshtml solution files.","https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fexternal-sources\u002Fupstreams\u002Froslynk\u002Fskills\u002Froslynk\u002FSKILL.md",[],[164,21,105,169,170,25,171],"semantic","razor","inspection",{"skillNo":173,"name":174,"nameZh":6,"description":175,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":176,"categories":177,"keywords":178,"publishedAt":43},"01M6DZRW6MEJCVW80K3S4XV60K","system-text-json-net11","Imperative guidance for the System.Text.Json APIs added in .NET 11: the built-in `JsonNamingPolicy.PascalCase` naming policy, and the strongly-typed generic `JsonSerializerOptions.GetTypeInfo` and `JsonSerializerOptions.TryGetTypeInfo` metadata accessors (generic overloads that return a typed `JsonTypeInfo`). USE ONLY when the user is targeting net11.0 or later and needs PascalCase JSON property or dictionary-key names without writing a custom naming policy, a strongly-typed generic `JsonTypeInfo` instead of the non-generic `JsonTypeInfo`, or a no-throw way to probe whether a type's serialization metadata is resolved. DO NOT USE when the target is earlier than net11.0, the requested behavior uses an established pre-net11 naming policy, or the user explicitly selected another JSON library.","https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fexternal-sources\u002Fupstreams\u002Fdotnet-skills\u002Fplugins\u002Fdotnet11\u002Fskills\u002Fsystem-text-json-net11\u002FSKILL.md",[],[179,180,181,182,123,183,184,185,186,187,188,21,189,190,191,192,30,193,194,195,196,25,197,198,23],"system","json","text","system text json net11","policy","metadata","library","serialization","behavior","user","only","probe","guidance","later","type","dictionary","naming","targeting","custom","generic",{"skillNo":200,"name":201,"nameZh":6,"description":202,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":203,"categories":204,"keywords":207,"publishedAt":43},"01M6DZRRJP3S662NFSG1RSPQQ7","process-api-net11","Provides guidance on the new System.Diagnostics.Process APIs introduced in .NET 11: Process.Run, Process.RunAndCaptureText, Process.StartAndForget, Process.ReadAllText\u002FBytes\u002FLines, KillOnParentExit, InheritedHandles, and StartDetached. USE FOR: starting, orchestrating, or capturing output from external processes in applications targeting net11.0 or later. DO NOT USE FOR: applications targeting .NET 10 or earlier, or basic pre-.NET 11 Process.Start usage without new APIs.","https:\u002F\u002Fgithub.com\u002Fmanagedcode\u002Fdotnet-skills\u002Fblob\u002Fmain\u002Fexternal-sources\u002Fupstreams\u002Fdotnet-skills\u002Fplugins\u002Fdotnet11\u002Fskills\u002Fprocess-api-net11\u002FSKILL.md",[205,206],{"categoryNo":80,"categoryName":81,"taxonomyType":82,"taxonomyTypeDesc":83},{"categoryNo":85,"categoryName":86,"taxonomyType":87,"taxonomyTypeDesc":88},[98,208,209,210,93,211,148,179,21,191,192,30,212,213,214,215,196,25,216],"process","process api net11","diagnostics","start","basic","processes","output","usage","new"]