Partial hydration represents a critical optimization strategy for modern React applications, allowing developers to selectively hydrate components based on their importance and usage patterns rather than hydrating the entire application immediately upon load.
1. Problem Statement & Requirements
Clarifying questions
- What is the target browser ecosystem? Are we targeting modern browsers only (Chrome 90+, Safari 15+, Firefox 90+) or must we support older browsers with polyfills?
- What is the expected content hydration strategy? Should we hydrate components on mount, or selectively hydrate based on user interaction or scroll position?
- What level of interactivity is required? Do we need to support full client-side reactivity, or just basic state updates and event handling?
- How should we handle server-rendered content? Should we assume all content is server-rendered, or do we need to support client-only rendering?
- What is the expected bundle size budget? Should we optimize for minimal overhead or allow more flexibility in implementation?
- Are there specific accessibility requirements? Do we need to support keyboard navigation, screen readers, or ARIA attributes in the hydration system?
- What is the expected user flow? Should we support progressive enhancement, or is it a full client-side application?
Functional requirements
- Selective hydration: Components should be hydratable on demand, not all at once
- Server-side rendering compatibility: Must work with SSR environments (Next.js, Gatsby)
- Performance optimization: Reduce bundle size and improve load times
- User interaction triggers: Hydrate components when user interacts (click, scroll, focus)
- State management integration: Support React state and context within hydrated components
- Error boundary support: Gracefully handle hydration errors in individual components
Non-functional requirements
- Performance targets:
- First Contentful Paint (FCP) under 2s
- Time to Interactive (TTI) under 5s
- Bundle size increase under 10KB for hydration logic
- Accessibility: Full keyboard navigation support, ARIA attribute compliance
- Scalability: Support for 1000+ components with selective hydration
- Maintainability: Clear component lifecycle and hydration state management
- Cross-browser compatibility: Support for modern browsers with graceful degradation
Constraints and assumptions
- Assumption: Server-side rendering is already implemented and working
- Constraint: Must not break existing SSR functionality or server-rendered content
- Assumption: Component state should be preserved across hydration boundaries
- Constraint: Client-side bundle size must remain under 50KB for core hydration logic
- Assumption: Components will be marked with hydration metadata or configuration
Out of scope
- Full client-side routing: This is a partial hydration system, not a full SPA framework
- Advanced state synchronization: Cross-component state management beyond React's built-in context
- Server-side rendering configuration: Server-side hydration logic is outside scope (handled by SSR framework)
- Complex component composition: Nested hydration scenarios beyond simple parent-child relationships
- Advanced performance monitoring: Real-time performance metrics and analytics (handled by observability layer)
2. Component Architecture
graph TD
A[HydrationManager] --> B[HydratableComponent]
A --> C[ClientHydrationBoundary]
A --> D[ServerHydrationBoundary]
B --> E[ContentRenderer]
B --> F[LoadingPlaceholder]
C --> G[ClientOnlyComponent]
D --> H[ServerOnlyComponent]
E --> I[ContentLoader]
F --> J[ProgressIndicator]
G --> K[ClientComponentWrapper]
H --> L[ServerComponentWrapper]
Component Tree Structure
The component architecture follows a hierarchical pattern where the HydrationManager serves as the root coordinator, delegating responsibilities to specialized boundary and content components:
-
Smart Components:
HydrationManager- orchestrates hydration lifecycle and stateClientHydrationBoundary- manages client-side hydration for specific componentsServerHydrationBoundary- handles server-side rendering and hydration markers
-
Dumb Components:
HydratableComponent- base component that can be hydrated conditionallyContentRenderer- renders content once hydration is completeLoadingPlaceholder- displays loading state during hydrationClientOnlyComponent- renders only on client-sideServerOnlyComponent- renders only on server-side
TypeScript Props Contracts
// HydrationManager Props
interface HydrationManagerProps {
children: React.ReactNode;
hydrationStrategy?: 'client-only' | 'server-only' | 'partial';
fallback?: React.ReactNode;
loading?: React.ReactNode;
error?: React.ReactNode;
}
// HydratableComponent Props
interface HydratableComponentProps {
id: string;
hydrationKey?: string;
clientOnly?: boolean;
serverOnly?: boolean;
loading?: React.ReactNode;
fallback?: React.ReactNode;
onHydrationComplete?: (id: string) => void;
onHydrationError?: (id: string, error: Error) => void;
}
// ClientHydrationBoundary Props
interface ClientHydrationBoundaryProps {
children: React.ReactNode;
fallback?: React.ReactNode;
loading?: React.ReactNode;
onHydrationComplete?: () => void;
onHydrationError?: (error: Error) => void;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> Loading : hydrationStart
Loading --> Ready : hydrationComplete
Loading --> Error : hydrationError
Ready --> Idle : reset
Error --> Loading : retry
Error --> Idle : reset
Component Responsibilities
-
HydrationManager:
- Coordinates hydration lifecycle across components
- Manages global hydration state and error boundaries
- Handles fallback rendering for failed hydration
-
HydratableComponent:
- Wraps content that can be conditionally hydrated
- Provides hydration state context to child components
- Triggers hydration completion callbacks
-
ClientHydrationBoundary:
- Renders content only when client-side hydration is complete
- Handles client-side component mounting and state management
-
ServerHydrationBoundary:
- Renders server-side content with hydration markers
- Manages client-server synchronization for partial hydration
Reusability Decisions
The system is designed to be highly reusable through:
- Component composition over inheritance
- Generic boundary components that can wrap any content
- Configurable fallback and loading states
- Extensible hydration strategies through props
Component Lifecycle Considerations
- Mounting: Server rendering with hydration markers, client-side hydration initiation
- Updating: Conditional rehydration based on hydration strategy and state changes
- Unmounting: Cleanup of hydration listeners and memory references
- Error Handling: Graceful degradation to fallback states during hydration failures
The architecture supports both progressive enhancement and client-side-only rendering paths, making it adaptable to various deployment scenarios while maintaining clean component boundaries.
3. State Management Design
Local state vs. global state decisions
For partial hydration, we need to distinguish between:
- Local state: Component-specific state that should be managed within the component's lifecycle (e.g., form input values, toggle states)
- Global state: State that affects multiple components or needs to persist across hydration boundaries (e.g., hydration status, error states, user preferences)
State management approach
We'll use React Context API + Zustand for the hybrid state management pattern:
- Zustand for global hydration state and cross-component coordination
- React Context for component-local state that needs to be shared across hydration boundaries
- React Query for server state management and caching
This combination provides:
- Minimal boilerplate for global state
- Fine-grained control over re-renders
- Built-in persistence support via middleware
- Easy integration with existing React Query patterns
State shape and normalization strategy
// Core hydration state structure
interface HydrationState {
// Global hydration status
isHydrated: boolean;
hydrationProgress: number;
pendingComponents: Set<string>;
failedComponents: Set<string>;
// Component-specific hydration data
componentStates: Record<string, {
isHydrated: boolean;
hydrationError?: string;
hydrationTimestamp?: number;
}>;
// Server state cache
serverStateCache: Record<string, any>;
}
Server state vs. client state separation
// Server state - immutable, fetched from API
interface ServerState {
userData: User;
contentData: Content[];
}
// Client state - mutable, managed locally
interface ClientState {
hydrationStatus: HydrationStatus;
componentLoadOrder: string[];
errorStates: Record<string, Error>;
}
Cache invalidation strategy
- Time-based invalidation: 5-minute TTL for component state
- Event-driven invalidation: Clear cache on user action (form submission, navigation)
- Component-specific invalidation: Invalidate only affected components
Optimistic updates design
For partial hydration, optimistic updates apply to:
- Component loading progress tracking
- Immediate UI feedback during hydration
- Fallback state management
Critical-path hook implementation
// src/hooks/useHydrationManager.ts
import { useState, useEffect, useCallback } from 'react';
import { create } from 'zustand';
import { useQueryClient } from '@tanstack/react-query';
// Store definition
interface HydrationStore {
isHydrated: boolean;
hydrationProgress: number;
pendingComponents: Set<string>;
failedComponents: Set<string>;
componentStates: Record<string, {
isHydrated: boolean;
hydrationError?: string;
hydrationTimestamp?: number;
}>;
// Actions
markComponentHydrated: (componentId: string) => void;
markComponentFailed: (componentId: string, error?: string) => void;
updateProgress: (progress: number) => void;
resetHydration: () => void;
}
// Zustand store creation
const useHydrationStore = create<HydrationStore>((set, get) => ({
isHydrated: false,
hydrationProgress: 0,
pendingComponents: new Set(),
failedComponents: new Set(),
componentStates: {},
markComponentHydrated: (componentId) => {
set((state) => ({
componentStates: {
...state.componentStates,
[componentId]: {
isHydrated: true,
hydrationTimestamp: Date.now(),
},
},
pendingComponents: new Set(state.pendingComponents).delete(componentId),
}));
},
markComponentFailed: (componentId, error) => {
set((state) => ({
componentStates: {
...state.componentStates,
[_componentId]: {
isHydrated: false,
hydrationError: error,
hydrationTimestamp: Date.now(),
},
},
failedComponents: new Set(state.failedComponents).add(componentId),
pendingComponents: new Set(state.pendingComponents).delete(componentId),
}));
},
updateProgress: (progress) => {
set({ hydrationProgress: progress });
},
resetHydration: () => {
set({
isHydrated: false,
hydrationProgress: 0,
pendingComponents: new Set(),
failedComponents: new Set(),
componentStates: {},
});
},
}));
// Custom hook for hydration management
export const useHydrationManager = () => {
const [isClient, setIsClient] = useState(false);
const queryClient = useQueryClient();
// Store state
const {
isHydrated,
hydrationProgress,
pendingComponents,
failedComponents,
componentStates,
markComponentHydrated,
markComponentFailed,
updateProgress,
resetHydration,
} = useHydrationStore();
// Initialize hydration tracking
useEffect(() => {
setIsClient(true);
// Reset on client mount
resetHydration();
return () => {
// Cleanup if needed
};
}, []);
// Component hydration tracking
const trackComponentHydration = useCallback((componentId: string) => {
// Add to pending components
useHydrationStore.getState().pendingComponents.add(componentId);
// Simulate hydration process
const simulateHydration = async () => {
try {
// In real implementation, this would be actual component hydration logic
await new Promise(resolve => setTimeout(resolve, 100));
// Mark as hydrated or failed
markComponentHydrated(componentId);
} catch (error) {
markComponentFailed(componentId, error instanceof Error ? error.message : 'Unknown error');
}
};
simulateHydration();
}, [markComponentHydrated, markComponentFailed]);
// Progress tracking
const updateHydrationProgress = useCallback((progress: number) => {
updateProgress(progress);
// Update global hydration status
if (progress >= 100) {
useHydrationStore.getState().isHydrated = true;
}
}, [updateProgress]);
// Error handling for hydration
const handleHydrationError = useCallback((componentId: string, error: Error) => {
markComponentFailed(componentId, error.message);
// Retry logic (optional)
const retryHydration = async () => {
try {
await new Promise(resolve => setTimeout(resolve, 2000));
// Retry logic here
} catch (retryError) {
console.error('Failed to retry hydration:', retryError);
}
};
// Optionally trigger retry
if (failedComponents.size < 3) {
retryHydration();
}
}, [markComponentFailed, failedComponents.size]);
// Server state cache management
const cacheServerState = useCallback((key: string, data: any) => {
// Store in query client cache for persistence
queryClient.setQueryData([key], data);
}, [queryClient]);
// Hydration status check
const isComponentHydrated = useCallback((componentId: string) => {
return componentStates[componentId]?.isHydrated || false;
}, [componentStates]);
return {
// State
isClient,
isHydrated,
hydrationProgress,
pendingComponents: Array.from(pendingComponents),
failedComponents: Array.from(failedComponents),
componentStates,
// Actions
trackComponentHydration,
updateHydrationProgress,
handleHydrationError,
cacheServerState,
isComponentHydrated,
};
};
// Usage example in a component
const HydratableComponent = ({ componentId }: { componentId: string }) => {
const {
isClient,
isHydrated,
trackComponentHydration,
isComponentHydrated,
} = useHydrationManager();
useEffect(() => {
if (isClient && !isComponentHydrated(componentId)) {
trackComponentHydration(componentId);
}
}, [isClient, componentId, isComponentHydrated]);
if (!isClient) {
return <div className="skeleton">Loading...</div>;
}
if (!isComponentHydrated(componentId)) {
return <div className="skeleton">Loading component...</div>;
}
return <div>Hydrated Component Content</div>;
};
4. Routing & Navigation
Route Structure and Hierarchy
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
B --> D[Home]
B --> E[Login]
B --> F[Register]
C --> G[Dashboard]
C --> H[Profile]
C --> I[Settings]
G --> J[Analytics]
G --> K[Reports]
The routing structure follows a clear separation between public and protected routes. Public routes (Home, Login, Register) are accessible without authentication, while protected routes (Dashboard, Profile, Settings) require valid authentication tokens. The dashboard route has nested sub-routes for Analytics and Reports.
Code Splitting Strategy
Route-based code splitting with React.lazy:
// routes.tsx
import { lazy } from 'react';
const PublicRoutes = lazy(() => import('../features/public/PublicRoutes'));
const ProtectedRoutes = lazy(() => import('../features/protected/ProtectedRoutes'));
const Dashboard = lazy(() => import('../features/dashboard/Dashboard'));
const Analytics = lazy(() => import('../features/analytics/Analytics'));
const Profile = lazy(() => import('../features/profile/Profile'));
export const routes = [
{
path: '/',
element: <PublicRoutes />,
children: [
{ path: '/', element: <Home /> },
{ path: '/login', element: <Login /> },
{ path: '/register', element: <Register /> },
],
},
{
path: '/dashboard',
element: <ProtectedRoutes />,
children: [
{ path: '/', element: <Dashboard /> },
{ path: '/analytics', element: <Analytics /> },
{ path: '/profile', element: <Profile /> },
],
},
];
Justification: Route-based splitting provides optimal bundle loading by only loading code when a user navigates to that route. This is particularly beneficial for partial hydration where we want to load only the necessary components for a specific view.
Navigation Guards and Authentication Flows
// AuthGuard.tsx
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
const AuthGuard = ({ children }: { children: React.ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const navigate = useNavigate();
const { token, validateToken } = useAuthStore();
useEffect(() => {
const checkAuth = async () => {
if (!token) {
setIsAuthenticated(false);
return;
}
try {
const valid = await validateToken(token);
setIsAuthenticated(valid);
} catch (error) {
setIsAuthenticated(false);
}
};
checkAuth();
}, [token, validateToken]);
useEffect(() => {
if (isAuthenticated === false) {
navigate('/login', { replace: true });
}
}, [isAuthenticated, navigate]);
if (isAuthenticated === null) {
return <div>Loading...</div>;
}
return <>{children}</>;
};
Deep Linking Support and URL Parameter Design
// URL parameter design for partial hydration
const DashboardRoute = () => {
const { search, page, filter } = useSearchParams();
// URL: /dashboard?search=analytics&page=2&filter=date
const searchParams = {
search: search || '',
page: parseInt(page) || 1,
filter: filter || 'default'
};
return (
<Dashboard
searchParams={searchParams}
hydrationState="partial"
/>
);
};
URL parameters are designed to support:
- Search queries (
search=analytics) - Pagination (
page=2) - Filtering (
filter=date) - Hydration state tracking (
hydrationState=partial)
Browser History Management and Scroll Restoration
// ScrollRestoration.tsx
import { useEffect } from 'react';
import { useLocation, useNavigation } from 'react-router-dom';
const ScrollRestoration = () => {
const location = useLocation();
const navigation = useNavigation();
useEffect(() => {
// Restore scroll position when navigating back
if (navigation.state === 'idle') {
const savedScrollPosition = sessionStorage.getItem(`scrollPosition-${location.pathname}`);
if (savedScrollPosition) {
window.scrollTo(0, parseInt(savedScrollPosition));
} else {
window.scrollTo(0, 0);
}
}
}, [location.pathname, navigation.state]);
useEffect(() => {
// Save scroll position before navigation
const handleBeforeUnload = () => {
sessionStorage.setItem(`scrollPosition-${location.pathname}`, window.scrollY.toString());
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [location.pathname]);
return null;
};
// App.tsx
const App = () => {
return (
<>
<ScrollRestoration />
<Router>
<Routes>
{/* ... routes */}
</Routes>
</Router>
</>
);
};
The scroll restoration strategy uses sessionStorage to persist scroll positions between route transitions, ensuring that users don't lose their place when navigating back to previously visited pages. This is particularly important for partial hydration scenarios where users may be interacting with multiple components that have scrolled content.
Partial Hydration Route Strategy
// PartialHydrationRoute.tsx
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
interface HydrationConfig {
components: string[];
threshold?: number;
fallback?: React.ReactNode;
}
const PartialHydrationRoute = ({
children,
hydrationConfig
}: {
children: React.ReactNode;
hydrationConfig: HydrationConfig
}) => {
const [isHydrated, setIsHydrated] = useState(false);
const location = useLocation();
useEffect(() => {
// Check if this route should be partially hydrated
const shouldHydrate = hydrationConfig.components.some(
component => location.pathname.includes(component)
);
if (shouldHydrate) {
// Simulate partial hydration logic
const timer = setTimeout(() => {
setIsHydrated(true);
}, 100);
return () => clearTimeout(timer);
}
}, [location.pathname, hydrationConfig]);
if (!isHydrated) {
return (
<div className="partial-hydration-fallback">
{hydrationConfig.fallback || <div>Loading...</div>}
</div>
);
}
return <>{children}</>;
};
This approach allows for granular control over which routes receive partial hydration treatment, enabling performance optimization at the route level while maintaining full hydration for critical user flows.
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 API interaction patterns across the application. The client will handle authentication, request/response transformation, 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 createApiClient = (): AxiosInstance => {
const client = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for auth token injection
client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling and normalization
client.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Handle auth errors
localStorage.removeItem('authToken');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
return client;
};
export const apiClient = createApiClient();
Request/Response Interceptors
Authentication token injection is handled via request interceptor, while response normalization ensures consistent error handling and data structure. The client includes automatic retry logic for transient network errors.
Error Handling and Retry Strategy
We implement a retry strategy with exponential backoff for transient network errors (5xx status codes) and client errors (408, 429). For 401 Unauthorized responses, we clear auth tokens and redirect to login.
// api/errorHandler.ts
export const handleApiError = (error: any): Promise<never> => {
if (error.response) {
const { status, data } = error.response;
// Handle specific HTTP errors
switch (status) {
case 401:
// Clear auth and redirect to login
localStorage.removeItem('authToken');
window.location.href = '/login';
break;
case 403:
// Handle forbidden access
throw new Error('Access denied');
case 404:
// Handle not found
throw new Error('Resource not found');
case 429:
// Handle rate limiting
throw new Error('Rate limit exceeded');
case 500:
// Handle server errors
throw new Error('Internal server error');
default:
// Handle other errors
throw new Error(data?.message || 'An error occurred');
}
} else if (error.request) {
// Network error
throw new Error('Network error - please check your connection');
} else {
// Configuration error
throw new Error('Request configuration error');
}
};
Loading, Error, and Empty States Design
We implement a standardized approach to handling loading states using React Query's built-in loading and error states. Each API call returns a consistent structure:
// types/api.ts
export interface ApiResponse<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
limit: number;
}
Data Fetching Strategy
We use React Query for data fetching with SSR support. For routes that require server-side rendering, we implement getServerSideProps for initial data hydration. Critical data is fetched during SSR, while non-critical data is fetched client-side with appropriate loading states.
API Response Transformation Layer
We implement a DTO-to-domain model mapping layer to normalize API responses:
// api/transformers.ts
export interface UserDto {
id: string;
name: string;
email: string;
createdAt: string;
}
export interface User {
id: string;
name: string;
email: string;
joinedAt: Date;
}
export const transformUserDto = (dto: UserDto): User => ({
id: dto.id,
name: dto.name,
email: dto.email,
joinedAt: new Date(dto.createdAt),
});
Sequence Diagram for Primary Data Flow
sequenceDiagram
participant U as User
participant C as Component
participant Q as React Query
participant A as API Client
participant S as Server
U->>C: Trigger data fetch
C->>Q: useQuery hook call
Q->>A: Fetch data via axios
A->>S: HTTP GET request
S-->>A: Response data
A-->>Q: Transformed response
Q-->>C: Update component state
C->>U: Render with data
6. Performance Design
Core Web Vitals targets (LCP, INP, CLS) with specific numbers
- Largest Contentful Paint (LCP): Target <2.5s for 75% of users, <4s for 90% of users
- Interaction to Next Paint (INP): Target <200ms for 90% of interactions
- Cumulative Layout Shift (CLS): Target <0.1 for 90% of users, <0.25 for 100% of users
Bundle size budget per route
- Home/Landing page: <50KB (excluding vendor bundles)
- Product listing pages: <80KB (excluding vendor bundles)
- Product detail pages: <120KB (excluding vendor bundles)
- User dashboard: <150KB (excluding vendor bundles)
Critical rendering path optimization
- Critical CSS: Inline critical CSS for above-the-fold content
- Resource hints: Preload key resources (fonts, critical images)
- HTML structure: Minimize DOM depth and reduce render-blocking resources
- JavaScript defer strategy: Defer non-critical JavaScript until after initial render
Image optimization strategy (formats, lazy loading, responsive images)
- Format selection: WebP for modern browsers, JPEG fallback for older browsers
- Lazy loading: Use
loading="lazy"attribute with IntersectionObserver polyfill for older browsers - Responsive images: Implement
srcsetandsizesattributes with appropriate breakpoints (320px, 480px, 768px, 1024px, 1200px) - Image compression: Use WebP format with 85% quality for most images, 90% for high-quality product images
Font loading strategy
- Font display: Use
font-display: swapto prevent FOIT (Flash of Invisible Text) - Preload critical fonts: Preload font files for the first 3-4 viewport characters
- Font subsetting: Implement font subsetting to reduce bundle size for specific character sets
- Fallback fonts: Define appropriate fallback fonts with matching metrics
Memoization strategy (useMemo, useCallback, React.memo) — what to memoize and what not to
What to memoize:
- Expensive calculations in
useMemo(e.g., sorting large arrays, complex filtering) - Callbacks passed to child components in
useCallback(e.g., event handlers, form submission functions) - Component rendering logic in
React.memofor pure components with stable props
What NOT to memoize:
- Simple state updates (e.g.,
useStatesetter functions) - Props that change frequently (e.g., user input, real-time data)
- Components with minimal rendering cost
Virtualization for large lists (react-window / react-virtual)
// Example virtualized list implementation
import { FixedSizeList as List } from 'react-window';
interface VirtualizedItem {
id: string;
title: string;
description: string;
}
const VirtualizedList = ({ items }: { items: VirtualizedItem[] }) => {
const itemSize = 80; // Fixed height for each list item
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const item = items[index];
return (
<div style={style} className="virtualized-item">
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
);
};
return (
<List
height={600}
itemCount={items.length}
itemSize={itemSize}
width="100%"
>
{Row}
</List>
);
};
Prefetching and preloading strategy
- Route prefetching: Preload critical routes when user hovers over navigation links (e.g., product detail pages when hovering over product cards)
- Resource preloading: Preload fonts, critical images, and JavaScript bundles for next likely user actions
- Data prefetching: Fetch data for likely next steps (e.g., pre-fetch product details when user is viewing a product list)
- Service worker caching: Implement service worker to cache frequently accessed resources for offline access and faster subsequent loads
// Example prefetching strategy
const prefetchResource = (url: string) => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
};
const prefetchCriticalRoutes = () => {
const criticalRoutes = ['/api/products', '/api/user/profile'];
criticalRoutes.forEach(route => {
prefetchResource(route);
});
};
The virtualization and memoization strategies are particularly important for partial hydration systems, as they help maintain performance when components are hydrated selectively. The prefetching strategy becomes crucial in partial hydration systems where components may be loaded on-demand, ensuring that subsequent interactions remain performant.
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:
- Route-level boundaries - Catch errors in entire route components (e.g., dashboard, profile)
- Component-level boundaries - Wrap individual complex components (e.g., charts, maps)
- Critical UI sections - Handle errors in key interactive elements (e.g., forms, modals)
graph TD
A[App Root] --> B[Route Boundary]
B --> C[Dashboard Route]
C --> D[UserCard Component]
D --> E[Error Boundary]
D --> F[AvatarImage Component]
F --> G[Image Error Boundary]
Global Error Handler Design
A centralized error handler manages uncaught errors and provides consistent feedback:
// ErrorHandlerContext.ts
import { createContext, useContext } from 'react';
interface ErrorHandler {
handleError: (error: Error, errorInfo?: string) => void;
handleNetworkError: (error: Error) => void;
handleComponentError: (error: Error, componentStack?: string) => void;
}
const ErrorHandlerContext = createContext<ErrorHandler>({
handleError: () => {},
handleNetworkError: () => {},
handleComponentError: () => {}
});
export const useErrorHandler = () => useContext(ErrorHandlerContext);
Offline / Network Failure Handling
The system implements robust offline resilience:
- Network monitoring using
navigator.onLineAPI - Offline state management with local storage fallbacks
- Retry mechanisms for failed requests with exponential backoff
- Graceful degradation for non-critical features
// OfflineManager.ts
class OfflineManager {
private isOnline: boolean = true;
private retryAttempts: Map<string, number> = new Map();
constructor() {
window.addEventListener('online', this.handleOnline);
window.addEventListener('offline', this.handleOffline);
}
handleOnline = () => {
this.isOnline = true;
// Retry queued requests
this.retryFailedRequests();
};
handleOffline = () => {
this.isOnline = false;
// Pause polling, queue requests
};
isOffline = () => !this.isOnline;
}
Graceful Degradation Patterns
Critical features maintain functionality even when non-essential components fail:
// DegradationStrategy.tsx
const DegradedComponent = ({ fallback, children }: { fallback?: React.ReactNode; children: React.ReactNode }) => {
const [hasError, setHasError] = useState(false);
if (hasError) {
return fallback || <div className="error-fallback">Content unavailable</div>;
}
return (
<ErrorBoundary onError={() => setHasError(true)}>
{children}
</ErrorBoundary>
);
};
User Feedback on Errors
Multiple feedback mechanisms ensure users understand error states:
- Toast notifications for non-critical errors
- Inline error messages for form validation
- Fallback UIs for missing content
- Error pages for route-level failures
// ToastManager.tsx
type ToastType = 'error' | 'warning' | 'success' | 'info';
interface Toast {
id: string;
type: ToastType;
message: string;
duration?: number;
}
class ToastManager {
private toasts: Toast[] = [];
addToast = (toast: Omit<Toast, 'id'>) => {
const newToast = { ...toast, id: Date.now().toString() };
this.toasts.push(newToast);
// Show toast in UI
return newToast.id;
};
removeToast = (id: string) => {
this.toasts = this.toasts.filter(t => t.id !== id);
};
}
Error Boundary Implementation
// ErrorBoundary.tsx
import { Component, ErrorInfo } from 'react';
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
errorInfo?: ErrorInfo;
}
class ErrorBoundary extends Component<{
fallback?: React.ReactNode;
onError?: (error: Error, errorInfo?: string) => void;
}, ErrorBoundaryState> {
state: ErrorBoundaryState = {
hasError: false
};
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ errorInfo });
this.props.onError?.(error, errorInfo.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="error-container">
<h2>Something went wrong</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error?.toString()}
<br />
{this.state.errorInfo?.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
Retry Strategy Implementation
// RetryManager.ts
class RetryManager {
private maxRetries = 3;
private baseDelay = 1000;
retryWithBackoff = async <T>(
operation: () => Promise<T>,
key: string
): Promise<T> => {
let lastError: Error;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt < this.maxRetries) {
const delay = this.baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
};
}
Error Recovery Patterns
- State recovery - Restore UI state from local storage when possible
- Content fallbacks - Show cached content while refreshing
- User session preservation - Maintain authentication state during network issues
// RecoveryManager.ts
class RecoveryManager {
static async recoverState(key: string) {
try {
const saved = localStorage.getItem(`recovery:${key}`);
return saved ? JSON.parse(saved) : null;
} catch {
return null;
}
}
static saveState(key: string, state: any) {
try {
localStorage.setItem(`recovery:${key}`, JSON.stringify(state));
} catch {
// Silent failure - don't block app
}
}
}
8. Testing Strategy
Unit Testing Approach
Component-level testing (70% of test coverage):
- Test all component render paths with different prop combinations
- Validate state transitions and event handlers using React Testing Library
- Mock external dependencies (API calls, global state) to isolate component logic
Utility function testing (20% of test coverage):
- Test hydration state calculation logic
- Validate partial hydration configuration parsing
- Test performance measurement utilities
Test coverage targets: 85% component coverage, 90% utility coverage
// Example: Unit test for hydration configuration parser
import { describe, it, expect } from 'vitest';
import { parseHydrationConfig } from './hydration-config';
describe('parseHydrationConfig', () => {
it('should parse valid configuration', () => {
const config = {
hydrationType: 'partial',
components: ['Header', 'ProductCard'],
threshold: 500,
};
expect(parseHydrationConfig(config)).toEqual({
hydrationType: 'partial',
components: ['Header', 'ProductCard'],
threshold: 500,
});
});
it('should handle missing optional fields', () => {
const config = { hydrationType: 'partial' };
expect(parseHydrationConfig(config)).toEqual({
hydrationType: 'partial',
components: [],
threshold: 1000,
});
});
});
Integration Testing Strategy
Hydration boundary testing: Validate that components properly hydrate when they cross the threshold, and that non-hydrated components remain in their initial state.
State synchronization: Test that client-side state updates properly sync with server-rendered content after hydration.
Performance impact testing: Verify that partial hydration doesn't introduce significant performance regressions in the component lifecycle.
E2E Testing Scope
Primary flows:
- User navigates to a page with partial hydration components
- Components hydrate in the correct order based on scroll position
- Server-rendered content matches client-rendered content
Testing tools: Playwright for end-to-end testing with:
- Performance monitoring (LCP, FCP, INP)
- Network throttling to simulate slow connections
- Multiple viewport sizes (mobile, tablet, desktop)
Visual Regression Testing
Test setup:
- Use Percy or Chromatic for visual regression testing
- Test hydration states across different screen sizes and devices
- Capture baseline screenshots of server-rendered content vs. hydrated content
Accessibility Testing Automation
Axe-core integration:
- Run axe-core tests in CI pipeline for each component
- Validate that hydration doesn't break keyboard navigation or screen reader support
- Test focus management after component hydration
Test Data Strategy
Test fixtures:
- Create realistic mock data for different hydration scenarios
- Include edge cases like empty states, loading states, and error states
- Use deterministic data to ensure test reliability across environments
Data generation:
- Implement factory patterns for generating test data
- Use faker.js or similar libraries for realistic but non-sensitive mock data
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The partial hydration system must meet all WCAG 2.1 AA success criteria:
- 1.1.1 Non-text Content: All non-text content must have text alternatives (e.g.,
alttext for images, ARIA labels for interactive elements) - 1.3.1 Info and Relationships: Content structure must be semantically correct using HTML5 elements and ARIA roles to convey relationships
- 1.3.2 Meaningful Sequence: Tab order must follow logical reading order and content flow
- 1.4.1 Use of Color: Color must not be the sole method for conveying information or distinguishing elements
- 1.4.3 Contrast (Minimum): Text contrast ratio must be at least 4.5:1 for normal text and 3:1 for large text
- 1.4.12 Text Spacing: Text must be resizable without loss of content or functionality
- 2.1.1 Keyboard: All functionality must be operable via keyboard alone
- 2.1.2 No Keyboard Trap: Users must not be trapped in interactive elements (e.g., focus can move out of modals)
- 2.4.1 Bypass Blocks: Content must allow skipping to main content (e.g., skip links)
- 2.4.3 Focus Order: Visual focus order must match the logical reading order
- 2.4.6 Headings and Labels: All headings must be properly structured (H1, H2, etc.) and labels must be descriptive
- 3.1.1 Language: The language of the page or content must be identified using
langattributes - 4.1.2 Name, Role, Value: All interactive elements must have proper ARIA attributes and accessible names
Keyboard navigation support
The partial hydration system must ensure full keyboard accessibility:
- Tab Navigation: All interactive elements must be reachable via tab navigation in logical order
- Enter/Space Activation: Buttons and interactive elements must respond to Enter and Space key presses
- Focus Management: Focus must be properly managed when components are hydrated or dehydrated
- Keyboard Traps: Prevent focus from being trapped inside dynamically rendered components
// Example keyboard navigation handler for hydrated components
const useKeyboardNavigation = (isHydrated: boolean) => {
useEffect(() => {
if (!isHydrated) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
// Ensure proper focus order
const focusableElements = document.querySelectorAll(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
// Handle focus logic here
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isHydrated]);
};
Focus management strategy
Focus must be carefully managed when components are hydrated or dehydrated:
- Focus Restoration: When a component is re-hydrated, focus should return to the previous element or appropriate location
- Focus Trapping: When modals or dropdowns are hydrated, focus should be trapped within the component
- Focus Indicators: Visual focus indicators must remain visible and accessible
// Focus management hook for partial hydration
const useFocusManagement = (
isHydrated: boolean,
focusRef: React.RefObject<HTMLElement>
) => {
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isHydrated) {
// Store previous focus
previousFocus.current = document.activeElement as HTMLElement;
// Move focus to the hydrated component
if (focusRef.current) {
focusRef.current.focus();
}
} else {
// Restore previous focus
if (previousFocus.current) {
previousFocus.current.focus();
}
}
}, [isHydrated, focusRef]);
};
ARIA roles and attributes plan
All interactive elements must have proper ARIA attributes:
- ARIA Roles: Use appropriate roles like
button,dialog,tabpanel, etc. - ARIA States: Indicate states like
aria-expanded,aria-selected,aria-hidden - ARIA Labels: Use
aria-labeloraria-labelledbyfor elements without visible text - Landmark Roles: Use
main,nav,article, etc. for semantic structure
Screen reader compatibility
The system must support screen readers:
- Semantic HTML: Use native HTML elements when possible (e.g.,
<button>over<div role="button">) - Live Regions: Use
aria-livefor dynamic content updates - Screen Reader Announcements: Use
aria-atomic,aria-relevantfor announcements - Descriptive Labels: All interactive elements must have descriptive labels
// Screen reader announcement component
const ScreenReaderAnnouncement = ({ message }: { message: string }) => (
<div
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{message}
</div>
);
// sr-only class for screen reader only content
const srOnlyStyles = `
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
`;
Additional accessibility considerations
- Dynamic Content Updates: When components are hydrated, ensure screen readers announce the changes
- Error States: Provide clear error messages with appropriate ARIA attributes (
aria-invalid,aria-describedby) - Loading States: Indicate loading states with ARIA attributes (
aria-busy,aria-live) - Keyboard Focus Indicators: Ensure focus indicators are visible and accessible in all themes
- High Contrast Mode Support: Test with high contrast mode enabled in Windows and macOS
- Voice Control Compatibility: Ensure all functionality works with voice control systems like Dragon NaturallySpeaking or iOS Voice Control
10. Security Considerations
XSS Prevention Strategy
The partial hydration system must prevent cross-site scripting attacks through comprehensive input sanitization and output encoding:
// Sanitization utility to prevent XSS in user-generated content
const sanitizeHTML = (html: string): string => {
const div = document.createElement('div');
div.textContent = html;
return div.innerHTML;
};
// DOM-based XSS prevention in event handlers
const handleUserInput = (event: React.FormEvent) => {
const target = event.target as HTMLInputElement;
// Prevent direct DOM manipulation from user input
const safeValue = sanitizeHTML(target.value);
// Use safe value for state updates or DOM operations
};
CSRF Protection
Implement CSRF token validation for all server-side interactions:
// CSRF token management in React context
interface CsrfContextType {
token: string;
updateToken: (newToken: string) => void;
}
const CsrfContext = React.createContext<CsrfContextType>({
token: '',
updateToken: () => {},
});
// Token injection in API requests
const apiClient = axios.create({
baseURL: '/api',
withCredentials: true,
});
apiClient.interceptors.request.use((config) => {
const token = getCsrfToken(); // From context or cookie
return {
...config,
headers: {
...config.headers,
'X-CSRF-Token': token,
},
};
});
Content Security Policy (CSP) Headers
Configure CSP headers to restrict script execution and inline content:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
Sensitive Data Handling in Frontend State
Protect sensitive data from being exposed in frontend state or logs:
// Secure state management with sensitive data filtering
interface SecureState {
publicData: PublicDataType;
// Sensitive fields are not stored in component state
sensitiveFields?: never; // Prevents accidental exposure
}
// Secure data handling in hydration context
const useSecureHydration = () => {
const [state, setState] = useState<SecureState>({ publicData: {} });
// Never expose sensitive data in logs or console
const updateState = (newState: SecureState) => {
// Filter sensitive data before logging
const loggableState = {
...newState,
sensitiveFields: undefined, // Remove before logging
};
console.log('Secure state update:', loggableState);
setState(newState);
};
};
Input Sanitization Approach
Implement comprehensive input validation and sanitization:
// Comprehensive input sanitization for hydration contexts
const sanitizeInput = (input: string): string => {
// Remove dangerous patterns
const dangerousPatterns = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/on\w+\s*=\s*["'][^"']*["']/gi,
/javascript:/gi,
/vbscript:/gi,
];
let sanitized = input;
dangerousPatterns.forEach(pattern => {
sanitized = sanitized.replace(pattern, '');
});
// HTML entity encoding for safe display
return sanitized
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
// Hydration-aware input validation
const validateHydrationInput = (data: any): boolean => {
// Prevent prototype pollution and object injection
if (typeof data === 'object' && data !== null) {
const keys = Object.keys(data);
return !keys.some(key => key.startsWith('__') || key.includes('constructor'));
}
return true;
};
Security Headers Implementation
Implement security headers at the server level:
// Server-side security header configuration
const securityHeaders = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",
};
// Middleware to apply security headers
const securityMiddleware = (req: Request, res: Response, next: NextFunction) => {
Object.entries(securityHeaders).forEach(([key, value]) => {
res.setHeader(key, value);
});
next();
};
Runtime Security Checks
Implement runtime checks to detect and prevent security violations:
// Runtime security validation for partial hydration
const validateHydrationContext = (context: HydrationContext): boolean => {
// Validate that hydration context is not compromised
if (!context || typeof context !== 'object') {
throw new Error('Invalid hydration context');
}
// Prevent prototype pollution
if (context.constructor !== Object) {
throw new Error('Prototype pollution detected');
}
// Validate component names are from allowed list
const allowedComponents = ['Button', 'Card', 'Input', 'Modal'];
if (context.component && !allowedComponents.includes(context.component)) {
throw new Error('Unauthorized component in hydration context');
}
return true;
};
// Secure hydration state management
const secureHydrationState = {
set: (key: string, value: any) => {
// Validate key and value before setting
if (typeof key !== 'string' || key.length > 100) {
throw new Error('Invalid state key');
}
// Prevent storing sensitive data in state
if (typeof value === 'object' && value !== null) {
const sensitiveKeys = ['token', 'password', 'secret'];
if (Object.keys(value).some(k => sensitiveKeys.includes(k))) {
throw new Error('Sensitive data cannot be stored in hydration state');
}
}
// Proceed with secure state update
return originalSetState(key, value);
},
};
Security Testing Considerations
Implement security testing as part of the CI/CD pipeline:
- Regular XSS vulnerability scans on user inputs
- CSRF token validation testing
- CSP header compliance verification
- Input sanitization regression tests
- Secure state management audits
The security design ensures that partial hydration maintains the same security standards as full server-side rendering while providing the performance benefits of client-side hydration.
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following the BEM-style naming convention with feature-scoped keys to prevent collisions:
// Translation keys organized by feature modules
{
"common": {
"buttons": {
"save": "Save",
"cancel": "Cancel",
"submit": "Submit"
},
"labels": {
"name": "Name",
"email": "Email"
}
},
"dashboard": {
"title": "Dashboard",
"greeting": "Welcome, {name}!",
"stats": {
"revenue": "Revenue: {amount}",
"users": "Active Users: {count}"
}
},
"auth": {
"login": {
"title": "Login",
"email_label": "Email Address",
"password_label": "Password",
"forgot_password": "Forgot Password?"
}
}
}
RTL Support Considerations
RTL layout handling through CSS classes and direction properties:
/* CSS for RTL support */
[dir="rtl"] .text-align-left {
text-align: right;
}
[dir="rtl"] .float-left {
float: right;
}
/* Component-level RTL handling */
.locale-container {
direction: ltr;
}
[dir="rtl"] .locale-container {
direction: rtl;
}
Date, Number, Currency Formatting Strategy
Using Intl API with locale-aware formatting:
// i18n utility functions
export const formatDate = (date: Date, locale: string): string => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date);
};
export const formatCurrency = (amount: number, locale: string, currency: string): string => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
};
export const formatNumber = (num: number, locale: string): string => {
return new Intl.NumberFormat(locale).format(num);
};
Locale Detection and Switching Strategy
Automatic detection with manual override capability:
// i18n context implementation
interface I18nContextType {
locale: string;
locales: string[];
messages: Record<string, any>;
setLocale: (locale: string) => void;
t: (key: string, params?: Record<string, any>) => string;
}
const I18nContext = React.createContext<I18nContextType>({
locale: 'en-US',
locales: ['en-US', 'fr-FR', 'ar-SA'],
messages: {},
setLocale: () => {},
t: () => ''
});
// Auto-detection logic
const detectUserLocale = (): string => {
const stored = localStorage.getItem('preferred-locale');
if (stored) return stored;
const browserLang = navigator.language || 'en-US';
const supported = ['en-US', 'fr-FR', 'ar-SA'];
return supported.includes(browserLang) ? browserLang : 'en-US';
};
Translation Key Management
Translation files organized by locale and feature:
// en-US.json
{
"dashboard": {
"title": "Dashboard",
"greeting": "Welcome, {name}!",
"stats": {
"revenue": "Revenue: {amount}",
"users": "Active Users: {count}"
}
}
}
// fr-FR.json
{
"dashboard": {
"title": "Tableau de bord",
"greeting": "Bienvenue, {name}!",
"stats": {
"revenue": "Revenus: {amount}",
"users": "Utilisateurs actifs: {count}"
}
}
}
i18n Hook Implementation
// Custom hook for i18n with caching and lazy loading
const useI18n = () => {
const [locale, setLocale] = useState<string>(detectUserLocale());
const [messages, setMessages] = useState<Record<string, any>>({});
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const loadMessages = async () => {
try {
setLoading(true);
const loadedMessages = await import(`./locales/${locale}.json`);
setMessages(loadedMessages.default);
} catch (error) {
console.error(`Failed to load translations for ${locale}`, error);
setMessages({});
} finally {
setLoading(false);
}
};
loadMessages();
}, [locale]);
const t = useCallback((key: string, params?: Record<string, any>): string => {
const keys = key.split('.');
let message = messages;
for (const k of keys) {
if (message && typeof message === 'object') {
message = message[k];
} else {
return key;
}
}
if (typeof message === 'string' && params) {
return Object.entries(params).reduce((acc, [paramKey, value]) =>
acc.replace(new RegExp(`{${paramKey}}`, 'g'), String(value)), message);
}
return typeof message === 'string' ? message : key;
}, [messages]);
const setLocaleWithCache = useCallback((newLocale: string) => {
localStorage.setItem('preferred-locale', newLocale);
setLocale(newLocale);
}, []);
return {
locale,
setLocale: setLocaleWithCache,
t,
loading,
messages
};
};
i18n Component Integration
// i18n provider component
const I18nProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { locale, setLocale, t, loading, messages } = useI18n();
return (
<I18nContext.Provider value={{ locale, setLocale, t, messages }}>
<div dir={locale.startsWith('ar') ? 'rtl' : 'ltr'}>
{loading ? <div>Loading translations...</div> : children}
</div>
</I18nContext.Provider>
);
};
// Translation component for declarative usage
const Trans: React.FC<{
i18nKey: string;
params?: Record<string, any>;
children?: React.ReactNode
}> = ({ i18nKey, params, children }) => {
const { t } = useContext(I18nContext);
return (
<span>
{children ? children : t(i18nKey, params)}
</span>
);
};
Testing Strategy for i18n
Unit tests for translation key resolution and formatting:
// Test example using Jest
describe('i18n translation', () => {
it('should resolve translation keys correctly', () => {
const { t } = useI18n();
expect(t('common.buttons.save')).toBe('Save');
expect(t('dashboard.greeting', { name: 'John' })).toBe('Welcome, John!');
});
it('should handle missing translation gracefully', () => {
const { t } = useI18n();
expect(t('non.existent.key')).toBe('non.existent.key');
});
it('should format numbers and dates correctly', () => {
const { t } = useI18n();
expect(t('dashboard.stats.revenue', { amount: 1234.56 })).toBe('Revenue: $1,234.56');
});
});
i18n Performance Considerations
Lazy loading of translation files with proper caching:
// Translation cache implementation to avoid repeated fetches
class TranslationCache {
private cache = new Map<string, Promise<any>>();
async getTranslations(locale: string): Promise<Record<string, any>> {
if (this.cache.has(locale)) {
return this.cache.get(locale)!;
}
const promise = import(`./locales/${locale}.json`).then(module => module.default);
this.cache.set(locale, promise);
return promise;
}
}
i18n Error Handling
Graceful degradation when translation files are missing or malformed:
// Fallback mechanism for i18n errors
const fallbackTranslation = (key: string): string => {
// Return key as-is or default English value
return key.split('.').pop() || key;
};
const safeTranslate = (t: (key: string, params?: any) => string, key: string, params?: any): string => {
try {
return t(key, params);
} catch (error) {
console.warn(`Translation error for key: ${key}`, error);
return fallbackTranslation(key);
}
};
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
The partial hydration system requires comprehensive RUM monitoring to ensure optimal user experience and performance. Key metrics include:
- Time to Interactive (TTI): Track how long it takes for the page to become fully interactive, especially after partial hydration occurs
- First Contentful Paint (FCP): Monitor when the first content appears to users, particularly for hydrated components
- Largest Contentful Paint (LCP): Measure the render time of the largest content element, which may be affected by hydration timing
- First Input Delay (FID): Track delays between user input and browser response, critical for interactive components
- Cumulative Layout Shift (CLS): Monitor layout shifts that may occur when components hydrate and change DOM structure
- Hydration Time: Custom metric tracking the time from component mount to full hydration completion
Error Tracking Integration
Integrating with error tracking platforms like Sentry or Datadog is essential for monitoring hydration failures:
- Component hydration errors: Capture when individual components fail to hydrate properly
- Partial hydration failures: Track specific component failures within a larger hydration boundary
- Memory leak detection: Monitor for components that don't properly clean up after hydration
- Console error aggregation: Capture and group console errors related to hydration process
Custom Events and User Journey Tracking
Implement custom event tracking for key hydration workflows:
// Example custom event tracking for hydration
const trackHydrationEvent = (event: {
component: string;
status: 'success' | 'failure' | 'pending';
duration?: number;
error?: string;
}) => {
// Send to analytics service or RUM provider
window.dataLayer?.push({
event: 'hydration_event',
...event,
timestamp: Date.now()
});
};
Performance Monitoring and Regression Detection
Performance monitoring includes:
- Baseline performance tracking: Establish performance baselines for different component hydration scenarios
- Regression detection: Alert when hydration times exceed thresholds or performance degrades
- Component-specific metrics: Track individual component hydration performance to identify bottlenecks
- Network condition impact: Monitor how different network conditions affect hydration timing
Implementation Strategy
// Performance monitoring hook for hydration tracking
import { useEffect, useRef } from 'react';
const useHydrationMonitoring = (componentName: string) => {
const startTimeRef = useRef<number | null>(null);
useEffect(() => {
startTimeRef.current = performance.now();
return () => {
if (startTimeRef.current) {
const duration = performance.now() - startTimeRef.current;
// Report to RUM service
reportHydrationMetrics({
component: componentName,
duration,
timestamp: Date.now()
});
}
};
}, [componentName]);
};
// Performance reporting function
const reportHydrationMetrics = (metrics: {
component: string;
duration: number;
timestamp: number;
}) => {
// Send to monitoring service (e.g., DataDog, Sentry)
if (window.RUM_CLIENT) {
window.RUM_CLIENT.capturePerformanceMetrics(metrics);
}
};
Alerting Strategy
Configure alerting thresholds:
- Critical: Hydration time > 3s (alerts for all components)
- Warning: Component hydration > 1.5s (alerts for specific component types)
- Performance degradation: 20% increase in hydration time over 7-day average
- Failure rate: >5% of component hydration attempts failing
Data Collection and Privacy
Ensure all monitoring respects user privacy:
- Anonymous data collection: No PII in performance metrics
- User opt-out capability: Allow users to disable RUM tracking
- Compliance with privacy regulations: GDPR, CCPA compliance for data collection
- Data retention policies: Clear policies on how long performance data is retained
Integration with Existing Monitoring Stack
The system should integrate with existing observability tools:
- APM integration: Connect to APM platforms for end-to-end tracing
- Log aggregation: Include hydration metrics in log streams
- Dashboard integration: Create dashboards showing hydration performance trends
- Incident response: Trigger alerts to incident management systems when thresholds exceeded
13. File & Folder Structure
Proposed Directory Structure
/src
/components
/hydration
HydrationBoundary.tsx
PartialHydration.tsx
HydrationProvider.tsx
useHydration.ts
/ui
Button.tsx
Card.tsx
...
/features
/dashboard
components/
DashboardGrid.tsx
WidgetCard.tsx
hooks/
useDashboardData.ts
index.ts
/hooks
useHydrationState.ts
useClientHydration.ts
/utils
hydrationUtils.ts
ssrUtils.ts
/types
hydrationTypes.ts
Module Boundaries and Import Rules
- Shared components (
/components/ui) can be imported by any feature - Hydration-specific components (
/components/hydration) are internal to the hydration system and should not be imported by features directly - Feature components (
/features/dashboard) should only import from their own directory or shared modules - No cross-feature imports between
/featuresdirectories
Shared vs Feature-Specific Code Separation
- Shared code lives in
/components,/hooks, and/utilswith clear separation of concerns - Feature-specific code lives in
/features/<feature-name>with its own components, hooks, and data fetching logic - Hydration utilities are shared across features but not exposed as public APIs
This structure supports clean separation of concerns while enabling efficient code organization and maintainability for a partial hydration system.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
React (18.2+) - Core framework for component-based UI development with concurrent rendering capabilities that enable partial hydration strategies. Provides hooks, context, and proper lifecycle management essential for implementing selective hydration.
React DOM (18.2+) - Required for DOM rendering and provides the hydrateRoot API that enables partial hydration by selectively hydrating components based on their hydration requirements.
React Server Components (RSC) - Enables server-side rendering and component splitting. While not yet fully stable in the client, RSC patterns provide foundational concepts for partial hydration by separating server-rendered and client-hydrated components.
Next.js (13+) - Provides built-in support for React Server Components and partial hydration through its use client directive. Offers automatic code splitting, server-side rendering, and optimized bundle loading that aligns with partial hydration goals.
Zustand (4.0+) - Lightweight state management solution that enables efficient global state updates without complex provider hierarchies. Ideal for managing hydration status and component lifecycle states.
React Query (5.0+) - Provides robust data fetching and caching capabilities with automatic revalidation. Essential for managing server state that should be hydrated conditionally based on component needs.
Emotion (11.0+) - CSS-in-JS solution that supports dynamic styling and can be selectively applied to hydrated components. Provides better performance than traditional CSS for component-specific styling.
Lodash (4.17+) - Utility library for common operations like deep equality checks, debounce, and memoization that are essential for efficient hydration logic.
Alternatives Considered and Rejected
Redux Toolkit - While powerful, Redux introduces unnecessary complexity for partial hydration scenarios. The granular control needed for selective hydration is better achieved with Zustand or Context API.
Jotai - Although excellent for fine-grained state management, Jotai's atom-based approach adds complexity that isn't justified for partial hydration use cases where simpler state management suffices.
MobX - Not chosen due to its imperative nature and complexity in managing selective reactivity for partial hydration scenarios.
Styled Components - Rejected due to its heavier bundle size and less efficient runtime performance compared to Emotion for partial hydration use cases.
Licensing and Bundle Size Impact Assessment
React + React DOM: MIT license, ~100KB minified. Core dependencies for any React application.
Next.js: MIT license, ~50KB for minimal setup. Provides optimized hydration and RSC support.
Zustand: MIT license, ~2KB minified. Lightweight alternative to Redux with minimal overhead.
React Query: MIT license, ~15KB minified. Essential for data hydration and caching.
Emotion: MIT license, ~10KB minified. Provides efficient CSS-in-JS with minimal runtime overhead.
Lodash: MIT license, ~20KB minified. Bundle size impact is manageable with tree-shaking.
Performance Considerations
All selected libraries are optimized for production builds and support tree-shaking to minimize bundle size. React Server Components and Next.js provide automatic code splitting that aligns with partial hydration goals.
The combination of React Query for data management, Zustand for state, and Emotion for styling provides a lightweight yet feature-rich foundation that supports selective hydration without introducing unnecessary overhead or complexity.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Hydration granularity control: Should teams be able to specify hydration levels per component (e.g., "defer", "lazy", "immediate") or should this be a global configuration? This affects how we design the hydration API and whether we need component-level props vs. global config.
- Fallback behavior for partially hydrated components: When a component fails to hydrate or is still loading, what's the expected fallback UI? Should it be a skeleton, loading spinner, or a static version of the component?
- Performance vs. user experience trade-offs: How aggressive should we make partial hydration? Should we prioritize faster initial load over complete interactivity, or vice versa?
Known trade-offs made in this design
- Complexity vs. simplicity: The partial hydration system adds significant complexity to the component lifecycle and state management, but it provides better performance characteristics for large applications. We're trading simpler component APIs for more sophisticated hydration control.
- Bundle size vs. feature richness: Including hydration logic in every component increases the bundle size, but it provides a consistent hydration strategy across the application. We could reduce this by making hydration opt-in, but that increases developer friction.
- Developer experience vs. performance: The system requires developers to understand hydration levels and component loading states, which adds cognitive overhead compared to a simple "hydrate everything" approach.
Risks and mitigations
- Race conditions in hydration: There's a risk of race conditions where components try to access state that hasn't been hydrated yet. Mitigation: Implement proper loading states and use React's Suspense for better error boundary handling.
- Memory leaks from partial hydration: Components that are partially hydrated might retain references to old data or DOM elements. Mitigation: Implement proper cleanup in useEffect and useCleanup hooks.
- Inconsistent hydration behavior: Different components might hydrate at different times, leading to inconsistent UI states. Mitigation: Use a centralized hydration queue and provide clear documentation on hydration timing.
- Debugging complexity: Debugging issues in partially hydrated components can be more complex. Mitigation: Provide detailed logging and debugging tools to track hydration status of individual components.
Technical debt acknowledged
- Component lifecycle complexity: The hydration system introduces additional complexity to the component lifecycle, which makes it harder for new developers to understand how components behave.
- Testing overhead: Testing partial hydration requires more sophisticated test setups, including mocking hydration states and simulating different loading conditions.
- API evolution risk: The hydration API might need to evolve over time, which could break existing implementations. We're designing with extensibility in mind but acknowledge this as a potential maintenance burden.
- Browser compatibility concerns: Some hydration strategies might not work consistently across all browsers, particularly older versions of Safari or Internet Explorer. We're targeting modern browsers but need to document compatibility limitations.
- Performance monitoring overhead: The hydration tracking system adds performance overhead that might impact the very metrics we're trying to optimize. We need to carefully measure and minimize this impact through efficient tracking mechanisms.