Skip to content

createRouterControls ​

Create the core navigation controls for managing browser history, URL paths, and query parameters. These controls are typically used internally by createRouter, but can also be used independently for custom routing solutions.

API ​

typescript
function createRouterControls(): RouterControls;

Returns ​

RouterControls object with the following properties:

PropertyTypeDescription
$historyStore<RouterAdapter | null>Current history adapter instance
$locationStateStore<LocationState>Current path and query state
$queryStore<Query>Current query parameters
$pathStore<string>Current pathname
setHistoryEvent<RouterAdapter>Initialize controls with history adapter
navigateEvent<NavigatePayload>Navigate to a path with query parameters
backEvent<void>Navigate back in history
forwardEvent<void>Navigate forward in history
locationUpdatedEvent<{ pathname, query }>Fires when location changes

Usage ​

Basic Setup ​

ts
import { createRouterControls, historyAdapter } from '@effector/router';
import { createBrowserHistory } from 'history';

const controls = createRouterControls();

// Initialize with browser history
controls.setHistory(historyAdapter(createBrowserHistory()));

Initialization Required

Router controls should be initialized with setHistory before use. Before initialization, navigate, back, and forward emit controls.navigationFailed with a discriminated not-initialized payload and do not throw or create a navigation attempt.

controls.initialized and controls.updated expose the same location lifecycle as Router. Reinitialization emits initialized; equal or hash-only changes do not emit updated.

ts
import { sample } from 'effector';

// Navigate to a new path
sample({
  clock: goToHomePage,
  fn: () => ({ path: '/' }),
  target: controls.navigate,
});

// Navigate with query parameters
sample({
  clock: searchSubmitted,
  fn: (searchTerm) => ({
    path: '/search',
    query: { q: searchTerm },
  }),
  target: controls.navigate,
});

// Replace current history entry
sample({
  clock: updateFilters,
  fn: (filters) => ({
    query: { filters },
    replace: true, // Don't add new history entry
  }),
  target: controls.navigate,
});

Update Query Parameters ​

ts
import { sample } from 'effector';

// Add query parameters while keeping current path
sample({
  clock: filterChanged,
  fn: (filter) => ({
    query: { filter, page: '1' },
  }),
  target: controls.navigate,
});

// Clear specific query parameter
sample({
  clock: clearSearch,
  fn: () => ({ query: { q: undefined } }),
  target: controls.navigate,
});
ts
import { sample } from 'effector';

// Browser back button
sample({
  clock: backButtonClicked,
  target: controls.back,
});

// Browser forward button
sample({
  clock: forwardButtonClicked,
  target: controls.forward,
});

Read Current State ​

ts
import { useUnit } from 'effector-react';

function CurrentLocation() {
  const path = useUnit(controls.$path);
  const query = useUnit(controls.$query);

  return (
    <div>
      <p>Path: {path}</p>
      <p>Query: {JSON.stringify(query)}</p>
    </div>
  );
}

Track Query Parameters ​

ts
import { z } from 'zod';
import { trackQuery } from '@effector/router';

const searchTracker = trackQuery({
  parameters: z.object({
    q: z.string(),
    page: z.string().default('1'),
  }),
  controls,
});

searchTracker.entered.watch(({ q, page }) => {
  console.log(`Searching for "${q}" (page ${page})`);
});

// Add or update the tracked parameters
searchTracker.enter({ q: 'router', page: '2' });
Config fieldTypeDescription
controlsRouterControlsControls that own query navigation
routesRoute[]Optional OR filter based on each route's $isOpened store
parametersZodTypeSchema used to validate and parse the tracked query parameters

The tracker reacts automatically to query and route activity. Its $state store exposes the current inactive, pending, or entered state, including when a tracker is created after history initialization. Compose one-shot behavior from that store and an application event with sample.

Server-Side Rendering ​

ts
import { createRouterControls, historyAdapter } from '@effector/router';
import { createMemoryHistory } from 'history';
import { fork, allSettled } from 'effector';

const controls = createRouterControls();
const scope = fork();

// Initialize with memory history for SSR
await allSettled(controls.setHistory, {
  scope,
  params: historyAdapter(
    createMemoryHistory({
      initialEntries: ['/products/123'],
    }),
  ),
});

Custom History Adapter ​

ts
import { createRouterControls, type RouterAdapter } from '@effector/router';

const controls = createRouterControls();

// Use custom adapter
const customAdapter = {
  location: {
    pathname: '/current-path',
    search: '?query=value',
    hash: '',
  },
  push: (location) => {
    console.log('Navigate to:', location);
  },
  replace: (location) => {
    console.log('Replace with:', location);
  },
  goBack: () => console.log('Go back'),
  goForward: () => console.log('Go forward'),
  listen: (listener) => {
    // Subscribe to location changes
    return {
      unsubscribe: () => {
        // Cleanup
      },
    };
  },
} satisfies RouterAdapter;

controls.setHistory(customAdapter);

React to Location Changes ​

ts
import { sample } from 'effector';

// Track all navigation
sample({
  clock: controls.locationUpdated,
  fn: ({ pathname, query }) => {
    console.log('Navigated to:', pathname, query);
  },
});

// Analytics tracking
sample({
  clock: controls.locationUpdated,
  fn: ({ pathname }) => ({
    event: 'pageview',
    path: pathname,
  }),
  target: sendAnalyticsFx,
});

Derive State from Path ​

ts
import { combine } from 'effector';

const $isHomePage = controls.$path.map((path) => path === '/');

const $currentSection = controls.$path.map((path) => {
  if (path.startsWith('/docs')) return 'docs';
  if (path.startsWith('/blog')) return 'blog';
  return 'home';
});

const $breadcrumbs = controls.$path.map((path) => {
  return path.split('/').filter(Boolean);
});

The navigate event accepts the following payload:

typescript
interface NavigatePayload {
  path?: string; // Optional: new pathname
  query?: Query; // Optional: query parameters
  replace?: boolean; // Optional: replace history entry (default: false)
}

When path or query is omitted, controls reuse the current value. Therefore controls.navigate({ path: '/settings' }) preserves the current query.

When query is provided, it replaces the complete query. Use query: {} to clear every key; use undefined for individual keys that should be absent. Route open, redirect targets, and framework link hrefs use the same effective URL rule, so a clicked link and native navigation resolve to the same query.

Query Type ​

Query parameters are represented as:

typescript
type Query = Record<string, string | null | Array<string | null>>;
type QueryInput = Record<
  string,
  string | null | Array<string | null> | undefined
>;

Examples:

ts
// Single value
{
  search: 'apple';
}

// Multiple values
{
  tags: ['javascript', 'typescript'];
}

// Remove parameter (undefined is omitted from the URL)
{
  filter: undefined;
}

null is serialized as a flag (?enabled), arrays use repeated keys in the same order, and object key order does not affect value equality.

Best Practices ​

Share Controls with Lower Layers ​

Create controls beside route declarations when feature models need to compose transition policy without importing the application router:

ts
// shared/routing
export const controls = createRouterControls();
export const routes = {
  /* createRoute declarations */
};

// feature model
export const leaveEditor = beforeNavigate({
  controls,
  from: routes.editor,
  filter: $dirty,
});

// app/routing
const router = createRouter({
  routes: Object.values(routes),
  controls,
});

The app remains responsible for setHistory. See Navigation lifecycle.

Initialize Early ​

Initialize history adapter as early as possible:

ts
// In app entry point
import { createBrowserHistory } from 'history';

controls.setHistory(historyAdapter(createBrowserHistory()));

Batch Query Updates ​

Use replace: true when updating query parameters frequently:

ts
// ❌ Creates multiple history entries
controls.navigate({ query: { page: '1' } });
controls.navigate({ query: { filter: 'active' } });

// ✅ Single history entry
controls.navigate({
  query: { page: '1', filter: 'active' },
  replace: true,
});

See Also ​

  • createRouter - Create complete router with controls
  • Adapters - History adapters and custom adapter creation
  • trackQuery - Track individual query parameters

Released under the MIT License.