# How Modly's Router Handles Navigation and State Preservation in React

> Discover how Modly's router manages navigation and state preservation with Zustand and React lazy loading. Keep your app state intact during transitions.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: internals
- Published: 2026-08-20

---

**Modly implements a minimal, store-driven routing system using Zustand for navigation state and React lazy loading for code-splitting, keeping application state intact across page transitions.**

Modly's router is a lightweight, custom-built solution that replaces traditional routing libraries with a centralized Zustand store. This architecture tracks the current page identifier and dynamically renders lazily-loaded components wrapped in React Suspense. Because navigation state persists in memory, all other application stores remain untouched when users switch between sections.

## Centralized Navigation State in Zustand

Navigation state lives in a single Zustand store at [`src/shared/stores/navStore.ts`](https://github.com/lightningpixel/modly/blob/main/src/shared/stores/navStore.ts). The store defines a union type of valid pages and exposes both the current value and a setter function.

```ts
export type Page = 'generate' | 'workflows' | 'models' | 'settings';

interface NavState {
  currentPage: Page;
  navigate: (page: Page) => void;
}

export const useNavStore = create<NavState>((set) => ({
  currentPage: 'generate',
  navigate: (page) => set({ currentPage: page })
}));

```

The `currentPage` field serves as the single source of truth for which section renders. Because Zustand stores persist for the application's lifetime, navigation state survives re-renders and component unmounting. The `navigate` function is the only mechanism that modifies this value.

## Router Component Implementation

The [`Router.tsx`](https://github.com/lightningpixel/modly/blob/main/Router.tsx) component subscribes to `currentPage` and conditionally renders the appropriate page component. It uses React Suspense to handle asynchronous loading states.

```tsx
const currentPage = useNavStore((s) => s.currentPage);
const { component: Page, wrapperClass } = ROUTES[currentPage];

return (
  <Suspense fallback={null}>
    <div className={wrapperClass}>
      <Page />
    </div>
  </Suspense>
);

```

When `currentPage` changes, the hook triggers a re-render and the new component mounts. The surrounding UI—including navigation bars, sidebars, and global providers—remains mounted and retains its own state.

## Route Configuration with Lazy Loading

Route definitions in [`src/shared/router/routes.tsx`](https://github.com/lightningpixel/modly/blob/main/src/shared/router/routes.tsx) map each `Page` identifier to a lazily-imported component and a layout-specific CSS class.

```ts
export const ROUTES: Record<Page, RouteConfig> = {
  generate:  { component: GeneratePage,  wrapperClass: 'flex flex-1 overflow-hidden' },
  workflows: { component: WorkflowsPage, wrapperClass: 'flex flex-1 overflow-hidden' },
  models:    { component: ModelsPage,    wrapperClass: 'flex-1 overflow-y-auto'      },
  settings:  { component: SettingsPage,  wrapperClass: 'flex-1 overflow-hidden'      },
};

```

Each page component is loaded via `React.lazy`, ensuring code is fetched on demand rather than bloating the initial bundle. The `wrapperClass` property provides consistent flexbox and overflow behavior per section without repeating layout logic in page components.

## State Preservation Across Navigation

The router does not implement explicit state preservation—it achieves it by architectural omission. Since navigation state is the only state the router touches, other Zustand stores continue holding data regardless of page changes.

- `workflowsStore`, `agentStore`, and custom stores all persist in memory
- Component-level state in persistent UI elements (sidebar, header) survives navigation
- Scroll positions within the `wrapperClass` container reset per page, but parent containers maintain their position

This design eliminates the need for hydration patterns or state serialization that complex routing libraries often require.

## Practical Navigation Patterns

### Triggering Navigation from UI Components

Any component can import `useNavStore` and call `navigate` directly:

```tsx
import { useNavStore } from '@shared/stores/navStore';

function NavBar() {
  const navigate = useNavStore((s) => s.navigate);
  
  return (
    <nav className="flex gap-4 p-2">
      <button onClick={() => navigate('generate')}>Generate</button>
      <button onClick={() => navigate('workflows')}>Workflows</button>
      <button onClick={() => navigate('models')}>Models</button>
      <button onClick={() => navigate('settings')}>Settings</button>
    </nav>
  );
}

```

### Adding a New Application Section

Extending the router requires three steps in [`navStore.ts`](https://github.com/lightningpixel/modly/blob/main/navStore.ts) and [`routes.tsx`](https://github.com/lightningpixel/modly/blob/main/routes.tsx):

```ts
// 1. Extend the Page type
export type Page = 'generate' | 'workflows' | 'models' | 'settings' | 'diagnostics';

```

```tsx
// 2. Create lazy-loaded component
const DiagnosticsPage = lazy(() => import('@areas/diagnostics/DiagnosticsPage'));

// 3. Add to ROUTES map
export const ROUTES: Record<Page, RouteConfig> = {
  // ...existing entries
  diagnostics: { 
    component: DiagnosticsPage, 
    wrapperClass: 'flex flex-1 overflow-hidden' 
  },
};

```

No changes to [`Router.tsx`](https://github.com/lightningpixel/modly/blob/main/Router.tsx) are necessary—the component dynamically reads from the `ROUTES` object.

### Accessing Persistent State Across Pages

Application state remains available regardless of navigation:

```tsx
import { useAgentStore } from '@shared/stores/agentStore';
import { useNavStore } from '@shared/stores/navStore';

function ModelViewer() {
  const messages = useAgentStore((s) => s.chatHistory);
  const navigate = useNavStore((s) => s.navigate);

  // Chat history persists even after navigating to Settings and back
  return (
    <div>
      <button onClick={() => navigate('settings')}>Go to Settings</button>
      <ChatHistory items={messages} />
    </div>
  );
}

```

## Key Files in Modly's Routing System

| File | Purpose |
|------|---------|
| [`src/shared/stores/navStore.ts`](https://github.com/lightningpixel/modly/blob/main/src/shared/stores/navStore.ts) | Zustand store defining `Page` type, `currentPage` state, and `navigate` action |
| [`src/shared/router/Router.tsx`](https://github.com/lightningpixel/modly/blob/main/src/shared/router/Router.tsx) | Component that subscribes to navigation state and renders active page within Suspense |
| [`src/shared/router/routes.tsx`](https://github.com/lightningpixel/modly/blob/main/src/shared/router/routes.tsx) | Route-to-component mapping with lazy imports and layout classes |

## Summary

- **Modly's router** uses a Zustand store ([`navStore.ts`](https://github.com/lightningpixel/modly/blob/main/navStore.ts)) as the single source of navigation truth, eliminating external routing dependencies
- **React Suspense** handles code-split page components, loading them on demand via `React.lazy`
- **State preservation** occurs automatically because only the page component tree swaps; all stores and persistent UI remain mounted
- **Route configuration** in [`routes.tsx`](https://github.com/lightningpixel/modly/blob/main/routes.tsx) couples components with layout classes for consistent rendering behavior
- **Navigation triggers** are decoupled from routing logic—any component can import `useNavStore` and call `navigate`

## Frequently Asked Questions

### How does Modly's router differ from React Router or Next.js routing?

Modly's solution is intentionally minimal: it stores only a page identifier in Zustand rather than managing URL history, query parameters, or server-side routing. This works because Modly is a single-page desktop application where deep-linking and SEO are not requirements. For web applications needing URL-based navigation, React Router would be more appropriate.

### Does navigation cause any state to reset in Modly?

Only component-local state inside the page component itself resets. All Zustand stores—including `navStore`, `agentStore`, and `workflowsStore`—persist unchanged. UI elements outside the `Router`'s rendered output (navigation bars, modals, toasts) also retain their state.

### Can the router handle nested routes or route parameters?

The current implementation does not support nested routing or dynamic parameters. The `Page` type is a flat union of string literals, and `ROUTES` is a single-level record. Adding nested behavior would require extending `navStore` to hold a path array or parameterized object, though this would increase complexity beyond Modly's current needs.

### What happens if a user navigates to a page before its code loads?

The `Suspense` boundary in [`Router.tsx`](https://github.com/lightningpixel/modly/blob/main/Router.tsx) renders `null` as a fallback, showing nothing until the lazy-loaded chunk arrives. For slower connections, replacing `null` with a loading spinner would improve perceived performance without structural changes.