mcpbeat

Reactor Navigation

microsoft/reactor-navigation

Reactor navigation — `UseNavigation` hook, `NavigationHost` renderer, `NavigationView` sidebar, `TabView` tabs, route params, stack operations, page lifecycle, deep linking, transitions, caching. Use when building multi-page apps or wiring sidebar/tab navigation.

3k 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-navigation

The instruction itself

15 sections, as written by the author

Reactor uses a stack-based navigation model with type-safe routes. You

define routes as an enum, create a navigation handle with UseNavigation,

and render the current page with NavigationHost. The system manages

back/forward stacks, transitions, caching, and deep linking.

Quick reference

| API | Purpose |

|-----|---------|

| UseNavigation(Route.Home) | Create a NavigationHandle<Route> (call once at root) |

| UseNavigation<Route>() | Retrieve the nearest ancestor's handle (child components) |

| NavigationHost(nav, route => ...) | Render the current page |

| NavigationView([items], content) | Sidebar navigation with icons |

| TabView(Tab(...), Tab(...)) | Tab-based parallel navigation |

| UseNavigationLifecycle(...) | Page appear/disappear callbacks |

| UseSystemBackButton(nav, window) | Wire system back button to nav stack |

| DeepLinkMap<Route> | Map URI patterns to routes |

1. Defining routes

Use an enum. Each value is a distinct page:

enum Route { Home, Settings, Profile, Details }

For routes that carry data, use a discriminated-union pattern with records:

abstract record Route
{
    public record Home : Route;
    public record Settings : Route;
    public record Details(int ItemId) : Route;
}

2. Basic navigation

class App : Component
{
    public override Element Render()
    {
        var nav = UseNavigation(Route.Home);

        return VStack(12,
            HStack(8,
                Button("Home", () => nav.Navigate(Route.Home)),
                Button("Settings", () => nav.Navigate(Route.Settings)),
                Button("Back", () => nav.GoBack()).IsEnabled(nav.CanGoBack)
            ),
            NavigationHost(nav, route => route switch
            {
                Route.Home     => Component<HomePage>(),
                Route.Settings => Component<SettingsPage>(),
                _ => TextBlock("Not found")
            })
        );
    }
}

Key points:

  • UseNavigation(Route.Home) — call once at the root with the initial route.
  • nav.Navigate(route) — pushes onto the back stack.
  • nav.GoBack() / nav.GoForward() — stack navigation.
  • NavigationHost(nav, route => ...) — renders the matched element.

3. Stack operations

| Method | Effect |

|--------|--------|

| Navigate(route) | Push route onto back stack |

| GoBack() | Pop current, return to previous |

| GoForward() | Move forward (after going back) |

| Replace(route) | Swap current route without touching the stack |

| Reset(route) | Clear all stacks, start fresh |

| PopTo(predicate) | Pop until a matching route is found |

Properties: CurrentRoute, CanGoBack, CanGoForward, BackStack,

ForwardStack, Depth.

Use Reset for sign-out flows to prevent navigating back to authenticated

pages.

4. NavigationView (sidebar)

var nav = UseNavigation(Route.Home);

return NavigationView(
    [
        NavItem("Home", icon: "Home", tag: "Home"),
        NavItem("Settings", icon: "Setting", tag: "Settings"),
        NavItem("Profile", icon: "Contact", tag: "Profile")
    ],
    content: NavigationHost(nav, route => route switch
    {
        Route.Home     => VStack(Heading("Home"), TextBlock("Welcome.")).Padding(24),
        Route.Settings => VStack(Heading("Settings"), TextBlock("Configure.")).Padding(24),
        Route.Profile  => VStack(Heading("Profile"), TextBlock("Your info.")).Padding(24),
        _ => TextBlock("Not found").Padding(24)
    })
);

NavItem(label, icon?, tag?) creates sidebar items. Icons are Segoe Fluent

Icons names. The NavigationView handles selection state.

Manual wiring (without UseNavigation):

SelectedTag and OnSelectionChanged are record properties — use with { }.

For the settings item: set IsSettingsVisible = true and check args.IsSettingsSelected (not via tag).

Use .Set(nv => nv.PaneDisplayMode = ...) for non-record properties.

5. TabView (parallel tabs)

Unlike stack navigation, tabs keep all content alive simultaneously:

return TabView(
    Tab("Documents", VStack(12,
        TextBlock("Your documents."),
        Button("New", () => { })).Padding(24)),
    Tab("Recent", TextBlock("Recently opened.").Padding(24)),
    Tab("Shared", TextBlock("Shared files.").Padding(24))
);

Use tabs when users switch freely between parallel workspaces.

6. Page lifecycle

UseNavigationLifecycle fires callbacks when a page appears or disappears:

UseNavigationLifecycle(
    onNavigatedTo: ctx => LoadData(ctx.PreviousRoute),
    onNavigatingFrom: ctx =>
    {
        if (hasUnsavedChanges) ctx.Cancel(); // prevent navigation
    },
    onNavigatedFrom: ctx => SaveDraft()
);

| Callback | When |

|----------|------|

| onNavigatedTo | After this page becomes active — fetch data, start timers |

| onNavigatingFrom | Before leaving — call ctx.Cancel() to block navigation |

| onNavigatedFrom | After leaving — save drafts, stop timers |

7. Page transitions

Set Transition on NavigationHost:

NavigationHost(nav, route => ...) with
{
    Transition = NavigationTransition.DrillIn()
}

| Transition | Effect |

|-----------|--------|

| NavigationTransition.Slide() | Slide + fade (default) |

| NavigationTransition.Fade() | Crossfade |

| NavigationTransition.DrillIn() | Scale + fade — use for list→detail |

| NavigationTransition.Spring() | Spring-physics slide |

| NavigationTransition.None | Instant swap |

GoBack automatically reverses direction. Transitions run on the compositor

thread.

8. Deep linking

DeepLinkMap maps URI patterns to route constructors:

var map = UseMemo(() => new DeepLinkMap<Route>()
    .Map("/", _ => Route.Home)
    .Map("/settings", _ => Route.Settings)
    .Map("/users/{id:int}/posts/{postId:int}", args =>
    {
        var userId = args.Get<int>("id");
        var postId = args.Get<int>("postId");
        return Route.Details;
    }, backStackFactory: () => new[] { Route.Home }));

// Resolve from activation URI:
map.Resolve(uri);

Pattern segments:

| Segment | Matches |

|---------|---------|

| /literal | Exact match |

| /{param} | String capture |

| /{param:int} | Typed capture (int, long, bool, Guid) |

| /{param?} | Optional parameter |

| /{**} | Wildcard — remaining path |

| ?key=value | Query string parameters |

backStackFactory builds a synthetic back stack so GoBack works after

deep-linked entry.

9. Page caching

NavigationHost(nav, route => ...) with
{
    CacheMode = NavigationCacheMode.Enabled,
    CacheSize = 5
}

| Cache mode | Behavior |

|-----------|----------|

| Disabled | Always unmount/remount (default) |

| Enabled | LRU cache up to CacheSize entries |

| Required | Always cached, never evicted |

Caching preserves scroll position, text input, and component state.

10. State serialization

var state = nav.GetState();         // NavigationState<TRoute> snapshot
nav.SetState(state);                // restore stacks + fire Navigated(Reset)

Reactor returns a plain NavigationState<TRoute> POCO and lets you pick the

storage format. For JSON persistence pair the snapshot with a

JsonSerializerContext (AOT-safe):

[JsonSerializable(typeof(NavigationState<AppRoute>))]
partial class AppJsonContext : JsonSerializerContext { }

var json = JsonSerializer.Serialize(nav.GetState(), AppJsonContext.Default.NavigationStateAppRoute);
// later…
nav.SetState(JsonSerializer.Deserialize(json, AppJsonContext.Default.NavigationStateAppRoute)!);

Use for persisting navigation across app restarts.

11. System back button

UseSystemBackButton(nav, window);

Wires the Windows title-bar back button and hardware back button to your

navigation stack automatically.

Critical gotchas

  • Call UseNavigation(initial) once at the root. Children retrieve the

same handle via UseNavigation<Route>() (no initial value) through

context.

  • Use enums for routes. Compile-time safety — can't navigate to a

non-existent route.

  • Use Reset for sign-out. Clears the stack so users can't go back.
  • DrillIn for list→detail. Signals hierarchy; pairs with connected

animation.

  • ctx.Cancel() in onNavigatingFrom — the only way to guard

against unsaved-changes navigation.

  • Don't forget backStackFactory in deep links. Without it, GoBack

does nothing after a deep-linked entry.

Threading

UseNavigation mutators are thread-safe by default (issue #234). The

handle captures the UI thread when it is created, so calling Navigate,

GoBack, GoForward, Replace, Reset, PopTo, or SetState from a

Task.Run, a timer, or after await … ConfigureAwait(false) auto-marshals

the operation back onto the captured dispatcher — no threadSafe: flag and

no manual DispatcherQueue.TryEnqueue needed:

await Task.Run(async () => {
    var itemId = await LoadItemIdAsync();
    nav.Navigate(new Route.Details(itemId)); // marshals onto the UI thread automatically
});

This mirrors the UseState / UseReducer contract. Two caveats:

  • No live dispatcher → loud throw. Off-thread calls in a headless or

unit-test context (no ReactorApp.UIDispatcher), or after the dispatcher

has shut down, throw InvalidOperationException instead of silently

corrupting the back/forward stacks. On the UI thread the call runs inline.

  • Off-thread bool means "scheduled", not "navigated". When a mutator

marshals, its return value reports that the operation was *enqueued*, not

that the navigation ultimately succeeded. Don't branch on it from a

background thread.

How to use it

Copy the folder

Take microsoft/reactor-navigation 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.