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.
npx skills add https://github.com/microsoft/microsoft-ui-reactor --skill reactor-navigation
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.
| 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 |
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;
}
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.| 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.
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.
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.
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 |
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.
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.
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.
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.
UseSystemBackButton(nav, window);
Wires the Windows title-bar back button and hardware back button to your
navigation stack automatically.
UseNavigation(initial) once at the root. Children retrieve thesame handle via UseNavigation<Route>() (no initial value) through
context.
non-existent route.
Reset for sign-out. Clears the stack so users can't go back.animation.
ctx.Cancel() in onNavigatingFrom — the only way to guardagainst unsaved-changes navigation.
backStackFactory in deep links. Without it, GoBackdoes nothing after a deep-linked entry.
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:
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.
bool means "scheduled", not "navigated". When a mutatormarshals, its return value reports that the operation was *enqueued*, not
that the navigation ultimately succeeded. Don't branch on it from a
background thread.
Take microsoft/reactor-navigation from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
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.