How Modly's Router Handles Navigation and State Preservation in React
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. The store defines a union type of valid pages and exposes both the current value and a setter function.
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 component subscribes to currentPage and conditionally renders the appropriate page component. It uses React Suspense to handle asynchronous loading states.
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 map each Page identifier to a lazily-imported component and a layout-specific CSS class.
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
wrapperClasscontainer 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:
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 and routes.tsx:
// 1. Extend the Page type
export type Page = 'generate' | 'workflows' | 'models' | 'settings' | 'diagnostics';
// 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 are necessary—the component dynamically reads from the ROUTES object.
Accessing Persistent State Across Pages
Application state remains available regardless of navigation:
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 |
Zustand store defining Page type, currentPage state, and navigate action |
src/shared/router/Router.tsx |
Component that subscribes to navigation state and renders active page within Suspense |
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) 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.tsxcouples components with layout classes for consistent rendering behavior - Navigation triggers are decoupled from routing logic—any component can import
useNavStoreand callnavigate
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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →