reactor-navigation
Skill839 starsChanged 6 days agoMITView on GitHub
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.
In this repository23 other files it gives its agents.
- AGENTS.mdAGENTS.md
- analyzer-dymSkill
- coverage-upliftSkill
- perf-compareSkill
- pr-lifecycleSkill
- pr-reviewSkill
- reactor-advancedSkill
- reactor-asyncSkill
- reactor-build-and-checkSkill
- reactor-chartsSkill
- reactor-commandingSkill
- reactor-designSkill
- reactor-devtoolsSkill
- reactor-dockingSkill
- reactor-dslSkill
- reactor-formsSkill
- reactor-getting-startedSkill
- reactor-inputSkill
- reactor-recipesSkill
- reactor-windowingSkill
- reactor-wrappingSkill
- reactor-appSkill
- ChartingSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 28
Use an enum. Each value is a distinct page:
- Very specific: line 91
Use Reset for sign-out flows to prevent navigating back to authenticated pages.
- Very specific: line 121
Use .Set(nv => nv.PaneDisplayMode = ...) for non-record properties.
- Distinctive: line 137
Use tabs when users switch freely between parallel workspaces.
- Distinctive: line 208
Use a unique key within each active page subtree, including nested hosts.
- Distinctive: line 288
Use for persisting navigation across app restarts.
- Very specific: line 336
Off-thread
boolmeans "scheduled", not "navigated": When a mutator marshals, its return value reports that the operation was *enqueued*, …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Navigation in Reactor
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:
Callbacks are scoped to the nearest enclosing NavigationHost. Each nested host
dispatches lifecycle events and guards for its own navigation stack.
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.Entrance() | Slide up + fade in — the default, and what WinUI's Frame plays with no transition info |
NavigationTransition.Slide() | Slide + fade — vertical unless you pass a direction |
NavigationTransition.Fade() | Crossfade |
NavigationTransition.DrillIn() | Scale + fade — use for list→detail |
NavigationTransition.Spring() | Spring-physics slide |
NavigationTransition.None | Instant swap |
NavigationTransition.Connected(key) | Native shared-element motion |
NavigationTransition.Default is an alias for Entrance(). Use Entrance() when you
mean the motion; use Default only when you mean "whatever the framework default is".
GoBack automatically reverses direction. Transitions run on the compositor thread.
Shared elements with independent page motion
Mark the source and destination with the same .ConnectedAnimation(key) and
select that key for the navigation:
nav.Navigate(Route.Details, new NavigateOptions
{
ConnectedAnimationKey = "device-card",
Transition = NavigationTransition.DrillIn(),
});The shared element and page transition run together. Transition = null uses
the host's default; NavigationTransition.None selects shared-element-only
motion. NavigationTransition.Connected(key) is a shorthand for that latter case.
The key and page transition travel together on back/forward navigation.
Use a unique key within each active page subtree, including nested hosts. The
destination can appear after its first render or effect: navigation waits for
its measured bounds before starting native motion. Missing, duplicate, or
zero-sized destinations fall back to the selected page transition. Reduced
motion completes the navigation immediately. Both motions finish before the
old page is cached or unmounted and lifecycle completion callbacks run.
Interrupted navigation still delivers the outgoing page's onNavigatedFrom;
the current destination receives onNavigatedTo when its transition completes.
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 viaUseNavigation<Route>()(no initial value) through context. - Use enums for routes. Compile-time safety — can't navigate to a non-existent route.
- Use
Resetfor sign-out. Clears the stack so users can't go back. - DrillIn for list→detail. Signals hierarchy; pairs with connected animation.
ctx.Cancel()inonNavigatingFrom— the only way to guard against unsaved-changes navigation.- Don't forget
backStackFactoryin 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, throwInvalidOperationExceptioninstead of silently corrupting the back/forward stacks. On the UI thread the call runs inline. - Off-thread
boolmeans "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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

