An opinionated architecture for large-scale client-side React SPAs.
Sovereign React is a set of structural decisions -- proven at scale -- for building React applications that stay fast, organized, and maintainable as they grow. Not a framework, not a library to install. It's opinions backed by working code: a starter template with a full component library, hooks, utilities, and the architectural patterns that tie them together.
You're building a client-side React SPA with many independent feature pages -- a dashboard, admin panel, utility collection, educational platform, or internal tool. You don't need a server. You want it to scale to 50, 100, or 200+ pages without the bundle exploding or the codebase becoming unmaintainable.
Most React architecture guides stop at "organize your folders well." They don't address what happens when you have 100+ features: bundle size creeping up, metadata bloating the main chunk, features coupling to each other, new pages requiring changes in five places. Sovereign React solves for that.
- Client-side only. No backend for core functionality. Data stays on the user's device. Privacy by default.
- Feature isolation. Each page is self-contained: its own component, hook, and (optionally) core logic. No cross-feature imports.
- Bundle discipline. Heavy metadata stays out of the main bundle. ESLint enforces import boundaries mechanically -- not through code review.
- Shared infrastructure, not shared state. Reusable components, hooks, and utilities -- but no global state library. React hooks + localStorage are sufficient when features don't couple.
- Opinions over options. One way to do things, documented with rationale. When there's a choice, we already made it.
# Clone the repo
git clone https://github.com/YOUR_USERNAME/sovereign-react.git
cd sovereign-react/template
# Install and run
npm install
npm run devThe template is a working Vite + React app with:
- 2 example feature pages demonstrating the patterns
- 30 shared UI components (54 at production scale)
- 11 general-purpose hooks (18 at production scale)
- 8 utility modules (clipboard, download, formatters, validators, etc.)
- ESLint-enforced import boundaries
- Dark mode, responsive layout, accessibility basics
Delete the example pages and start building your own features.
| Doc | What it covers |
|---|---|
| Architecture | Philosophy, stack decisions (Vite, Tailwind, React Router), and why not Next.js |
| Project Structure | Folder conventions, file naming, what goes where |
| State Management | localStorage strategy, React Context, why no Redux/Zustand |
| Doc | What it covers |
|---|---|
| Routing & Registry | Two-tier registry pattern, auto-generated routes, lazy metadata |
| Bundle Discipline | ESLint import boundaries, eager vs lazy, the 89KB optimization |
| Doc | What it covers |
|---|---|
| Adding a Feature | Step-by-step checklist for creating a new page |
| Components | Shared component library API reference (30 template, 54 at scale) |
| Hooks | Hook library API reference (11 template, 18 at scale) |
| Doc | What it covers |
|---|---|
| Security | CSP headers, DOMPurify, Web Crypto, input validation |
| Testing | Vitest + React Testing Library patterns |
| Performance | Debouncing, memoization, Web Workers, code splitting |
| Mobile | 320px minimum, touch targets, responsive patterns |
| Doc | What it covers |
|---|---|
| Chaining & Pipelines | Feature-to-feature data flow, type system, pipeline execution |
| Scaling Patterns | useToolSettings, useToolHistory, SEO data layer, GameShell |
Vite + React.lazy() gives you automatic per-page code splitting with zero configuration. You write this:
{ id: 'my-feature', path: '/my-feature', component: lazy(() => import('../pages/features/MyFeaturePage')) },And Vite automatically creates a separate chunk for that page. No webpack config. No manual optimization. The page's code is only downloaded when the user navigates there. Add 200 pages and the main bundle doesn't grow -- each page is its own chunk, loaded on demand.
That's the baseline. It's free. The architecture just makes sure you don't accidentally opt out of it.
The baseline handles pages, but at 50+ features a new problem appears: metadata bloat. Icons, descriptions, categories, and tags for every feature get imported into the main bundle even though they're only needed on the Home page and in search.
Sovereign React splits this into two files:
routeManifest.js -- In the main bundle. Lightweight. Just routing data.
// ~5KB -- only what the router needs
export const routeManifest = [
{ id: 'counter', path: '/counter', component: lazy(() => import('../pages/features/CounterPage')) },
{ id: 'file-processor', path: '/file-processor', component: lazy(() => import('../pages/features/FileProcessorPage')) },
// scales to 200+ entries with minimal bundle impact
];registry.js -- Lazy-loaded. Full metadata. Icons, descriptions, categories, tags.
// ~100KB+ -- loaded on demand by Home page, search, overlays
import { Calculator, FileText, Clock } from 'lucide-react';
export const features = [
{ id: 'counter', name: 'Counter', icon: Calculator, category: 'examples', description: '...' },
// rich metadata for discovery, search, and display
];ESLint enforces this boundary. If you accidentally import registry.js in an eagerly-loaded module, the linter catches it:
error: Unexpected path "./registry" imported in restricted zone.
registry.js must not be imported in App.jsx (bloats main bundle).
Use routeManifest.js for routing data.
This pattern reduced a production app's main bundle from 126KB to 89KB gzipped.
Read more: Routing & Registry | Bundle Discipline
Every feature follows the same pattern:
src/pages/features/MyFeaturePage.jsx # Page component (layout + UI)
src/hooks/useMyFeature.js # Logic, state, handlers
src/components/features/MyFeature/ # Feature-specific subcomponents (if needed)
Page renders UI using shared components. Hook manages all logic and state. This separation means:
- Logic is testable without rendering UI
- UI is swappable without touching logic
- Features never import from each other (ESLint-enforced)
Shared logic goes to src/hooks/, src/utils/, or src/components/shared/ -- never into another feature's folder.
When features produce output that other features can consume, you can build a pipeline system on top of the architecture. This is an advanced pattern that emerged at 100+ features.
The key insight: separate processing logic from UI logic. Each chainable feature registers a pure process(input, params) function in a core registry, independent of React. A shared type system (MIME-like types: text/plain, image/png, text/json, etc.) lets the app automatically discover which features can follow which -- and suggest the next step.
The chain UI is built from composable components (ChainableOutput, ChainButton, ChainPopover) that auto-resolve feature identity from React Context. Adding chaining to a feature page is a one-line change: replace <CopyButton> with <ChainableOutput>.
Read more: Chaining & Pipelines
The template includes 30 production-ready components:
Form Controls: Button, Input, Textarea, Select, Toggle, Slider, RadioGroup Display: Card, Alert, EmptyState, LoadingSpinner, KeyValueList, ResultDisplay Actions: CopyButton, DownloadButton, FileUpload Navigation: TabGroup, ModeToggle, PresetButtons Overlays: ConfirmDialog, InfoDialog, Toast (context-based) Layout: SplitPane, FeatureLayout, ToolSection, HistoryPanel Batch Processing: BatchInput, BatchPreviewGrid, FileProcessorLayout
All components use Tailwind CSS with dark mode support. See Components Reference for full API documentation.
In production (Overtooled, 178+ features), the library grew to 54 components. Key additions: chain system UI (ChainableOutput, ChainButton, ChainPopover, ChainStepCard, ChainParamForm, ChainSuggestionList), game support (GameShell), data display (StatCard, Table, CodeBlock), and navigation (CommandPalette, SearchDropdown, RelatedTools).
11 general-purpose hooks included:
| Hook | Purpose |
|---|---|
useLocalStorage |
Persistent state with cross-instance sync via custom events |
useDebounce |
Debounce any value with configurable delay |
useClipboard |
Copy to clipboard with fallback and status tracking |
useAsync |
Async operations with loading/error state and stale-result prevention |
useFileUpload |
File reading with type/size validation |
useKeyboardShortcut |
Keyboard shortcuts with modifier support |
useInterval |
setInterval with React lifecycle management |
useUndo |
Undo/redo state history |
useMediaQuery |
Responsive breakpoint detection |
useFullscreen |
CSS-based fullscreen overlay |
useObjectURL |
Blob URL management with automatic cleanup |
See Hooks Reference for full API documentation.
In production, 7 more general-purpose hooks emerged: useToolSettings (per-feature settings), useToolHistory (per-feature history), useFavorites, useRecentTools, useStopwatch, useTimer, and useVersionCheck. The chain system added 4 specialized hooks: useChainExecution, useMultiChainTabs, useUsageGraph, and useSavedPipelines. See Scaling Patterns.
| Layer | Choice | Why |
|---|---|---|
| Build | Vite | Fast dev server, optimized production builds, minimal config |
| UI | React 18 | Component model, hooks, lazy loading |
| Styling | Tailwind CSS | Utility-first, dark mode, responsive, zero CSS file management |
| Routing | React Router v6 | Standard SPA routing, lazy-loaded routes |
| State | React hooks + localStorage | No global state needed when features are isolated |
| Testing | Vitest + React Testing Library | Fast, React-native, behavior-focused |
| Linting | ESLint + eslint-plugin-import-x | Import boundary enforcement |
This architecture powers Overtooled, a collection of 178+ client-side web utilities. At that scale:
- Main bundle: 89KB gzipped (with 178 lazy-loaded routes)
- Each feature adds ~10-20KB on demand
- No cross-feature coupling
- Adding a new feature is a 3-file operation (page + hook + registry entries)
- Feature-to-feature chaining via a type-safe pipeline system
- 54 shared components, 18 general-purpose hooks, 6 React Contexts
- Zero backend, zero server costs, instant tool responses
The patterns here aren't theoretical -- they've been battle-tested at production scale.
This is an opinionated guide, not a community-driven framework. That said:
- Bug reports and corrections are welcome
- If you've used these patterns at scale, share your experience in Discussions
- PRs for new docs or improved explanations are appreciated
MIT