- name
- reactor-navigation
- description
- 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.
# 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:
```csharp
enum Route { Home, Settings, Profile, Details }
```
For routes that carry data, use a discriminated-union pattern with records:
```csharp
abstract record Route
{
public record Home : Route;
public record Settings : Route;
public record Details(int ItemId) : Route;
}
```
## 2. Basic navigation
```csharp
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)
```csharp
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:
```csharp
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.
```csharp
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`:
```csharp
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:
```csharp
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:
```csharp
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
```csharp
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
```csharp
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):
```csharp
[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
```csharp
UseSystemBackButton(nav, window);
```
Wires the Windows title-bar back button and hardware back button to your
navigation stack automatically.
## Critical gotchas
1. **Call `UseNavigation(initial)` once at the root.** Children retrieve the
same handle via `UseNavigation<Route>()` (no initial value) through
context.
2. **Use enums for routes.** Compile-time safety — can't navigate to a
non-existent route.
3. **Use `Reset` for sign-out.** Clears the stack so users can't go back.
4. **DrillIn for list→detail.** Signals hierarchy; pairs with connected
animation.
5. **`ctx.Cancel()` in `onNavigatingFrom`** — the only way to guard
against unsaved-changes navigation.
6. **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:
```csharp
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.
GitHubで見る