A signal-based state system provides efficient reactive updates by tracking dependencies and automatically invalidating derived computations when underlying data changes.
1. Problem Statement & Requirements
Clarifying questions
- What's the target browser support matrix? Are we targeting modern browsers only (Chrome 90+, Firefox 80+, Safari 14+) or do we need to support older browsers like IE11 or mobile Safari 12?
- What's the expected scale of state updates? Are we talking about hundreds of concurrent subscribers or millions of state changes per second?
- Do we need to support server-side rendering (SSR) and client hydration? If so, how should we handle state serialization/deserialization between server and client?
- What's the expected frequency of state changes? Is this a high-frequency update system (e.g., real-time chat) or more infrequent (e.g., user preferences)?
- Are there specific performance targets? What are the target Core Web Vitals (LCP, INP, CLS) we need to meet?
- What's the expected team size and ownership model? Is this a shared system across multiple teams or owned by one specific team?
- What's the expected lifetime of state signals? Should signals be automatically cleaned up or do we need long-lived signal references?
Functional requirements
- Signal-based state management - Components should be able to subscribe to specific state signals and automatically re-render when those signals change
- Immutable signal updates - Signal values should be immutable and updates should create new signal instances rather than mutating existing ones
- Signal composition - Signals should be composable, allowing for derived signals that depend on other signals
- Signal subscription management - Components should be able to subscribe and unsubscribe from signals cleanly
- Cross-component communication - Signals should support communication between unrelated components through shared state sources
Non-functional requirements
- Performance - Signal updates should be as fast as possible with minimal memory overhead
- Memory efficiency - Signal systems should not cause memory leaks or excessive garbage collection
- Type safety - Full TypeScript support with strict type checking for signal values and subscriptions
- Developer experience - Clear error messages, debugging tools, and intuitive API design
- Scalability - System should handle thousands of concurrent signal subscriptions efficiently
Constraints and assumptions
- TypeScript ecosystem - Assumption that the codebase uses TypeScript with strict mode enabled
- React ecosystem - The system will integrate with React components and follow React best practices
- No external dependencies - Implementation should not rely on third-party libraries for core signal functionality (though utility libraries like lodash may be acceptable)
- Single-threaded execution - Assumption that all signal operations happen on the main thread
- No server-side rendering concerns - For MVP, we'll assume client-side only operation
Out of scope
- Server-side rendering integration - Initial implementation will not support SSR hydration or server-side state synchronization
- Cross-tab communication - Signals won't support communication between browser tabs or windows
- Signal persistence - Signals will not persist across page reloads or session storage
- Advanced signal operators - Complex signal transformation operators (e.g.,
merge,switchMap) are out of scope for the initial design - Signal observability tools - Developer tooling or debugging utilities for signal state are not part of the core implementation
- Multiple signal sources - The system will only support a single global signal registry for simplicity
2. Component Architecture
graph TD
A[SignalStateProvider] --> B[SignalManager]
A --> C[SignalConsumer]
B --> D[SignalStore]
B --> E[SignalRegistry]
C --> F[UseSignalHook]
F --> G[SignalSubscription]
F --> H[SignalValue]
E --> I[SignalEventBus]
Component Tree Structure
The component architecture follows a clear separation of concerns with a provider pattern:
- SignalStateProvider: Root context provider that initializes the signal system
- SignalManager: Central orchestrator that manages signal lifecycle and subscriptions
- SignalConsumer: Smart components that consume signals via custom hooks
- UseSignalHook: Custom React hook that provides signal values and subscription management
- SignalStore: Internal state store for signal values and metadata
- SignalRegistry: Registry of all active signals with their dependencies
Component Classification
Smart Components (Container):
SignalStateProvider- Context provider that initializes and manages the signal systemSignalManager- Central orchestrator for signal lifecycle managementUseSignalHook- Custom hook that provides reactive signal consumption
Dumb Components (Presentational):
SignalValue- Display component for individual signal valuesSignalSubscription- Component that renders based on signal subscriptions
TypeScript Props Contracts
// SignalStateProvider props
interface SignalStateProviderProps {
children: React.ReactNode;
initialState?: Record<string, any>;
maxRetries?: number;
retryDelay?: number;
}
// SignalConsumer props
interface SignalConsumerProps {
signalName: string;
render?: (value: any, isLoading: boolean, error: Error | null) => React.ReactNode;
fallback?: React.ReactNode;
loading?: React.ReactNode;
onError?: (error: Error) => void;
}
// UseSignalHook return type
interface SignalHookResult {
value: any;
isLoading: boolean;
error: Error | null;
refetch: () => void;
subscribe: (callback: (value: any) => void) => () => void;
update: (newValue: any) => void;
}
// SignalManager props
interface SignalManagerProps {
children: React.ReactNode;
signals: Record<string, SignalConfig>;
}
// SignalConfig type
interface SignalConfig {
initialValue?: any;
dependencies?: string[];
updateStrategy?: 'immediate' | 'debounced' | 'throttled';
debounceMs?: number;
throttleMs?: number;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Loading : subscribe()
Loading --> Success : dataReady()
Loading --> Error : error()
Success --> Idle : unsubscribe()
Error --> Idle : retry()
Error --> Loading : retrying()
Component Responsibilities
SignalStateProvider
- Initializes the signal system context
- Manages global configuration (initial state, retry logic)
- Provides the signal store and registry to child components
SignalManager
- Registers and manages all signals in the system
- Handles signal lifecycle (creation, updates, cleanup)
- Coordinates between signal consumers and store
UseSignalHook
- Implements the reactive pattern for signal consumption
- Handles subscription lifecycle and cleanup
- Provides error boundaries and loading states
SignalStore
- Maintains actual signal values in memory
- Handles value updates and change notifications
- Implements caching and normalization logic
Reusability Decisions
The architecture prioritizes:
- High reusability through context-based state management
- Type safety with TypeScript interfaces for all components
- Composability - consumers can work with any signal regardless of its source
- Testability - each component has well-defined inputs and outputs
Component Lifecycle Considerations
- Mount: Signal consumers register for updates
- Update: Value changes trigger re-renders in subscribed components
- Unmount: Signal consumers clean up subscriptions to prevent memory leaks
- Error Handling: Graceful degradation when signals fail to load or update
// Example usage of the signal consumer pattern
const UserProfile = () => {
const { value: user, isLoading, error } = useSignal('userProfile');
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
3. State Management Design
Local state vs. global state decisions
For a signal-based state system, we'll adopt a hybrid approach:
- Local state: Component-specific state managed with React's useState/useReducer
- Global state: Cross-component shared state managed through our signal system
- Server state: Data fetched from APIs managed by React Query or similar
The key insight is that signals provide a more declarative way to manage global state than traditional context or Redux, while still allowing for efficient re-renders.
State management approach
I'll implement a custom signal-based state system built on top of React's built-in hooks, rather than adopting external libraries like Zustand or Jotai. This approach offers:
- TypeScript-first design with full type inference
- Zero-runtime dependencies for core signal functionality
- React-native compatible - signals work in both web and React Native
- Developer experience similar to MobX or Vue's reactivity systems
State shape and normalization strategy
// Signal-based state structure
interface SignalState {
user: {
id: string;
name: string;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
};
ui: {
activeTab: string;
loadingStates: Record<string, boolean>;
errors: Record<string, string>;
};
data: {
posts: Post[];
comments: Comment[];
};
}
Server state vs. client state separation
Server state (fetched data) will be managed by React Query, while client state (UI state, preferences, etc.) is handled by our signal system. This separation ensures:
- Server data is properly cached and invalidated
- Client state updates are immediate and efficient
- Clear ownership boundaries between data sources
Cache invalidation strategy
Our signal system will support:
- Explicit invalidation via
signal.invalidate() - Time-based expiration for volatile data
- Dependency tracking to automatically invalidate when related signals change
Optimistic updates design
Optimistic updates will be handled through:
- Signal-based transaction API that batches updates
- Undo/redo capability for user actions
- Error rollback that restores previous signal values
Critical-path hook implementation
// Signal system core implementation
type Signal<T> = {
get: () => T;
set: (value: T) => void;
subscribe: (callback: () => void) => () => void;
invalidate: () => void;
};
type SignalStore = {
[key: string]: Signal<any>;
};
// Core signal implementation
class SignalSystem {
private store: SignalStore = {};
private subscribers: Map<string, Set<() => void>> = new Map();
createSignal<T>(key: string, initialValue: T): Signal<T> {
let value = initialValue;
const subscribers = new Set<() => void>();
return {
get: () => value,
set: (newValue: T) => {
value = newValue;
subscribers.forEach(callback => callback());
},
subscribe: (callback: () => void) => {
subscribers.add(callback);
return () => subscribers.delete(callback);
},
invalidate: () => {
// Trigger revalidation for dependent signals
subscribers.forEach(callback => callback());
}
};
}
getSignal<T>(key: string): Signal<T> | undefined {
return this.store[key] as Signal<T>;
}
createComputed<T>(key: string, computeFn: () => T): Signal<T> {
let value = computeFn();
const subscribers = new Set<() => void>();
// Create dependency tracking
const dependencies = new Set<string>();
return {
get: () => value,
set: (newValue: T) => {
value = newValue;
subscribers.forEach(callback => callback());
},
subscribe: (callback: () => void) => {
subscribers.add(callback);
return () => subscribers.delete(callback);
},
invalidate: () => {
// Recompute value and notify subscribers
const oldValue = value;
value = computeFn();
if (value !== oldValue) {
subscribers.forEach(callback => callback());
}
}
};
}
}
// Global signal system instance
const signalSystem = new SignalSystem();
// Custom hook for signal-based state management
export function useSignal<T>(key: string, initialValue: T): [T, (value: T) => void] {
const signal = useMemo(() => {
return signalSystem.createSignal<T>(key, initialValue);
}, [key]);
const [value, setValue] = useState<T>(signal.get());
useEffect(() => {
const unsubscribe = signal.subscribe(() => {
setValue(signal.get());
});
return unsubscribe;
}, [signal]);
const setSignalValue = useCallback((newValue: T) => {
signal.set(newValue);
}, [signal]);
return [value, setSignalValue];
}
// Computed signal hook - for derived state
export function useComputed<T>(key: string, computeFn: () => T): T {
const signal = useMemo(() => {
return signalSystem.createComputed<T>(key, computeFn);
}, [key, computeFn]);
const [value, setValue] = useState<T>(signal.get());
useEffect(() => {
const unsubscribe = signal.subscribe(() => {
setValue(signal.get());
});
return unsubscribe;
}, [signal]);
return value;
}
// Example usage in a component
interface UserPreferences {
theme: 'light' | 'dark';
notifications: boolean;
}
const useUserPreferences = () => {
const [preferences, setPreferences] = useSignal<UserPreferences>(
'user/preferences',
{ theme: 'light', notifications: true }
);
const toggleTheme = () => {
setPreferences({
...preferences,
theme: preferences.theme === 'light' ? 'dark' : 'light'
});
};
const toggleNotifications = () => {
setPreferences({
...preferences,
notifications: !preferences.notifications
});
};
return {
preferences,
toggleTheme,
toggleNotifications
};
};
// Example computed signal for derived state
const useUserDisplayName = () => {
const [user] = useSignal<{ name: string; email: string }>(
'user/profile',
{ name: '', email: '' }
);
return useComputed<string>(
'user/displayName',
() => user.name || user.email.split('@')[0]
);
};
This implementation provides:
- Type-safe signal system with proper TypeScript inference
- Efficient reactivity through subscription model
- Computed signals that automatically update when dependencies change
- Memory management with proper cleanup of subscribers
- Extensible design that can be enhanced with additional features like transaction support or time-based invalidation
The signal system is designed to be lightweight, performant, and highly composable - making it ideal for complex frontend state management while maintaining the simplicity of React's built-in hooks.
4. Routing & Navigation
Route Structure and Hierarchy
The signal-based state system will be integrated into a feature-rich application with the following route structure:
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
A --> D[Shared Features]
B --> E[Login]
B --> F[Signup]
B --> G[Forgot Password]
C --> H[Dashboard]
C --> I[Profile]
C --> J[Settings]
D --> K[Signal Explorer]
D --> L[Signal History]
D --> M[Signal Configuration]
Protected routes will require authentication via JWT tokens, while public routes are accessible without authentication. The shared features section contains components that can be used across different route contexts.
Code Splitting Strategy
Route-based code splitting will be implemented using React.lazy and Suspense for optimal bundle size management:
-
Dashboard Route (
/dashboard) - 200KB bundle size target- Lazy-loaded components: DashboardAnalytics, SignalList, UserStats
- Critical path: User dashboard view with real-time signal updates
-
Signal Explorer Route (
/signals/explorer) - 350KB bundle size target- Lazy-loaded components: SignalFilter, SignalTable, SignalDetails
- Critical path: Interactive signal exploration with filtering capabilities
-
Settings Route (
/settings) - 150KB bundle size target- Lazy-loaded components: UserPreferences, NotificationSettings, Security
- Critical path: User configuration and preferences management
Navigation Guards and Authentication Flows
Authentication flow will be handled through route guards:
// auth-guard.ts
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/auth';
export const AuthGuard = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
const { isAuthenticated, isLoading } = useAuthStore();
useEffect(() => {
if (!isAuthenticated && !isLoading) {
navigate('/login', { replace: true });
}
}, [isAuthenticated, isLoading, navigate]);
if (isLoading) return <div>Loading...</div>;
return children;
};
Deep Linking Support and URL Parameter Design
URL parameters will follow a consistent naming convention:
// Route parameter types
interface SignalRoutes {
signalId: string; // UUID format
filterType: 'all' | 'active' | 'inactive';
viewMode: 'list' | 'grid' | 'chart';
dateRange: string; // ISO format: YYYY-MM-DD
}
// Example URLs:
// /signals/explorer?filterType=active&viewMode=chart&dateRange=2023-12-01
// /signals/12345678-1234-1234-1234-123456789012/details
Browser History Management and Scroll Restoration
Scroll restoration will be handled using React Router's built-in capabilities with custom logic:
// scroll-restoration.ts
import { useEffect } from 'react';
import { useLocation, useNavigation } from 'react-router-dom';
export const ScrollRestoration = () => {
const location = useLocation();
const navigation = useNavigation();
useEffect(() => {
if (navigation.state === 'idle') {
window.scrollTo(0, 0);
}
}, [location, navigation.state]);
return null;
};
The scroll restoration will be implemented at the route level to ensure proper behavior when navigating between different signal views, with special handling for complex components like the signal explorer that may have internal scrollable content.
Route Configuration Example
// routes.ts
import { lazy } from 'react';
import { createBrowserRouter } from 'react-router-dom';
const Dashboard = lazy(() => import('../pages/Dashboard'));
const SignalExplorer = lazy(() => import('../pages/SignalExplorer'));
const Login = lazy(() => import('../pages/Login'));
export const router = createBrowserRouter([
{
path: '/',
element: <AuthGuard><Dashboard /></AuthGuard>,
loader: dashboardLoader,
},
{
path: '/signals/explorer',
element: <AuthGuard><SignalExplorer /></AuthGuard>,
loader: signalExplorerLoader,
},
{
path: '/login',
element: <Login />,
},
]);
This routing strategy ensures optimal performance through code splitting, maintains proper authentication flow, and supports deep linking for all major user journeys within the signal-based state system.
5. API Integration Layer
API Client Design Pattern
We'll implement a custom fetch wrapper with axios as the underlying HTTP client to provide consistent request/response handling and error management. The client will be configured with base URL, default headers, and interceptors for authentication and error handling.
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000';
const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for adding auth token
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for handling errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle token expiration
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
Request/Response Interceptors
Authentication token injection is handled in the request interceptor, while response interception handles 401 errors by redirecting to login. The client also normalizes responses and transforms errors for consistent handling.
Error Handling and Retry Strategy
We implement a retry mechanism with exponential backoff for transient network errors (5xx, 408, 429) with a maximum of 3 retries. All API errors are normalized into standardized error objects that include status code, message, and optional details.
// api/errorHandler.ts
export interface ApiError {
status: number;
message: string;
details?: any;
isRetryable?: boolean;
}
export const handleApiError = (error: any): ApiError => {
if (error.response) {
const { status, data } = error.response;
return {
status,
message: data.message || `API Error: ${status}`,
details: data.details,
isRetryable: [500, 502, 503, 504, 408, 429].includes(status),
};
}
if (error.request) {
return {
status: 0,
message: 'Network error - please check your connection',
isRetryable: true,
};
}
return {
status: 0,
message: error.message || 'Unknown error occurred',
isRetryable: false,
};
};
Loading, Error, and Empty States Design
We implement a consistent state management pattern across all API consumers using a unified loading/error state object:
// types/api.ts
export interface ApiState<T> {
data: T | null;
loading: boolean;
error: ApiError | null;
refetch: () => void;
}
Data Fetching Strategy
For a signal-based state system, we use CSR (Client-Side Rendering) with React Query for optimal data fetching and caching. React Query provides automatic revalidation, background updates, and efficient cache management that aligns well with signal-based reactive updates.
API Response Transformation Layer
We implement a DTO (Data Transfer Object) to Domain Model mapping layer that ensures type safety and clean separation between API responses and our internal domain models:
// api/transformers.ts
import { User, Post } from '../types/domain';
export const transformUserResponse = (dto: any): User => ({
id: dto.id,
name: dto.name,
email: dto.email,
avatarUrl: dto.avatar_url,
});
export const transformPostResponse = (dto: any): Post => ({
id: dto.id,
title: dto.title,
content: dto.content,
authorId: dto.author_id,
createdAt: new Date(dto.created_at),
});
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant C as Component
participant Q as React Query
participant A as API Client
participant S as Signal System
C->>Q: useQuery({ queryKey, queryFn })
Q->>A: fetch data
A->>A: apply interceptors
A-->>Q: response
Q->>S: notify signal subscribers
S-->>C: update component state
C->>C: re-render with new data
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): < 2.5s for 90% of users
- Interaction to Next Paint (INP): < 200ms for 90% of interactions
- Cumulative Layout Shift (CLS): < 0.1 for 90% of users
Bundle Size Budget per Route
- Home/Landing: < 500KB (including all dependencies)
- Dashboard/Profile: < 800KB (with charts, tables, forms)
- Settings/Preferences: < 300KB
- Analytics/Reports: < 1.2MB (with heavy charting libraries)
Critical Rendering Path Optimization
// Critical CSS extraction and inlining strategy
const criticalCss = `
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
// Preload critical resources
const preloadCriticalResources = () => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/critical.css';
document.head.appendChild(link);
};
Image Optimization Strategy
- Format selection: WebP for modern browsers, JPEG fallback
- Lazy loading:
loading="lazy"attribute with Intersection Observer - Responsive images:
srcsetwith multiple sizes,sizesattribute
const OptimizedImage = ({ src, alt, sizes }: { src: string; alt: string; sizes?: string }) => {
return (
<img
src={src}
alt={alt}
loading="lazy"
sizes={sizes || "(max-width: 768px) 100vw, 50vw"}
srcSet={`
${src}?w=320 320w,
${src}?w=640 640w,
${src}?w=1024 1024w,
${src}?w=1920 1920w
`}
/>
);
};
Font Loading Strategy
// Font loading with font-display: swap strategy
const preloadFonts = () => {
const fontLinks = [
{ href: '/fonts/inter-regular.woff2', weight: '400' },
{ href: '/fonts/inter-bold.woff2', weight: '700' }
];
fontLinks.forEach(({ href, weight }) => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'font';
link.crossOrigin = 'anonymous';
link.href = href;
link.setAttribute('data-font-weight', weight);
document.head.appendChild(link);
});
};
Memoization Strategy
// React.memo for components with expensive renders
const ExpensiveComponent = React.memo(({ data, onChange }: { data: any; onChange: () => void }) => {
const processedData = useMemo(() => {
return data.map((item: any) => ({
...item,
processed: expensiveCalculation(item.value)
}));
}, [data]);
return (
<div>
{processedData.map((item: any) => (
<div key={item.id}>{item.processed}</div>
))}
</div>
);
});
// useCallback for stable event handlers
const useStableCallback = (callback: () => void) => {
return useCallback(callback, []);
};
Virtualization for Large Lists
// React Window implementation for large lists
import { FixedSizeList as List } from 'react-window';
const VirtualizedList = ({ items }: { items: any[] }) => {
const Row = ({ index, style }: { index: number; style: any }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<List
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
};
Prefetching and Preloading Strategy
// Route-based prefetching with Intersection Observer
const usePrefetch = (route: string) => {
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Prefetch critical data for route
prefetchRouteData(route);
}
});
});
const element = document.getElementById(`prefetch-${route}`);
if (element) {
observer.observe(element);
}
return () => observer.disconnect();
}, [route]);
};
// Prefetch critical data
const prefetchRouteData = async (route: string) => {
const prefetchUrls = {
dashboard: ['/api/reports', '/api/notifications'],
profile: ['/api/user', '/api/preferences']
};
const urls = prefetchUrls[route as keyof typeof prefetchUrls] || [];
// Concurrent pre-fetching
await Promise.all(
urls.map(url => fetch(url, { method: 'GET' }))
);
};
7. Error Handling & Resilience
Error Boundary Placement Strategy
Error boundaries should be strategically placed to prevent cascading failures while maintaining user experience. The placement follows a hierarchical approach:
- Application-level error boundary - Catches unhandled errors at the root level
- Feature-specific boundaries - Wrap major feature components (e.g., dashboard, forms)
- Component-level boundaries - For critical sub-components that might fail independently
graph TD
A[App Root] --> B[ErrorBoundary]
B --> C[DashboardFeature]
B --> D[UserSettingsFeature]
C --> E[DashboardLayout]
E --> F[ChartComponent]
E --> G[DataGridComponent]
F --> H[ErrorBoundary]
G --> I[ErrorBoundary]
Global Error Handler Design
A centralized error handler manages all uncaught exceptions and provides consistent user feedback:
// src/core/error/ErrorManager.ts
interface ErrorContext {
component?: string;
operation?: string;
timestamp: number;
}
class ErrorManager {
private errorListeners: Array<(error: Error, context?: ErrorContext) => void> = [];
private errorReportingService: ErrorReportingService;
constructor() {
this.errorReportingService = new ErrorReportingService();
// Global error handler for uncaught exceptions
window.addEventListener('error', (event) => {
this.handleGlobalError(event.error, { operation: 'window.error' });
});
// Promise rejection handler
window.addEventListener('unhandledrejection', (event) => {
this.handleGlobalError(event.reason, { operation: 'unhandled.promise.rejection' });
});
}
handleGlobalError(error: Error, context?: ErrorContext): void {
const errorWithContext = new ErrorWithMetadata(error, context);
// Notify listeners
this.errorListeners.forEach(listener => listener(errorWithContext, context));
// Report to monitoring service
this.errorReportingService.report(errorWithContext);
// Show user-friendly error
if (typeof window !== 'undefined') {
this.showUserError(errorWithContext);
}
}
addListener(listener: (error: Error, context?: ErrorContext) => void): void {
this.errorListeners.push(listener);
}
private showUserError(error: ErrorWithMetadata): void {
// Show toast or fallback UI
const toast = new Toast({
message: 'Something went wrong. Please try again.',
type: 'error',
duration: 5000
});
toast.show();
}
}
interface ErrorWithMetadata extends Error {
context?: ErrorContext;
timestamp: number;
}
Offline / Network Failure Handling
Implement robust offline detection and graceful degradation:
// src/core/network/NetworkManager.ts
class NetworkManager {
private isOnline: boolean = navigator.onLine;
private offlineListeners: Array<() => void> = [];
private onlineListeners: Array<() => void> = [];
constructor() {
window.addEventListener('online', () => this.handleOnline());
window.addEventListener('offline', () => this.handleOffline());
}
private handleOnline(): void {
this.isOnline = true;
this.onlineListeners.forEach(listener => listener());
}
private handleOffline(): void {
this.isOnline = false;
this.offlineListeners.forEach(listener => listener());
}
isNetworkAvailable(): boolean {
return this.isOnline;
}
onOffline(callback: () => void): void {
this.offlineListeners.push(callback);
}
onOnline(callback: () => void): void {
this.onlineListeners.push(callback);
}
// Retry logic with exponential backoff
async retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
throw lastError;
}
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
await this.delay(delay);
}
}
throw lastError!;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Graceful Degradation Patterns
Implement fallback strategies for critical features:
// src/core/error/GracefulDegradation.ts
interface FallbackStrategy {
component: React.ComponentType<any>;
message?: string;
priority: number;
}
class GracefulDegradation {
private fallbackStrategies: Map<string, FallbackStrategy> = new Map();
registerFallback(
featureName: string,
strategy: FallbackStrategy
): void {
this.fallbackStrategies.set(featureName, strategy);
}
getFallback(
featureName: string,
error?: Error
): FallbackStrategy | null {
const strategy = this.fallbackStrategies.get(featureName);
if (!strategy) return null;
// Apply conditional fallback logic
if (error && error.message.includes('network')) {
return {
...strategy,
component: NetworkErrorFallback,
message: 'Network error - showing cached data'
};
}
return strategy;
}
}
const NetworkErrorFallback: React.FC = () => (
<div className="fallback-network-error">
<p>Network unavailable. Showing cached data.</p>
<button onClick={() => window.location.reload()}>
Retry Connection
</button>
</div>
);
User Feedback on Errors
Comprehensive error feedback system with multiple channels:
// src/core/error/ToastManager.ts
interface ToastOptions {
message: string;
type?: 'success' | 'error' | 'warning' | 'info';
duration?: number;
closable?: boolean;
}
class ToastManager {
private toasts: Array<Toast> = [];
private toastContainer: HTMLElement;
constructor() {
this.toastContainer = this.createToastContainer();
document.body.appendChild(this.toastContainer);
}
private createToastContainer(): HTMLElement {
const container = document.createElement('div');
container.className = 'toast-container';
return container;
}
show(options: ToastOptions): void {
const toast = new Toast(options);
this.toasts.push(toast);
// Render toast
this.toastContainer.appendChild(toast.element);
// Auto-remove after duration
if (options.duration) {
setTimeout(() => this.remove(toast), options.duration);
}
}
remove(toast: Toast): void {
const index = this.toasts.indexOf(toast);
if (index !== -1) {
this.toasts.splice(index, 1);
toast.element.remove();
}
}
}
class Toast {
element: HTMLElement;
constructor(private options: ToastOptions) {
this.element = this.createElement();
}
private createElement(): HTMLElement {
const toast = document.createElement('div');
toast.className = `toast toast--${this.options.type || 'info'}`;
toast.innerHTML = `
<div class="toast__content">
${this.options.message}
</div>
${this.options.closable ?
'<button class="toast__close">×</button>' : ''}
`;
return toast;
}
}
Error Boundary Implementation
// src/core/error/ErrorBoundary.tsx
import React from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: React.ErrorInfo;
}
interface ErrorBoundaryProps {
fallbackComponent?: React.ComponentType<{
error?: Error;
resetErrorBoundary: () => void;
}>;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
children: React.ReactNode;
}
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
this.props.onError?.(error, errorInfo);
// Log to console for debugging
console.error('ErrorBoundary caught:', error, errorInfo);
}
resetErrorBoundary = (): void => {
this.setState({ hasError: false, error: undefined });
};
render(): React.ReactNode {
if (this.state.hasError) {
const { fallbackComponent: Fallback } = this.props;
if (Fallback) {
return <Fallback error={this.state.error} resetErrorBoundary={this.resetErrorBoundary} />;
}
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>We're sorry, but an error occurred while rendering this component.</p>
<button onClick={this.resetErrorBoundary}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Offline Fallback UI
// src/core/error/OfflineFallback.tsx
import React from 'react';
const OfflineFallback: React.FC = () => (
<div className="offline-fallback">
<div className="offline-icon">⚠️</div>
<h3>You're offline</h3>
<p>Some features may be limited until you reconnect.</p>
<div className="offline-actions">
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
</div>
</div>
);
export default OfflineFallback;
Error Recovery Strategy
Implement error recovery patterns for common failure scenarios:
// src/core/error/ErrorRecovery.ts
class ErrorRecovery {
private recoveryStrategies: Map<string, RecoveryStrategy> = new Map();
registerRecovery(
errorType: string,
strategy: RecoveryStrategy
): void {
this.recoveryStrategies.set(errorType, strategy);
}
async attemptRecovery(
error: Error,
context?: RecoveryContext
): Promise<boolean> {
const strategy = this.recoveryStrategies.get(error.name);
if (!strategy) return false;
try {
await strategy.recover(error, context);
return true;
} catch (recoveryError) {
console.error('Recovery failed:', recoveryError);
return false;
}
}
}
interface RecoveryContext {
retryCount: number;
lastAttempt: Date;
operation?: string;
}
interface RecoveryStrategy {
recover: (error: Error, context?: RecoveryContext) => Promise<void>;
}
8. Testing Strategy
Unit Testing Approach
For unit testing, we'll focus on component-level and utility function testing using Jest and React Testing Library. The strategy includes:
- Component Tests: 80%+ coverage for all smart components, with particular attention to edge cases and error states
- Utility Functions: 100% coverage for all custom hooks, state transformation functions, and data processing utilities
- State Management: Test all signal-based state transitions and side-effect handlers with mock dependencies
// Example unit test for a signal-based component
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SignalComponent } from './SignalComponent';
describe('SignalComponent', () => {
it('renders with initial state', () => {
render(<SignalComponent />);
expect(screen.getByText('Initial State')).toBeInTheDocument();
});
it('updates state on signal trigger', async () => {
const user = userEvent.setup();
render(<SignalComponent />);
await user.click(screen.getByRole('button', { name: 'Update State' }));
expect(screen.getByText('Updated State')).toBeInTheDocument();
});
});
Integration Testing Strategy
Integration tests will verify the interaction between components and the signal system:
- Signal Propagation: Test that signals correctly propagate through component hierarchies
- State Synchronization: Validate that multiple components observing the same signal stay in sync
- Nested Signal Handling: Test complex nested signal structures and their cleanup
E2E Testing Scope
End-to-end testing will cover critical user journeys using Playwright:
- Core User Flows: Signal creation, propagation, and cleanup workflows
- Multi-User Scenarios: Simulate concurrent signal updates from different sources
- Performance Regression: Test that signal handling doesn't cause performance degradation
Visual Regression Testing
Implement visual regression testing using Percy or Chromatic to catch UI changes:
- Signal State Transitions: Capture screenshots during state changes
- Edge Case Rendering: Test various signal combinations and their visual impact
- Responsive Behavior: Validate signal-driven UI across device sizes
Accessibility Testing Automation
Integrate axe-core for automated accessibility testing:
import { axe, toHaveNoViolations } from 'axe-core';
import { render } from '@testing-library/react';
expect.extend(toHaveNoViolations);
it('should not have any automatically detectable accessibility issues', async () => {
const { container } = render(<SignalComponent />);
const results = await axe.run(container);
expect(results).toHaveNoViolations();
});
Test Data Strategy
Create comprehensive test data fixtures:
- Signal State Variants: Multiple signal configurations (empty, populated, error states)
- Component Scenarios: Different component arrangements and nesting levels
- Edge Cases: Boundary conditions, invalid inputs, and concurrent updates
Testing Coverage Targets
- Components: 85% code coverage with 100% branch coverage for signal handling logic
- Utilities: 95% code coverage with full test suite for all custom hooks and state management functions
- Integration: 100% of core signal propagation flows tested end-to-end
- E2E: 100% of critical user journeys covered with Playwright tests
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The signal-based state system must support full WCAG 2.1 AA compliance through:
- Perceivable: All state changes must be communicated via multiple modalities (visual, auditory, tactile)
- Operable: Keyboard navigation must be complete and predictable
- Understandable: State transitions should be clear and consistent with user expectations
- Robust: The system must work across different assistive technologies and browsers
Keyboard navigation support
All interactive elements within the signal-based state system must be fully keyboard accessible:
// Keyboard navigation support in components using signals
const SignalComponent = () => {
const [state, setState] = useSignal({ focused: false, active: false });
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
setState(prev => ({ ...prev, active: !prev.active }));
}
if (e.key === 'Tab') {
setState(prev => ({ ...prev, focused: true }));
}
};
return (
<div
tabIndex={0}
onKeyDown={handleKeyDown}
onFocus={() => setState(prev => ({ ...prev, focused: true }))}
onBlur={() => setState(prev => ({ ...prev, focused: false }))}
>
{state.active ? 'Active State' : 'Inactive State'}
</div>
);
};
Focus management strategy
The system must implement proper focus trapping and restoration:
// Focus management for modal-like signal interactions
const useFocusTrap = (isOpen: boolean) => {
const focusableElementsRef = useRef<HTMLElement[]>([]);
const lastFocusedRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
lastFocusedRef.current = document.activeElement as HTMLElement;
const elements = Array.from(
document.querySelectorAll<HTMLElement>('[tabindex], [href], button, input')
).filter(el => !el.hasAttribute('disabled') && el.offsetParent !== null);
focusableElementsRef.current = elements;
if (elements.length > 0) {
elements[0].focus();
}
} else if (lastFocusedRef.current) {
lastFocusedRef.current.focus();
}
}, [isOpen]);
};
ARIA roles and attributes plan
Signal-based state changes must be properly announced to screen readers:
// ARIA integration for signal-driven UI updates
const SignalWithAria = () => {
const [state, setState] = useSignal({
count: 0,
loading: false,
error: null
});
return (
<div>
<span
aria-live="polite"
aria-atomic="true"
role="status"
>
{state.loading ? 'Loading...' : `Count: ${state.count}`}
</span>
{state.error && (
<div
role="alert"
aria-live="assertive"
className="error-message"
>
{state.error}
</div>
)}
</div>
);
};
Screen reader compatibility
All state changes must be announced through:
aria-liveregions for dynamic content updatesrole="status"for status messagesrole="alert"for error conditions- Proper landmark regions to help navigate content structure
The system should support screen reader announcements for:
- State transitions (e.g., "State changed to active")
- Loading states ("Loading data...")
- Error conditions ("Error: Failed to load data")
Additional accessibility considerations
- Contrast ratios: All state indicators must meet 4.5:1 contrast ratio
- Focus indicators: Clear visual focus states for all interactive elements
- Semantic HTML: Use appropriate HTML elements to convey meaning
- Alternative text: All images and visual state indicators must have appropriate alt text
- Dynamic content updates: Ensure screen readers are notified of state changes without requiring user interaction
The signal system should provide built-in accessibility utilities that can be composed into any component, ensuring that state-driven UI updates are always accessible to users with disabilities.
10. Security Considerations
XSS Prevention Strategy
The signal-based state system must prevent cross-site scripting attacks by implementing strict input sanitization and output encoding. All user-provided data flowing into the state system must be sanitized before being processed or rendered.
// Sanitization utility to prevent XSS in signal values
const sanitizeInput = (input: string): string => {
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
// Signal wrapper that sanitizes inputs
class SecureSignal<T> {
private value: T;
private subscribers: Set<() => void> = new Set();
constructor(initialValue: T) {
this.value = sanitizeInput(initialValue as unknown as string) as T;
}
get(): T {
return this.value;
}
set(newValue: T): void {
const sanitizedValue = sanitizeInput(newValue as unknown as string) as T;
this.value = sanitizedValue;
this.notifySubscribers();
}
subscribe(callback: () => void): () => void {
this.subscribers.add(callback);
return () => this.subscribers.delete(callback);
}
private notifySubscribers(): void {
this.subscribers.forEach(callback => callback());
}
}
CSRF Protection
For any state updates that involve user actions or API calls, implement CSRF token validation to prevent cross-site request forgery attacks. The signal system should integrate with existing CSRF protection mechanisms in the application.
// CSRF token integration for secure state updates
class SecureSignalWithCSRF<T> extends SecureSignal<T> {
private csrfToken: string | null = null;
set(newValue: T): void {
if (!this.csrfToken) {
throw new Error('CSRF token required for state updates');
}
// Validate CSRF token before allowing update
this.validateCSRFToken(this.csrfToken);
super.set(newValue);
}
setCSRFToken(token: string): void {
this.csrfToken = token;
}
private validateCSRFToken(token: string): void {
// Implementation would check token validity against server
if (!token || token.length < 32) {
throw new Error('Invalid CSRF token');
}
}
}
Content Security Policy (CSP) Headers
The system should work within a strict CSP environment by avoiding inline scripts and styles, which is critical for preventing code injection attacks. All state updates should be handled through declarative methods rather than dynamic DOM manipulation.
Sensitive Data Handling in Frontend State
The signal system must not store or expose sensitive user data in the frontend state. Any sensitive information should be:
- Never stored in signals or observable state
- Removed from state when no longer needed
- Encrypted at rest if persisted in localStorage
// Example of sensitive data handling in signals
class SecureStateStore {
private sensitiveFields: Set<string> = new Set(['password', 'ssn', 'creditCard']);
setSignalValue(signalName: string, value: any): void {
if (this.sensitiveFields.has(signalName)) {
console.warn(`Attempt to store sensitive data in signal: ${signalName}`);
return;
}
// Proceed with normal signal update
this.updateSignal(signalName, value);
}
private updateSignal(signalName: string, value: any): void {
// Implementation to update signal state
}
}
Input Sanitization Approach
All inputs to the signal system must undergo comprehensive sanitization:
- Remove or escape HTML tags
- Validate data types and ranges
- Implement rate limiting for state updates
- Use strict type checking to prevent injection attacks
// Comprehensive input validation for signals
const validateSignalInput = <T>(value: T, expectedType: string): T => {
// Type checking
if (typeof value !== expectedType) {
throw new TypeError(`Expected ${expectedType}, got ${typeof value}`);
}
// For string inputs, perform additional sanitization
if (typeof value === 'string') {
// Prevent common injection patterns
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/on\w+\s*=\s*["'][^"']*["']/gi,
/javascript:/gi
];
if (dangerousPatterns.some(pattern => pattern.test(value))) {
throw new Error('Input contains potentially dangerous content');
}
}
return value;
};
Security Best Practices Implementation
- Input Validation: All signal inputs must be validated before processing
- Output Encoding: Any state rendered to DOM must be properly encoded
- Secure Defaults: Signals should default to secure configurations
- Audit Trail: Track sensitive signal updates for security monitoring
- Rate Limiting: Implement rate limiting to prevent abuse of state update endpoints
The signal-based system should integrate with the application's security framework and follow established security protocols to ensure robust protection against common web vulnerabilities.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Translation keys will follow a hierarchical, descriptive naming convention to ensure maintainability and clarity:
{feature}.{component}.{property}
Examples:
dashboard.welcome.messageuser.profile.settings.labelcommon.button.savenotifications.alert.success
Keys will be organized in flat JSON files per locale, with nested structure preserved through dot notation. This approach supports both simple key-value mapping and complex interpolation patterns.
RTL Support Considerations
RTL support will be implemented through CSS class toggling and layout adjustments:
-
CSS Classes:
rtl-directionclass applied to root container when RTL mode is active- Direction-aware CSS properties using
:dir()pseudo-class where supported
-
Component Adjustments:
- Layout components will conditionally swap flex directions and margins
- Text alignment will dynamically switch between left/right
- Icon orientations (chevrons, arrows) will flip appropriately
-
RTL Detection:
- Automatic detection based on browser language settings
- Manual override via user preference setting
Date, Number, Currency Formatting Strategy
All date/time and numeric formatting will leverage the browser's built-in Intl API:
-
Date/Time Formatting:
Intl.DateTimeFormatwith locale-specific options- Support for relative time (e.g., "2 hours ago")
- Custom format patterns for specific use cases
-
Number Formatting:
Intl.NumberFormatfor currency, percentage, and decimal display- Automatic grouping separators (comma vs. period)
- Localized decimal point
-
Currency Handling:
- Default to user's locale currency when available
- Fallback to USD for unsupported locales
- Support for multiple currency display modes
Locale Detection and Switching Strategy
-
Automatic Detection:
- Primary detection from
navigator.languageornavigator.languages - Fallback to English (
en-US) when no supported language found - Default to
en-USfor server-side rendering contexts
- Primary detection from
-
User Preference Management:
- Store preferred locale in localStorage or session storage
- Allow user to manually switch between supported locales
- Persist preference across sessions
-
Locale Switching Implementation:
- React context provider for locale state
- Dynamic import of translation files based on selected language
- React component re-rendering with updated context values
// Example of i18n context structure
interface I18nContextType {
locale: string;
translations: Record<string, string>;
formatMessage: (key: string, params?: Record<string, any>) => string;
formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => string;
formatNumber: (value: number, options?: Intl.NumberFormatOptions) => string;
switchLocale: (newLocale: string) => void;
}
Translation File Organization
Translation files will be organized by locale with a consistent structure:
{
"en-US": {
"common": {
"button": {
"save": "Save"
}
},
"dashboard": {
"welcome": {
"message": "Welcome to the dashboard"
}
}
},
"ar-SA": {
"common": {
"button": {
"save": "حفظ"
}
},
"dashboard": {
"welcome": {
"message": "مرحبا بك في لوحة التحكم"
}
}
}
}
i18n Implementation Considerations
- Performance: Translation files will be loaded on-demand using dynamic imports to minimize initial bundle size
- Fallback Strategy: Fallback to English for missing keys or locales
- Testing: Unit tests will verify translation key existence and formatting consistency across locales
- Tooling: Integration with i18n extraction tools to detect unused keys and missing translations
- Developer Experience: Clear error messages when translation keys are missing or malformed
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The signal-based state system should capture key performance indicators to monitor user experience and system health:
- Signal Reaction Time: Time from signal emission to component re-render (target: <50ms for 95% of reactions)
- Signal Propagation Latency: End-to-end time from signal creation to all dependent components updating (target: <100ms for 95% of propagation paths)
- Memory Usage: Peak memory consumption during signal operations (target: <50MB for typical app state)
- Signal Count: Number of signals emitted per user session (target: <1000 signals/session for typical usage)
- Component Re-render Count: Frequency of re-renders in response to signals (target: <50% of components re-render per signal)
Error Tracking Integration
Integrate with error tracking platforms (Sentry/Datadog) to capture:
- Signal Processing Errors: Exceptions during signal emission or subscription handling
- Component Update Failures: React rendering errors in components subscribed to signals
- Memory Leak Detection: Unusual growth patterns in signal subscription tracking
- Type Safety Violations: Runtime type errors in signal value handling
Custom Events and User Journey Tracking
Implement custom event tracking for key user flows:
// Signal-based journey tracking
const trackSignalJourney = (journeyId: string, signalName: string, duration: number) => {
// Send to analytics service
analytics.track('signal_journey_complete', {
journeyId,
signalName,
duration,
timestamp: Date.now()
});
};
Performance Monitoring and Regression Detection
- Baseline Performance: Establish performance baselines for signal reactions and component updates
- Regression Detection: Alert on >10% degradation in signal processing time over 24h
- Threshold-based Alerts: Configure alerts for:
- Signal reaction time >100ms
- Component re-render count >50 per signal
- Memory usage >100MB
// Performance monitoring hook
const useSignalPerformanceMonitor = () => {
const [metrics, setMetrics] = useState<SignalMetrics>({
reactionTimes: [],
memoryUsage: 0,
signalCount: 0
});
useEffect(() => {
// Setup performance monitoring
const interval = setInterval(() => {
// Collect and report metrics
const newMetrics = collectSignalMetrics();
setMetrics(newMetrics);
// Alert on regressions
if (hasPerformanceRegression(newMetrics)) {
reportPerformanceAlert(newMetrics);
}
}, 60000);
return () => clearInterval(interval);
}, []);
return metrics;
};
Signal Health Dashboard
Create a real-time dashboard to visualize:
- Signal Frequency: Rate of signal emissions per type
- Subscription Health: Number of active subscribers per signal
- Component Impact: Re-render frequency by component type
- Memory Footprint: Signal subscription memory usage trends
Integration with Existing Monitoring Stack
The system should integrate with existing observability tools:
- APM Integration: Send signal processing traces to APM tools (e.g., New Relic, Datadog)
- Log Aggregation: Log signal operations with structured JSON for centralized logging
- Alerting Rules: Configure alerting rules in monitoring platforms based on signal performance thresholds
Data Collection Strategy
- Sampling: Sample 1% of user sessions for detailed signal tracking
- Anonymization: Remove PII from collected metrics before sending to monitoring systems
- Batching: Batch multiple performance events to reduce network overhead
- Rate Limiting: Implement rate limiting to prevent monitoring from impacting app performance
Metrics Export and Reporting
Implement metrics export capabilities:
// Metrics export interface
interface SignalMetrics {
reactionTimes: number[];
memoryUsage: number;
signalCount: number;
componentUpdates: Record<string, number>;
errorRates: {
signalProcessing: number;
componentRendering: number;
};
}
Continuous Improvement Loop
Establish feedback loops for monitoring improvements:
- Automated Regression Testing: Run performance tests as part of CI/CD pipeline
- User Feedback Integration: Correlate user complaints with signal performance data
- A/B Testing Support: Track performance differences between signal implementations
The monitoring strategy ensures that the signal-based state system remains performant, observable, and maintainable at scale.
13. File & Folder Structure
Proposed Directory Structure
/src
├── features/
│ ├── user-profile/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── store/
│ │ └── types.ts
│ └── dashboard/
│ ├── components/
│ ├── hooks/
│ ├── store/
│ └── types.ts
├── shared/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ ├── types/
│ └── constants/
├── core/
│ ├── state/
│ │ ├── signals.ts
│ │ ├── signal-store.ts
│ │ └── types.ts
│ ├── router/
│ ├── api/
│ └── providers/
├── app/
│ ├── routes.tsx
│ ├── App.tsx
│ └── index.tsx
└── types/
└── global.d.ts
Module Boundaries and Import Rules
Feature modules (user-profile, dashboard) are completely isolated with no cross-feature imports. Each feature owns its own components, hooks, and store logic.
Shared modules provide reusable utilities and components that can be imported across features but not vice versa.
Core module contains the signal-based state system and shared infrastructure that all features depend on, but no feature-specific logic.
Import Strategy
- Feature modules can import from shared and core modules
- Shared modules can only import from other shared modules or core modules
- Core modules can only import from shared modules (no feature imports)
- Barrel exports are used for clean module organization:
// src/features/user-profile/index.ts
export * from './components';
export * from './hooks';
export * from './store';
Shared vs Feature-Specific Code
Shared code includes:
- Generic UI components (buttons, modals, forms)
- Common utility functions (date formatting, validation)
- Global types and constants
- Signal system implementation
Feature-specific code includes:
- Component implementations unique to each feature
- Feature-specific hooks and store logic
- Route-specific components and data fetching
This structure enables clear ownership boundaries, prevents circular dependencies, and supports scalable feature development while maintaining a consistent signal-based state system across the application.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
1. React (v18+)
- Justification: Core library for component-based UI development with concurrent rendering and automatic batching
- Bundle size impact: ~150KB gzipped (minimal impact with tree-shaking)
- Alternatives considered: Preact - smaller bundle but less ecosystem support, Svelte - compile-time benefits but different mental model
2. TypeScript (v5+)
- Justification: Strong typing prevents runtime errors, improves developer experience, and enables better IDE support
- Bundle size impact: None (compile-time only)
- Alternatives considered: JavaScript - less safe and maintainable, Flow - less adoption in modern ecosystem
3. React Hook Form (v7+)
- Justification: High-performance form handling with built-in validation, minimal re-renders, and excellent TypeScript support
- Bundle size impact: ~15KB gzipped
- Alternatives considered: Formik - more verbose, less performant; native React forms - more boilerplate
4. React Query (v5+)
- Justification: Production-ready data fetching with automatic caching, background updates, and optimistic UI support
- Bundle size impact: ~25KB gzipped
- Alternatives considered: SWR - simpler but less feature-rich; custom fetchers - more boilerplate
5. Zustand (v4+)
- Justification: Lightweight state management with minimal boilerplate and excellent TypeScript support
- Bundle size impact: ~2KB gzipped
- Alternatives considered: Redux Toolkit - more complex setup, Context API - no built-in state normalization
6. Tailwind CSS (v3+)
- Justification: Utility-first CSS framework that enables rapid UI development with consistent design system
- Bundle size impact: ~10KB gzipped (when using only required utilities)
- Alternatives considered: CSS Modules - more verbose, Styled Components - larger bundle size
7. Framer Motion (v10+)
- Justification: Production-ready animation library with excellent performance and declarative API
- Bundle size impact: ~20KB gzipped (optional, can be tree-shaken)
- Alternatives considered: CSS animations - less flexible; GSAP - larger bundle size
8. Axios (v1+)
- Justification: Robust HTTP client with request/response interceptors, automatic JSON parsing, and error handling
- Bundle size impact: ~15KB gzipped
- Alternatives considered: Fetch API - requires more boilerplate; ky - smaller but less mature
Alternatives Considered and Rejected
Redux Toolkit vs Zustand
- Rejected: Zustand provides simpler API with less boilerplate while maintaining type safety and performance
- Reasoning: Redux Toolkit adds significant complexity for simple state management needs, with no clear benefit in this context
Jotai vs Zustand
- Rejected: Zustand offers better ecosystem integration and more intuitive API for typical use cases
- Reasoning: Jotai's atomic approach is overkill for most state management needs in this system
Emotion vs Styled Components
- Rejected: Tailwind CSS provides better performance and more predictable rendering
- Reasoning: Styled Components increase bundle size and can cause performance issues with complex styling
Playwright vs Cypress
- Rejected: Cypress provides better debugging experience and more mature ecosystem for E2E testing
- Reasoning: Playwright's performance advantages don't justify the reduced developer experience and tooling maturity
Licensing and Bundle Size Impact Assessment
License Compliance:
- All selected libraries are MIT or BSD licensed, ensuring no commercial restrictions
- Open source license compliance is maintained through proper attribution and dependency management
Bundle Size Budget Analysis:
- Core bundle size: ~200KB (React + TypeScript + Tailwind + Axios + Zustand + React Query)
- Optional features: Additional 30KB (Framer Motion, React Hook Form)
- Total estimated bundle size: ~230KB (gzipped) - within acceptable limits for modern web applications
Tree-shaking and optimization strategies:
- All libraries support tree-shaking to minimize bundle size
- Module federation and code splitting will be implemented to further optimize loading
- Critical path dependencies will be prioritized in bundle optimization
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
-
Signal vs. Store API consistency: Should the signal-based system expose a Redux-like store interface for familiarity, or maintain a more functional reactive approach? This decision impacts developer onboarding and migration complexity from existing state management solutions.
-
Reactive vs. imperative update strategy: The system must decide whether to prioritize reactive updates (automatic re-renders when signals change) or imperative update patterns (explicit trigger methods). This affects developer ergonomics and performance characteristics.
-
Type system integration: How deeply should the signal system integrate with TypeScript's type system? Should it provide compile-time guarantees for signal dependencies and prevent common errors like accessing stale signals?
Known trade-offs made in this design
-
Bundle size vs. feature richness: The signal system trades some bundle size efficiency for developer experience benefits like automatic dependency tracking and built-in debugging capabilities. For performance-critical applications, this trade-off may need reconsideration.
-
Debugging complexity vs. abstraction: While signals provide clean abstractions, they can make debugging harder when developers need to trace signal dependencies or understand why a component re-renders. The system includes debugging utilities but adds cognitive overhead.
-
Learning curve vs. productivity: Signals introduce a new paradigm that requires developers to understand reactive programming concepts, potentially slowing initial development velocity compared to more familiar patterns like Redux or Context.
Risks and mitigations
-
Signal dependency tracking bugs: There's risk of incorrect dependency tracking that could cause components to not re-render when they should, or over-render when they shouldn't. Mitigation includes comprehensive unit testing with dependency tracking assertions and runtime validation of signal usage patterns.
-
Memory leak risk: Signals that aren't properly disposed of can cause memory leaks in long-running applications. Mitigation involves providing clear disposal patterns and lifecycle management hooks for signals, with warnings in development mode for leaked signals.
-
Performance regression: If not carefully implemented, signal systems can cause performance regressions due to excessive re-renders or inefficient dependency tracking. Mitigation includes performance benchmarks, memoization strategies, and clear documentation on best practices for signal usage.
Technical debt acknowledged
-
Limited ecosystem integration: The signal system currently has minimal integration with existing React ecosystem tools like React DevTools, Redux DevTools, or React Query. This creates technical debt in terms of developer tooling and debugging capabilities that will need to be addressed for production readiness.
-
Type inference limitations: TypeScript's type inference can struggle with complex signal compositions, potentially leading to less helpful error messages or type narrowing issues. This is technical debt that will require ongoing attention as the system evolves.
-
Testing complexity: The reactive nature of signals makes unit testing more complex than traditional state management approaches, requiring more sophisticated test patterns and potentially more brittle tests that break on minor implementation changes.