mcpbeat

Reactor Commanding

microsoft/reactor-commanding

Reactor commanding — actions that appear in multiple surfaces (menu + toolbar), need keyboard shortcuts, or need `CanExecute`. `Command`, `StandardCommand`, `UseCommand`, `CommandHost`. Use when wiring shared actions or shortcuts.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
604
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/microsoft/microsoft-ui-reactor --skill reactor-commanding

The instruction itself

9 sections, as written by the author

Commanding in Reactor

Use Command when an action shows up in multiple surfaces (toolbar + menu +

context menu), needs a keyboard shortcut, or needs CanExecute disabling.

Use a bare Action for one-off button clicks with no reuse.

> Controlled prop note: command examples may host controlled inputs such as

> TextBox(text, onChange). Factory call sites stay plain-valued, but direct

> reads from migrated element records are Optional<T>; see

> migration/050-optional-t.md.

Command record

var save = new Command
{
    Label = "Save",                                  // required
    Execute = () => Save(),                          // sync
    // OR:
    ExecuteAsync = async () => await SaveAsync(),    // async (wrap with UseCommand)
    CanExecute = hasChanges,                         // default true
    Icon = SymbolIcon("Save"),
    Description = "Save the document",               // tooltip + a11y
    Accelerator = Accelerator(VirtualKey.S, VirtualKeyModifiers.Control),
    AccessKey = "S",                                 // Alt+key
    DebounceMs = 0,                                  // >0 = leading-edge debounce (needs UseCommand)
};
// Computed: IsEnabled = CanExecute && !IsExecuting && !IsDebouncing

Command<T> is identical but Execute/ExecuteAsync receive a typed

parameter — bind the parameter at the call site with MenuItem(cmd, item).

StandardCommand factory

Pre-built commands with correct labels, icons, and accelerators:

var cut    = StandardCommand.Cut(() => CutSelection());
var copy   = StandardCommand.Copy(() => CopySelection());
var paste  = StandardCommand.Paste(() => PasteFromClipboard());
var undo   = StandardCommand.Undo(() => Undo());
var redo   = StandardCommand.Redo(() => Redo());
var delete = StandardCommand.Delete(() => DeleteSelected());
var save   = StandardCommand.Save(async () => await SaveAsync()); // async overload
var open   = StandardCommand.Open(() => OpenFile());

// CanExecute parameter:
var cut2 = StandardCommand.Cut(() => CutSelection(), canExecute: hasSelection);

Also available: SelectAll, Close, Share, Play, Pause, Stop,

Forward, Backward.

Command-aware DSL overloads

Define once, bind anywhere:

var save = StandardCommand.Save(() => SaveFile());

Button(save)              // label → content, execute → click, isEnabled → isEnabled
AppBarButton(save)        // + icon, accelerator, accessKey, description
MenuItem(save)            // + icon, accelerator, accessKey, description
MenuItem(deleteCmd, item) // parameterized: binds item as argument

Custom content (icon + label, stacked layouts) — build the element, then

attach the command with the .Command() modifier. Works on Button,

HyperlinkButton, RepeatButton, ToggleButton, and AppBarButton:

// Factory takes a text label only:
Button(save)

// .Command() binds execute + isEnabled + icon/accelerator/accessKey/description
// onto any custom-content clickable, and auto-disables while !command.IsEnabled:
Button(HStack(Icon(SymbolIcon("Save")), Text("Save"))).Command(save)

.Command() re-applies IsEnabled on every update (so UseCommand

toggling IsExecuting, or CanExecute changes, flow through) — never

re-thread .IsEnabled(command.IsEnabled) by hand. It composes with

.IsDisabledFocusable(): a disabled command keeps the button reachable

via Tab instead of dropping it from the tab order.

Per-site overrides with with:

var delete = StandardCommand.Delete(() => DeleteSelected());
MenuItem(delete)                                          // "Delete"
MenuItem(delete with { Label = "Remove permanently" })
AppBarButton(delete with { Icon = SymbolIcon("Clear") })

UseCommand — async lifecycle

Only needed for commands with ExecuteAsync. Sync commands pass through

unchanged.

class Editor : Component
{
    public override Element Render()
    {
        var saveCmd = UseCommand(StandardCommand.Save(async () =>
        {
            await SaveAsync();
        }));

        // saveCmd.Execute is now a sync wrapper around the async
        // saveCmd.IsExecuting is true while the async is in-flight
        // saveCmd.IsEnabled auto-flips to false while executing
        return HStack(
            Button(saveCmd),
            saveCmd.IsExecuting ? ProgressRing() : Empty());
    }
}
  • Consumes 2 hook slots; re-entrance guard ignores clicks while executing.
  • IsExecuting resets to false even if ExecuteAsync throws.
  • Call unconditionally — don't wrap in if.
  • DebounceMs > 0 adds leading-edge debounce: the first fire runs,

re-fires within the window are dropped, and IsEnabled is false for the

duration so the button auto-disables then re-enables. Replaces the

Task.Delay-to-absorb-double-clicks workaround. Needs UseCommand to

persist the window — a raw new Command { DebounceMs = … } bound

directly does not debounce. Works on sync Execute too (stays on the

UI thread; no Task.Run hop). For async commands the disabled window is

the longer of the lambda lifetime and DebounceMs.

CommandHost — keyboard-scoped accelerators

Limits Accelerator registration to a subtree:

var save = StandardCommand.Save(() => SaveFile());
var undo = StandardCommand.Undo(() => UndoAction());

CommandHost([save, undo],
    VStack(
        TextBlock("Ctrl+S / Ctrl+Z only fire inside this region"),
        TextBox(value, onChange)))

Commands without an Accelerator are ignored by CommandHost.

Sharing commands via Context

Editor-provides / toolbar-consumes:

record EditorCommands(Command Save, Command Undo, Command Redo);
static readonly Context<EditorCommands?> EditorCtx = new(null);

class Editor : Component
{
    public override Element Render()
    {
        var save = UseCommand(StandardCommand.Save(async () => await SaveAsync()));
        var undo = StandardCommand.Undo(() => Undo());
        return TextBox(text, onChange)
            .Provide(EditorCtx, new EditorCommands(save, undo, redo));
    }
}

class Toolbar : Component
{
    public override Element Render()
    {
        var cmds = UseContext(EditorCtx);
        if (cmds is null) return Empty();
        return CommandBar(primaryCommands: [
            AppBarButton(cmds.Save),
            AppBarButton(cmds.Undo),
        ]);
    }
}

ICommand interop

Bridge existing MVVM/CommunityToolkit ICommand:

var cmd = CommandInterop.FromCommand(
    viewModel.SaveCommand,
    "Save",
    icon: SymbolIcon("Save"),
    accelerator: Accelerator(VirtualKey.S, VirtualKeyModifiers.Control));

Anti-patterns

  • Don't create commands inside loops. Define once, bind per item with

MenuItem(cmd, item).

  • Don't UseCommand for sync-only commands — it wastes hook slots.
  • Don't call UseCommand conditionally — hooks must run in the same order

every render.

  • Don't mix Execute and ExecuteAsync on the same command — pick one.

How to use it

Copy the folder

Take microsoft/reactor-commanding from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.