Skip to content

chainRoute

Create a virtual route that wraps an existing route with additional lifecycle hooks and conditions. Useful for implementing guards, loading data before navigation, or creating conditional navigation flows.

API

typescript
function chainRoute<T extends object | void = void>(
  props: ChainRouteProps<T>
): VirtualRoute<RouteOpenedPayload<T>, T>

Parameters

ParameterTypeDescription
props.routeRoute<T>The route to wrap
props.beforeOpenEvent | Effect | ArrayUnit(s) to execute when route opens
props.openOnUnit | Unit[]Optional. Unit(s) that trigger the virtual route to open
props.cancelOnUnit | Unit[]Optional. Unit(s) that close the virtual route and fire cancelled

Returns

VirtualRoute<RouteOpenedPayload<T>, T> - A virtual route with all standard virtual route properties plus a cancelled event.

Usage

Basic Authorization Guard

ts
import { createRoute, chainRoute } from '@argon-router/core';
import { createEvent, createEffect, sample } from 'effector';

const profileRoute = createRoute({ path: '/profile' });

const authorized = createEvent();
const rejected = createEvent();

const checkAuthFx = createEffect(async () => {
  const isAuthorized = await checkUserAuth();
  if (!isAuthorized) throw new Error('Not authorized');
});

sample({
  clock: checkAuthFx.doneData,
  target: authorized,
});

sample({
  clock: checkAuthFx.failData,
  target: rejected,
});

const guardedProfile = chainRoute({
  route: profileRoute,
  beforeOpen: checkAuthFx,
  openOn: authorized,
  cancelOn: rejected,
});

// Redirect to login when cancelled
sample({
  clock: guardedProfile.cancelled,
  target: loginRoute.open,
});

Load Data Before Route Opens

ts
import { createRoute, chainRoute } from '@argon-router/core';
import { createEffect, sample } from 'effector';

const userRoute = createRoute<{ userId: string }>({ path: '/user/:userId' });

const loadUserDataFx = createEffect(async ({ params }: { params: { userId: string } }) => {
  return await fetchUser(params.userId);
});

const userRouteWithData = chainRoute({
  route: userRoute,
  beforeOpen: loadUserDataFx,
  openOn: loadUserDataFx.done,
});

// Use the loaded data
sample({
  clock: loadUserDataFx.doneData,
  fn: (data) => data.result,
  target: $currentUser,
});

Multiple Before Open Effects

ts
import { createRoute, chainRoute } from '@argon-router/core';
import { createEffect } from 'effector';

const dashboardRoute = createRoute({ path: '/dashboard' });

const checkAuthFx = createEffect(async () => {
  await verifyAuth();
});

const loadDashboardDataFx = createEffect(async () => {
  return await fetchDashboardData();
});

const guardedDashboard = chainRoute({
  route: dashboardRoute,
  // Effects run sequentially in order
  beforeOpen: [checkAuthFx, loadDashboardDataFx],
  openOn: loadDashboardDataFx.done,
  cancelOn: [checkAuthFx.fail, loadDashboardDataFx.fail],
});

Chain Multiple Guards

ts
import { createRoute, chainRoute } from '@argon-router/core';

const adminRoute = createRoute({ path: '/admin' });

// First check: authentication
const authenticatedRoute = chainRoute({
  route: adminRoute,
  beforeOpen: checkAuthFx,
  openOn: checkAuthFx.done,
  cancelOn: checkAuthFx.fail,
});

// Second check: admin role
const authorizedAdminRoute = chainRoute({
  route: authenticatedRoute,
  beforeOpen: checkAdminRoleFx,
  openOn: checkAdminRoleFx.done,
  cancelOn: checkAdminRoleFx.fail,
});

// Third check: load admin data
const adminRouteWithData = chainRoute({
  route: authorizedAdminRoute,
  beforeOpen: loadAdminDataFx,
  openOn: loadAdminDataFx.done,
});

Conditional Navigation

ts
import { createRoute, chainRoute } from '@argon-router/core';
import { createEvent, sample } from 'effector';

const purchaseRoute = createRoute({ path: '/purchase/:productId' });

const hasInventory = createEvent();
const outOfStock = createEvent();

const checkInventoryFx = createEffect(async ({ params }) => {
  const product = await fetchProduct(params.productId);
  if (product.stock === 0) throw new Error('Out of stock');
  return product;
});

sample({
  clock: checkInventoryFx.doneData,
  target: hasInventory,
});

sample({
  clock: checkInventoryFx.failData,
  target: outOfStock,
});

const validatedPurchase = chainRoute({
  route: purchaseRoute,
  beforeOpen: checkInventoryFx,
  openOn: hasInventory,
  cancelOn: outOfStock,
});

// Show error when out of stock
sample({
  clock: validatedPurchase.cancelled,
  fn: () => 'Product is out of stock',
  target: showErrorToast,
});

With Multiple Cancel Conditions

ts
import { createRoute, chainRoute } from '@argon-router/core';
import { createEvent } from 'effector';

const editorRoute = createRoute({ path: '/edit/:documentId' });

const documentLocked = createEvent();
const permissionDenied = createEvent();
const documentDeleted = createEvent();

const guardedEditor = chainRoute({
  route: editorRoute,
  beforeOpen: checkDocumentAccessFx,
  openOn: checkDocumentAccessFx.done,
  cancelOn: [documentLocked, permissionDenied, documentDeleted],
});

// Handle different cancellation reasons
sample({
  clock: guardedEditor.cancelled,
  source: {
    isLocked: documentLocked,
    isDenied: permissionDenied,
    isDeleted: documentDeleted,
  },
  fn: (reasons) => {
    if (reasons.isLocked) return 'Document is locked';
    if (reasons.isDenied) return 'Permission denied';
    if (reasons.isDeleted) return 'Document was deleted';
  },
  target: showErrorMessage,
});

How It Works

  1. When the original route.opened fires, beforeOpen effect(s) execute sequentially
  2. If openOn is provided, the virtual route opens when those units trigger
  3. If cancelOn units trigger, the virtual route closes and cancelled event fires
  4. The virtual route's $params store contains the transformed route parameters

Using the Cancelled Event

The virtual route includes a cancelled event that fires when navigation is prevented:

ts
const guardedRoute = chainRoute({
  route: someRoute,
  beforeOpen: checkSomethingFx,
  openOn: checkSomethingFx.done,
  cancelOn: checkSomethingFx.fail,
});

// React to cancellation
sample({
  clock: guardedRoute.cancelled,
  target: showAccessDeniedMessage,
});

Best Practices

Use for Guards

Perfect for implementing authorization, permissions, and data loading:

ts
// ✅ Good: Clear guard pattern
const guardedRoute = chainRoute({
  route: protectedRoute,
  beforeOpen: checkPermissionsFx,
  openOn: checkPermissionsFx.done,
  cancelOn: checkPermissionsFx.fail,
});

Chain Multiple Concerns

Keep each concern separate by chaining multiple guards:

ts
// ✅ Good: Separate authentication and authorization
const authenticated = chainRoute({
  route: baseRoute,
  beforeOpen: checkAuthFx,
  openOn: checkAuthFx.done,
  cancelOn: checkAuthFx.fail,
});

const authorized = chainRoute({
  route: authenticated,
  beforeOpen: checkRoleFx,
  openOn: checkRoleFx.done,
  cancelOn: checkRoleFx.fail,
});

Handle All Cancel Cases

Always handle the cancelled event to provide user feedback:

ts
sample({
  clock: guardedRoute.cancelled,
  target: redirectToLogin,
});

See Also

Released under the MIT License.