Navigation

The Navigation service manages browser history and exposes reactive state for the current URL. It’s provided as an Effect context via Navigation.makeLayer and consumed in components via accessor effects or the NavigationContext tag.

Setting Up

Provide the Navigation layer at your app’s root:

import { Navigation } from "@effex/router";
import { runApp, mount } from "@effex/dom";
import { Effect } from "effect";

runApp(
  Effect.gen(function* () {
    yield* mount(App(), document.getElementById("root")!);
  }),
  { layer: Navigation.makeLayer(router) },
);

Once provided, any component in the tree can navigate or read the current route.

Programmatic Navigation

Type-Safe Route Navigation

Use Navigation.pushRoute to navigate with type-safe params:

import { Navigation } from "@effex/router";

// Params are inferred from the route definition
yield* Navigation.pushRoute(UserRoute, {
  params: { id: 123 },
});

// With search params
yield* Navigation.pushRoute(SearchRoute, {
  params: {},
  searchParams: { q: "effect", page: 2 },
});

// Replace instead of push (no new history entry)
yield* Navigation.replaceRoute(UserRoute, {
  params: { id: 456 },
});

The type safety comes from the Route object — if UserRoute has params: Schema.Struct({ id: Schema.NumberFromString }), TypeScript enforces that you pass { id: number }.

Path-Based Navigation

When you don’t need typed params:

yield* Navigation.pushPath("/users/123");
yield* Navigation.replacePath("/login");

History

yield* Navigation.back;
yield* Navigation.forward;

Reading Route State

All route state is reactive — bind it directly to your UI.

Current Pathname

const path = yield* Navigation.pathname;
// "string" — one-time read

// Or access the Readable for reactive binding:
const nav = yield* NavigationContext;
nav.pathname;  // Readable<string>

Search Params

const params = yield* Navigation.searchParams;
// URLSearchParams — one-time read

Current Match

const match = yield* Navigation.currentMatch;
// { route, params } — the currently matched route and raw params

For declarative navigation in the UI, use the Link component:

import { Link } from "@effex/router";
import { $ } from "@effex/dom";

// Path-based
Link({ href: "/users" }, $.of("Users"));

// Type-safe route-based
Link(
  { to: UserRoute, params: { id: 123 } },
  $.of("View User"),
);

// With search params
Link(
  { href: "/search", searchParams: { q: "test" } },
  $.of("Search"),
);

// Replace instead of push
Link(
  { href: "/settings", replace: true },
  $.of("Settings"),
);

// External links work normally
Link(
  { href: "https://example.com", target: "_blank" },
  $.of("External"),
);

Active State

Link automatically sets data attributes based on the current pathname:

  • data-active-exact="true" — when the href matches the current path exactly
  • data-active-prefix="true" — when the current path starts with the href

Style active links with CSS:

a[data-active-exact] {
  font-weight: bold;
}

a[data-active-prefix] {
  color: var(--primary);
}

Standard Anchor Behavior

Link renders a real <a> element. Modified clicks (Ctrl+click, Cmd+click, middle-click) work normally — they open in a new tab. Only plain left-clicks are intercepted for SPA navigation.

Outlet

The Outlet component renders the currently matched route:

import { Outlet } from "@effex/router";
import { $ } from "@effex/dom";

const App = () =>
  $.div(
    { class: "app" },
    collect(
      Header(),
      $.main({}, Outlet({ router })),
      Footer(),
    ),
  );

Outlet reads from NavigationContext, matches the current pathname against the router’s routes, and renders the matched route’s component. When the URL changes, the previous route is unmounted and the new one is rendered.

With Animations

Outlet({
  router,
  animate: {
    enterFrom: "opacity-0",
    enter: "transition-opacity duration-150",
    enterTo: "opacity-100",
    exit: "transition-opacity duration-150",
    exitTo: "opacity-0",
  },
});

The exiting route animates out, then the entering route animates in.

Guards and Layouts

Outlet handles guards and layouts automatically. If a matched route has a guard that returns false, the guard’s redirect or fallback is used. Layouts wrap the matched route inside-out, as configured on the Router.