This document presents a robust low-level design for automatic token refresh mechanisms in modern web applications, covering implementation strategies, edge cases, and security considerations for seamless user experiences.
1. Problem Statement & Requirements
Clarifying questions
- What authentication method is being used (JWT, OAuth, Session-based)?
- Do we need to support refresh tokens in local storage or session storage?
- Should token refresh happen automatically in the background or require user interaction?
- What is the expected behavior when refresh tokens expire or are invalidated server-side?
- Do we need to support concurrent requests during token refresh (e.g., multiple API calls while refresh is in progress)?
- Are there specific browser support requirements (e.g., mobile browsers, IE11 compatibility)?
- Should the system handle token refresh for third-party API calls or only internal services?
Functional requirements
- Automatically refresh access tokens before they expire
- Handle concurrent API requests during token refresh
- Retry failed requests with new tokens when refresh succeeds
- Gracefully handle refresh token expiration or invalidation
- Support multiple authentication providers (if applicable)
- Maintain session state across page reloads
- Provide hooks or utilities for token-aware API calls
Non-functional requirements
- Performance: Token refresh should not block UI rendering or cause noticeable delays
- Reliability: Should handle network failures gracefully and retry token refresh logic
- Scalability: Design should support high concurrent request volumes without token refresh contention
- Security: Refresh tokens must not be exposed in logs or client-side errors
- Accessibility: Token refresh should not interrupt keyboard navigation or screen reader users
Constraints and assumptions
- Authentication is based on JWT with refresh token flow
- Refresh tokens are stored securely (e.g., HTTP-only cookies or secure storage)
- API responses include standard HTTP status codes for token expiration (401/403)
- Application has access to a centralized authentication service or API client
- No cross-origin token refresh is required (same domain only)
- Token refresh logic should not introduce significant bundle size overhead
Out of scope
- Refresh token rotation or refresh token revocation
- Token encryption or secure storage outside of browser (e.g., encrypted local storage)
- Token refresh for third-party services or external APIs
- Integration with authentication providers like Auth0, Firebase, or Okta (unless explicitly required)
- Refresh token lifecycle management beyond basic expiration handling
- Token refresh for server-side rendered pages (SSR) or static site generation (SSG)
- Token refresh in service workers or background processes
2. Component Architecture
graph TD
A[AuthManager] --> B[TokenRefreshService]
A --> C[AuthProvider]
A --> D[AuthInterceptor]
B --> E[TokenService]
C --> F[UserContext]
D --> G[APIHttpClient]
G --> H[AuthErrorBoundary]
Component Tree Structure
The authentication system consists of a hierarchical component structure:
AuthManager- Root container managing authentication lifecycleTokenRefreshService- Background token refresh logicAuthProvider- Context provider for auth stateAuthInterceptor- HTTP client interceptor for token handlingTokenService- Core token operations and storageUserContext- Authentication state consumerAPIHttpClient- HTTP client with interceptorsAuthErrorBoundary- Error handling for auth-related failures
Component Classification
Smart Components (Container):
AuthManager- Orchestrates authentication flow, manages token refresh lifecycleAuthProvider- Provides authentication context to child componentsTokenRefreshService- Implements background refresh logic with retry mechanisms
Dumb Components (Presentational):
UserContext- Consumes authentication state from contextAuthErrorBoundary- Handles auth-related error states
TypeScript Props Contracts
// AuthManager props
interface AuthManagerProps {
children: React.ReactNode;
onAuthSuccess?: (user: User) => void;
onAuthFailure?: () => void;
refreshThresholdMs?: number;
maxRefreshAttempts?: number;
}
// TokenRefreshService props
interface TokenRefreshServiceProps {
onTokenRefreshed: (newToken: string) => void;
onError: (error: Error) => void;
refreshThresholdMs: number;
maxRefreshAttempts: number;
}
// AuthInterceptor props
interface AuthInterceptorProps {
httpClient: HttpClient;
onTokenRefreshed: (newToken: string) => void;
onAuthFailure: () => void;
}
UI State Machine
stateDiagram-v2
[*] --> Idle
Idle --> CheckingTokenExpiry
CheckingTokenExpiry --> RefreshingToken
CheckingTokenExpiry --> Idle
RefreshingToken --> TokenRefreshSuccess
RefreshingToken --> TokenRefreshFailed
TokenRefreshSuccess --> Idle
TokenRefreshFailed --> RetryTokenRefresh
RetryTokenRefresh --> RefreshingToken
RetryTokenRefresh --> AuthFailure
AuthFailure --> Idle
Component Responsibilities
AuthManager:
- Initializes authentication flow
- Manages token refresh lifecycle
- Handles auth state transitions
- Provides error boundaries for authentication failures
TokenRefreshService:
- Monitors token expiry timing
- Triggers refresh before expiration
- Implements retry logic for failed refreshes
- Manages concurrent refresh requests
AuthProvider:
- Provides authentication context to components
- Handles user session state updates
- Manages token storage and retrieval
AuthInterceptor:
- Intercepts HTTP requests to inject auth headers
- Handles 401 responses for token refresh
- Manages request queuing during refresh operations
Reusability Decisions
The authentication system is designed to be reusable across different application contexts:
- Token refresh logic is abstracted into a service
- HTTP interceptor can be applied to any client instance
- Context provider supports different user models
- Error handling follows standardized patterns
Component Lifecycle Considerations
- Mounting: AuthManager initializes token refresh timers and sets up interceptors
- Updating: TokenRefreshService monitors token expiry and triggers refreshes
- Unmounting: Cleanup of timers, event listeners, and ongoing requests
- Error Recovery: Graceful handling of refresh failures with retry logic
The component lifecycle ensures proper cleanup to prevent memory leaks and maintain consistent authentication state across the application.
3. State Management Design
Local state vs. global state decisions
For automatic token refresh, we need to manage authentication state across the entire application. The token refresh logic should be handled globally since:
- Multiple components may need to access and potentially refresh tokens
- Token state is application-wide (not component-specific)
- We need to coordinate token refresh attempts to avoid multiple concurrent requests
- Error handling and retry logic should be centralized
State management approach
Zustand is selected as the state management solution because:
- Simplicity: Minimal boilerplate and easy to understand for team adoption
- TypeScript native: Excellent type support with no additional configuration needed
- No provider wrapping: Avoids unnecessary component tree bloat
- Middleware support: Can easily add logging, persistence, or other middleware
- Lightweight: Bundle size impact is minimal compared to Redux Toolkit
State shape and normalization strategy
interface AuthState {
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
tokenExpiry: number | null;
refreshStatus: 'idle' | 'pending' | 'success' | 'error';
refreshError: string | null;
}
Server state vs. client state separation
- Client state: Authentication tokens, refresh status, user session metadata
- Server state: User profile data, permissions, API responses (managed by React Query)
Cache invalidation strategy
When tokens are refreshed:
- Invalidate all cached API requests that used the old token
- Update all auth-related React Query cache entries
- Clear any expired token-based cache keys
Optimistic updates design
For refresh operations, we implement:
- Optimistic token replacement: Update local state immediately when refresh starts
- Fallback handling: If refresh fails, revert to previous token state with error notification
- Concurrent request handling: Prevent multiple simultaneous refresh attempts
Critical-path hook implementation
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthStore {
accessToken: string | null;
refreshToken: string | null;
isAuthenticated: boolean;
tokenExpiry: number | null;
refreshStatus: 'idle' | 'pending' | 'success' | 'error';
refreshError: string | null;
// Actions
setTokens: (accessToken: string, refreshToken: string) => void;
clearTokens: () => void;
setRefreshStatus: (status: 'idle' | 'pending' | 'success' | 'error') => void;
setRefreshError: (error: string | null) => void;
refreshAccessToken: () => Promise<boolean>;
}
const useAuthStore = create<AuthStore>()(
persist(
(set, get) => ({
accessToken: null,
refreshToken: null,
isAuthenticated: false,
tokenExpiry: null,
refreshStatus: 'idle',
refreshError: null,
setTokens: (accessToken, refreshToken) => {
const exp = Math.floor(Date.now() / 1000) + 3600; // 1 hour expiry
set({
accessToken,
refreshToken,
isAuthenticated: true,
tokenExpiry: exp
});
},
clearTokens: () => {
set({
accessToken: null,
refreshToken: null,
isAuthenticated: false,
tokenExpiry: null,
refreshStatus: 'idle',
refreshError: null
});
},
setRefreshStatus: (status) => {
set({ refreshStatus: status });
},
setRefreshError: (error) => {
set({ refreshError: error });
},
refreshAccessToken: async () => {
const { refreshToken, refreshStatus } = get();
// Prevent concurrent refresh attempts
if (refreshStatus === 'pending' || !refreshToken) {
return false;
}
set({ refreshStatus: 'pending' });
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Token refresh failed');
}
const { accessToken } = await response.json();
set({
accessToken,
refreshStatus: 'success',
refreshError: null
});
return true;
} catch (error) {
set({
refreshStatus: 'error',
refreshError: error instanceof Error ? error.message : 'Unknown refresh error'
});
// Clear tokens on refresh failure
get().clearTokens();
return false;
}
},
}),
{
name: 'auth-storage',
partialize: (state) => ({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
}),
}
)
);
// Custom hook for token refresh with automatic retry logic
export const useTokenRefresh = () => {
const {
accessToken,
refreshToken,
isAuthenticated,
refreshStatus,
refreshAccessToken,
setRefreshError
} = useAuthStore();
const handleTokenRefresh = async (): Promise<boolean> => {
if (refreshStatus === 'pending') {
// Wait for ongoing refresh to complete
return new Promise((resolve) => {
const interval = setInterval(() => {
if (useAuthStore.getState().refreshStatus !== 'pending') {
clearInterval(interval);
resolve(useAuthStore.getState().isAuthenticated);
}
}, 100);
});
}
if (refreshStatus === 'error') {
// Clear error state and retry
setRefreshError(null);
}
return refreshAccessToken();
};
return {
accessToken,
isAuthenticated,
refreshStatus,
handleTokenRefresh
};
};
// Token interceptor for API client
export const createAuthInterceptor = () => {
const store = useAuthStore.getState();
return async (request: Request): Promise<Request> => {
const { accessToken, refreshToken } = store;
// If we have a valid token, add it to the request
if (accessToken) {
return new Request(request, {
headers: {
...request.headers,
Authorization: `Bearer ${accessToken}`,
},
});
}
// If we don't have a token but have refresh token, attempt refresh
if (refreshToken && !store.refreshStatus) {
const success = await store.refreshAccessToken();
if (success) {
return new Request(request, {
headers: {
...request.headers,
Authorization: `Bearer ${store.accessToken}`,
},
});
}
}
return request;
};
};
The hook returns:
accessToken: Current valid access tokenisAuthenticated: Authentication status flagrefreshStatus: Current refresh operation statehandleTokenRefresh: Async function to trigger token refresh with proper concurrency handling
Key state transitions:
idle→pending→successorerror- Error state resets to
idleon retry - Concurrent refresh attempts are queued and handled sequentially
Side-effect handlers:
- Token refresh API calls
- Cache invalidation on successful refresh
- Error state management and user notifications
4. Routing & Navigation
Route Structure and Hierarchy
graph TD
A[Root] --> B[Public Routes]
A --> C[Protected Routes]
B --> B1[Login]
B --> B2[Register]
B --> B3[Forgot Password]
C --> C1[Dashboard]
C --> C2[Profile]
C --> C3[Settings]
C --> C4[Admin]
C1 --> C11[Analytics]
C1 --> C12[Reports]
The application will follow a protected route structure with:
- Public routes: Login, Register, Forgot Password (no authentication required)
- Protected routes: Dashboard, Profile, Settings, Admin (require valid auth token)
- Nested routes under protected areas for feature organization
Code Splitting Strategy
Route-based code splitting with React.lazy and Suspense:
// routes/index.tsx
import { lazy, Suspense } from 'react';
import { RouteObject } from 'react-router-dom';
const Login = lazy(() => import('../features/auth/Login'));
const Dashboard = lazy(() => import('../features/dashboard/Dashboard'));
const Profile = lazy(() => import('../features/profile/Profile'));
export const routes: RouteObject[] = [
{
path: '/login',
element: (
<Suspense fallback={<div>Loading...</div>}>
<Login />
</Suspense>
),
},
{
path: '/dashboard',
element: (
<ProtectedRoute>
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
</ProtectedRoute>
),
},
{
path: '/profile',
element: (
<ProtectedRoute>
<Suspense fallback={<div>Loading...</div>}>
<Profile />
</Suspense>
</ProtectedRoute>
),
},
];
Justification:
- Route-based splitting reduces initial bundle size
- Protected routes can be lazy-loaded after authentication check
- Public pages load immediately for better perceived performance
Navigation Guards and Authentication Flows
// components/ProtectedRoute.tsx
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/auth';
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const navigate = useNavigate();
const { token, refreshToken, isRefreshing } = useAuthStore();
useEffect(() => {
const checkAuth = async () => {
if (!token && !refreshToken) {
setIsAuthenticated(false);
return;
}
// If token is expired but refresh is in progress, wait
if (isRefreshing) {
// Optionally show loading spinner or redirect to login
return;
}
try {
const isValid = await validateToken(token);
if (isValid) {
setIsAuthenticated(true);
} else {
// Token expired, attempt refresh
const refreshed = await refreshAuth(refreshToken);
if (refreshed) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
}
} catch (error) {
setIsAuthenticated(false);
}
};
checkAuth();
}, [token, refreshToken, isRefreshing]);
if (isAuthenticated === null) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
navigate('/login');
return null;
}
return <>{children}</>;
};
Deep Linking Support and URL Parameter Design
URL structure will follow RESTful conventions:
/dashboard/analytics?period=7d&filter=active/profile/settings?tab=notifications
Query parameters will be normalized and validated:
// utils/url.ts
export const parseUrlParams = (search: string) => {
const params = new URLSearchParams(search);
return Object.fromEntries(params.entries());
};
export const buildUrl = (base: string, params: Record<string, string>) => {
const url = new URL(base, window.location.origin);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
return url.toString();
};
Browser History Management and Scroll Restoration
// components/ScrollRestoration.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export const ScrollRestoration = () => {
const location = useLocation();
useEffect(() => {
// Restore scroll position when navigating back
const handlePopState = () => {
window.scrollTo(0, 0);
};
window.addEventListener('popstate', handlePopState);
// Scroll to top on route change
window.scrollTo(0, 0);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, [location]);
return null;
};
The application will use React Router's built-in scroll restoration with custom handling for:
- Page-level scroll position preservation
- Deep link navigation consistency
- Smooth scrolling behavior for anchor links
5. API Integration Layer
API Client Design Pattern
We'll implement a centralized Axios instance with request/response interceptors to handle automatic token refresh and consistent error handling across all API calls.
// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api';
const apiClient: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
withCredentials: true, // Required for cookies
});
export default apiClient;
Request/Response Interceptors
The interceptor system handles automatic token refresh when a 401 Unauthorized response is received, ensuring that the user's session remains valid without interrupting their workflow.
// api/interceptors.ts
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
import apiClient from './client';
import { getAccessToken, setAccessToken } from '@/utils/authStorage';
import { refreshToken } from '@/services/authService';
let isRefreshing = false;
let failedQueue: Array<{ resolve: Function; reject: Function }> = [];
const processQueue = (error: AxiosError | null, token: string | null = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const token = getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
apiClient.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: AxiosError) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// If we're already refreshing, queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest._retry = true;
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
}).catch(err => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const newToken = await refreshToken();
setAccessToken(newToken);
processQueue(null, newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// Redirect to login page or trigger logout
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
Error Handling and Retry Strategy
Comprehensive error handling with retry logic for transient network issues, and proper user feedback mechanisms.
// api/errorHandler.ts
import { AxiosError } from 'axios';
export class ApiError extends Error {
constructor(
public status: number,
public message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
export const handleApiError = (error: AxiosError): ApiError => {
if (error.response) {
const { status, data } = error.response;
// Handle specific HTTP status codes
switch (status) {
case 401:
return new ApiError(401, 'Unauthorized - Please log in again', 'UNAUTHORIZED');
case 403:
return new ApiError(403, 'Forbidden - You do not have permission', 'FORBIDDEN');
case 404:
return new ApiError(404, 'Resource not found', 'NOT_FOUND');
case 429:
return new ApiError(429, 'Rate limit exceeded', 'RATE_LIMITED');
case 500:
return new ApiError(500, 'Internal server error', 'INTERNAL_ERROR');
default:
return new ApiError(status, data?.message || 'API request failed', 'UNKNOWN_ERROR');
}
}
if (error.request) {
return new ApiError(0, 'Network error - Please check your connection', 'NETWORK_ERROR');
}
return new ApiError(0, error.message || 'Unknown API error', 'UNKNOWN_ERROR');
};
// Retry configuration
export const retryConfig = {
retries: 3,
retryDelay: (retryCount: number) => Math.min(1000 * 2 ** retryCount, 10000),
retryCondition: (error: AxiosError) => {
return (
error.code === 'ECONNABORTED' ||
error.message.includes('timeout') ||
[500, 502, 503, 504].includes(error.response?.status || 0)
);
}
};
Loading, Error, and Empty States Design
Consistent state management across all API-consuming components with proper loading indicators and error feedback.
// hooks/useApi.ts
import { useState, useEffect } from 'react';
import { AxiosError } from 'axios';
import { ApiError, handleApiError } from '@/api/errorHandler';
type LoadingState = 'idle' | 'loading' | 'success' | 'error';
interface ApiResult<T> {
data: T | null;
loading: LoadingState;
error: ApiError | null;
refetch: () => void;
}
export const useApi = <T>(apiCall: () => Promise<T>): ApiResult<T> => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<LoadingState>('idle');
const [error, setError] = useState<ApiError | null>(null);
const refetch = async () => {
setLoading('loading');
setError(null);
try {
const result = await apiCall();
setData(result);
setLoading('success');
} catch (err) {
const apiError = handleApiError(err as AxiosError);
setError(apiError);
setLoading('error');
}
};
useEffect(() => {
refetch();
}, []);
return { data, loading, error, refetch };
};
Data Fetching Strategy
For a modern Next.js application, we'll use a hybrid approach combining server-side rendering for initial loads and client-side fetching for dynamic updates:
- SSR Routes: Use getServerSideProps for authenticated pages that require initial data
- CSR Routes: Use React Query for client-side data fetching with automatic caching and refetching
- ISR Routes: For content that needs to be updated periodically but doesn't require real-time updates
// pages/dashboard.tsx
import { GetServerSideProps } from 'next';
import { dehydrate, QueryClient } from '@tanstack/react-query';
export const getServerSideProps: GetServerSideProps = async (context) => {
const queryClient = new QueryClient();
// Fetch initial data on server
await queryClient.prefetchQuery(['userProfile'], () => fetchUserProfile());
return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
};
// Component using React Query
import { useQuery } from '@tanstack/react-query';
const Dashboard = () => {
const { data, isLoading, error } = useQuery(['userProfile'], fetchUserProfile);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Welcome, {data?.name}</h1>
{/* Dashboard content */}
</div>
);
};
API Response Transformation Layer
Consistent data transformation from raw API responses to domain models for better type safety and maintainability.
// api/transformers.ts
export interface UserResponse {
id: string;
name: string;
email: string;
createdAt: string;
}
export interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
export const transformUserResponse = (response: UserResponse): User => ({
id: response.id,
name: response.name,
email: response.email,
createdAt: new Date(response.createdAt),
});
Sequence Diagram for Primary Data Flows
sequenceDiagram
participant Client
participant AuthInterceptor
participant AuthService
participant TokenService
Client->>AuthInterceptor: Make API request with token
AuthInterceptor->>AuthInterceptor: Check token validity
AuthInterceptor->>AuthService: Token invalid, request refresh
AuthService->>TokenService: Request new token
TokenService->>AuthService: Return new access token
AuthService->>AuthInterceptor: Return new token to client
AuthInterceptor->>Client: Retry original request with new token
Client->>Client: Process successful response
Note over AuthService,TokenService: Token refresh happens transparently
Authentication Service Implementation
// services/authService.ts
import apiClient from '@/api/client';
export const refreshToken = async (): Promise<string> => {
try {
const response = await apiClient.post('/auth/refresh');
return response.data.accessToken;
} catch (error) {
throw new Error('Failed to refresh token');
}
};
export const logout = async (): Promise<void> => {
try {
await apiClient.post('/auth/logout');
} catch (error) {
console.error('Logout failed', error);
} finally {
// Clear local storage and redirect
localStorage.removeItem('accessToken');
window.location.href = '/login';
}
};
6. Performance Design
Core Web Vitals Targets
- Largest Contentful Paint (LCP): <2.5s for 95% of users
- Interaction to Next Paint (INP): <200ms for 90% of interactions
- Cumulative Layout Shift (CLS): <0.1 for 95% of users
- First Contentful Paint (FCP): <1.8s for 95% of users
Bundle Size Budget per Route
- Home/Overview: <200KB (including all dependencies)
- Auth Flow: <150KB (token refresh logic + auth components)
- Profile/Settings: <100KB (minimal dependencies)
- Data-heavy Views: <300KB (virtualized lists, charts)
Critical Rendering Path Optimization
- Critical CSS: Inline essential styles for above-the-fold content
- Resource Hints: Preload critical fonts and assets (
<link rel="preload">) - Code Splitting: Route-based chunking to reduce initial load
- Lazy Loading: Non-critical components loaded on demand
Image Optimization Strategy
- Formats: WebP for modern browsers, JPEG fallback for older
- Lazy Loading:
loading="lazy"attribute on all images - Responsive Images:
srcsetwith multiple sizes for different viewports - Image Compression: Server-side optimization with WebP conversion
Font Loading Strategy
- Font Display:
font-display: swapto prevent FOIT - Preload Critical Fonts:
<link rel="preload" as="font">for primary font families - Font Subsets: Load only necessary character sets (Latin, Cyrillic, etc.)
- Fallback Fonts: Use system fonts as fallback to prevent layout shifts
Memoization Strategy
// ❌ Avoid - unnecessary re-renders
const UserCard = ({ user }: { user: User }) => {
const formattedName = formatUserName(user.name);
return <div>{formattedName}</div>;
};
// ✅ Do - memoize expensive operations
const UserCard = React.memo(({ user }: { user: User }) => {
const formattedName = useMemo(() => formatUserName(user.name), [user.name]);
return <div>{formattedName}</div>;
});
// ✅ Do - memoize callback handlers
const UserList = ({ users }: { users: User[] }) => {
const handleUserClick = useCallback((userId: string) => {
// navigation logic
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>
<UserCard user={user} onClick={handleUserClick} />
</li>
))}
</ul>
);
};
Virtualization for Large Lists
// For large lists, use react-window or react-virtual
import { FixedSizeList as List } from 'react-window';
const VirtualizedUserList = ({ users }: { users: User[] }) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<UserCard user={users[index]} />
</div>
);
return (
<List
height={600}
itemCount={users.length}
itemSize={80}
width="100%"
>
{Row}
</List>
);
};
Prefetching and Preloading Strategy
- Token Refresh: Preemptively refresh token 5 minutes before expiration
- Critical Routes: Prefetch data for next likely route (e.g., dashboard after login)
- Preload Dependencies: Load heavy components only when user is likely to navigate there
- Background Fetching: Fetch non-critical data in background when user is idle
// Token refresh prefetching strategy
const tokenRefreshService = {
// Schedule refresh 5 minutes before expiration
scheduleTokenRefresh: (tokenExpiry: Date) => {
const refreshTime = new Date(tokenExpiry).getTime() - 5 * 60 * 1000;
const timeUntilRefresh = Math.max(0, refreshTime - Date.now());
setTimeout(() => {
// Refresh token in background
refreshToken();
}, timeUntilRefresh);
}
};
7. Error Handling & Resilience
Error Boundary Placement Strategy
graph TD
A[App Root] --> B[ErrorBoundary]
B --> C[MainLayout]
C --> D[DashboardPage]
C --> E[UserSettingsPage]
D --> F[DashboardContent]
D --> G[UserList]
G --> H[UserCard]
G --> I[UserCardSkeleton]
F --> J[AnalyticsWidget]
F --> K[RecentActivity]
Error boundaries are strategically placed at the component tree levels where graceful degradation is most important:
- App-level boundary: Catches unhandled errors in the entire application
- Page-level boundaries: Wrap individual pages to prevent cascading failures
- Component-level boundaries: Wrap critical sub-components like user cards or analytics widgets
Global Error Handler Design
// src/core/error/GlobalErrorHandler.tsx
import React from 'react';
interface GlobalErrorState {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
class GlobalErrorHandler extends React.Component<{}, GlobalErrorState> {
constructor(props: {}) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): GlobalErrorState {
return { hasError: true, error, errorInfo: null };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
this.setState({ errorInfo });
// Log to monitoring service (Sentry, etc.)
console.error('Global error caught:', error, errorInfo);
}
render(): React.ReactNode {
if (this.state.hasState) {
return (
<div className="error-container">
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo?.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
export default GlobalErrorHandler;
Offline / Network Failure Handling
// src/core/network/NetworkStatusManager.ts
import { useEffect, useState } from 'react';
export const useNetworkStatus = () => {
const [isOnline, setIsOnline] = useState<boolean>(navigator.onLine);
const [lastError, setLastError] = useState<Error | null>(null);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const handleNetworkError = (error: Error) => {
setLastError(error);
// Trigger retry logic or fallback UI
};
return { isOnline, lastError, handleNetworkError };
};
Graceful Degradation Patterns
// src/components/UserList/UserList.tsx
import React, { useEffect, useState } from 'react';
import { useNetworkStatus } from '../../core/network/NetworkStatusManager';
interface User {
id: string;
name: string;
email: string;
}
const UserList = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { isOnline } = useNetworkStatus();
useEffect(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const userData = await response.json();
setUsers(userData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch users');
// Fallback to cached data if available
if (localStorage.getItem('cachedUsers')) {
setUsers(JSON.parse(localStorage.getItem('cachedUsers') || '[]'));
}
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) {
return <UserListSkeleton />;
}
if (error && !isOnline) {
return (
<div className="offline-fallback">
<p>Offline mode - showing cached data</p>
<UserList items={users} />
</div>
);
}
if (error && isOnline) {
return (
<div className="error-fallback">
<p>Failed to load users. Please try again later.</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return <UserList items={users} />;
};
export default UserList;
User Feedback on Errors
// src/core/toast/ToastManager.tsx
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
interface ToastOptions {
autoClose?: number;
position?: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
hideProgressBar?: boolean;
closeOnClick?: boolean;
pauseOnHover?: boolean;
}
export const ToastManager = () => (
<ToastContainer
position="top-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop
closeOnClick
rtl={false}
pauseOnHover
/>
);
export const showTokenRefreshError = (message: string) => {
toast.error(message, {
position: "top-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
});
};
export const showNetworkError = (message: string) => {
toast.error(message, {
position: "top-center",
autoClose: 3000,
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
});
};
export const showOfflineWarning = () => {
toast.warning("You're offline. Some features may be limited.", {
position: "bottom-center",
autoClose: 4000,
hideProgressBar: true,
closeOnClick: false,
pauseOnHover: false,
});
};
Retry Strategy Implementation
// src/core/api/RetryableRequest.ts
interface RetryConfig {
maxRetries?: number;
retryDelay?: number;
backoffMultiplier?: number;
}
const defaultRetryConfig: RetryConfig = {
maxRetries: 3,
retryDelay: 1000,
backoffMultiplier: 2,
};
export const withRetry = async <T>(
requestFn: () => Promise<T>,
config: RetryConfig = defaultRetryConfig
): Promise<T> => {
const { maxRetries, retryDelay, backoffMultiplier } = config;
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
throw lastError;
}
// Exponential backoff
const delay = retryDelay * Math.pow(backoffMultiplier, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
};
Token Refresh Error Recovery
// src/core/auth/TokenRefreshManager.ts
import { useState, useEffect } from 'react';
import { withRetry } from '../api/RetryableRequest';
interface TokenRefreshState {
isRefreshing: boolean;
lastTokenRefresh: Date | null;
refreshError: Error | null;
}
export const useTokenRefresh = () => {
const [state, setState] = useState<TokenRefreshState>({
isRefreshing: false,
lastTokenRefresh: null,
refreshError: null,
});
const refreshAccessToken = async (): Promise<string> => {
try {
setState(prev => ({ ...prev, isRefreshing: true }));
const response = await withRetry(() =>
fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: getStoredRefreshToken() })
})
);
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const { accessToken } = await response.json();
storeAccessToken(accessToken);
setState(prev => ({
...prev,
isRefreshing: false,
lastTokenRefresh: new Date(),
refreshError: null
}));
return accessToken;
} catch (error) {
setState(prev => ({
...prev,
isRefreshing: false,
refreshError: error as Error
}));
throw error;
}
};
return {
...state,
refreshAccessToken
};
};
8. Testing Strategy
Unit Testing Approach
Component-level testing (70% coverage target):
- Test all interactive components with various state transitions
- Mock external dependencies (API calls, browser APIs)
- Verify proper rendering for all state combinations (loading, success, error)
- Test edge cases: empty data, malformed responses, invalid inputs
Utility function testing (90% coverage target):
- Token validation logic with various JWT formats
- Refresh token expiration handling
- Error state normalization functions
- HTTP status code mapping to user-facing messages
Custom hook testing (85% coverage target):
- Test token refresh side effects and state transitions
- Verify proper cleanup of timers and subscriptions
- Mock API responses to test error recovery flows
- Test concurrent refresh attempts and race conditions
Integration Testing Strategy
API integration tests (60% coverage):
- End-to-end flow from token expiration to refresh success
- Test concurrent requests during refresh window (401 handling)
- Verify proper token propagation to downstream API calls
- Test refresh failure scenarios with fallback logic
State synchronization tests (50% coverage):
- Ensure UI updates correctly when tokens refresh
- Validate that pending requests are retried with new tokens
- Test session persistence across page reloads (localStorage)
- Verify token refresh doesn't break ongoing user interactions
E2E Testing Scope
Cypress-based testing (focus on critical flows):
- Authentication flow: login → token refresh → logout
- Concurrent request handling during refresh window
- Session timeout and automatic re-login scenarios
- User journey: token expiration mid-action → retry success
Test coverage priorities:
- Token refresh during active user session (high priority)
- Concurrent API calls during refresh window (medium priority)
- Token expiration edge cases (low priority but critical)
Visual Regression Testing
Screenshot testing strategy:
- Capture UI states during token refresh transitions
- Test loading spinners, error messages, and success states
- Validate responsive behavior across device sizes
- Monitor for layout shifts during refresh operations
Accessibility Testing Automation
axe-core integration (100% coverage):
- Test keyboard navigation through token refresh states
- Verify proper ARIA attributes on loading indicators
- Ensure focus management during error recovery flows
- Validate screen reader announcements for refresh status
Test Data Strategy
Test data fixtures:
- Valid JWT tokens with various expiration times (immediate, 1min, 30min)
- Invalid token formats and malformed payloads
- API response mocks with different HTTP status codes (401, 403, 500)
- User session states: authenticated, expired, pending refresh
Test data management:
- Use deterministic fixtures for reproducible test runs
- Separate real API credentials from test data
- Implement test environment configuration overrides
- Maintain token expiration timeline for time-based tests
9. Accessibility (A11y)
WCAG 2.1 AA compliance checklist for this feature
The automatic token refresh system must maintain full accessibility compliance across all user interactions and error states:
- Keyboard Navigation: All token refresh flows must be fully operable via keyboard (Tab/Shift+Tab navigation, Enter/Space activation)
- Focus Management: Proper focus restoration after token refresh operations
- Screen Reader Support: ARIA labels and roles for token refresh indicators and error states
- Contrast Ratios: Sufficient color contrast for token refresh status indicators (4.5:1 minimum)
- Error Handling: Clear error messages with appropriate ARIA attributes for assistive technologies
- Time Limits: No automatic token refresh that interferes with user control or time limits
Keyboard navigation support
All token refresh operations must be keyboard-accessible:
- Users can navigate to token refresh indicators using Tab key
- Refresh actions are triggerable with Enter or Space key
- Focus is properly managed during refresh operations (no focus loss)
- Keyboard users receive appropriate feedback when tokens are refreshed
Focus management strategy
Focus handling during token refresh:
stateDiagram-v2
[*] --> Idle
Idle --> Refreshing : User triggers refresh
Refreshing --> Success : Token refreshed successfully
Refreshing --> Error : Authentication failed
Success --> Idle : Focus restored to original context
Error --> Idle : Focus restored to error context
Focus management considerations:
- Preserve user's original focus position when refresh begins
- Restore focus to the most relevant element after refresh completes
- Implement proper focus trapping for modal refresh states
- Handle focus restoration in case of network errors or timeouts
ARIA roles and attributes plan
ARIA implementation for token refresh:
// TokenRefreshIndicator.tsx
interface TokenRefreshIndicatorProps {
status: 'idle' | 'refreshing' | 'success' | 'error';
message?: string;
onRefresh?: () => void;
}
const TokenRefreshIndicator = ({
status,
message,
onRefresh
}: TokenRefreshIndicatorProps) => {
const ariaLabel = status === 'refreshing'
? "Token refresh in progress"
: status === 'success'
? "Token refreshed successfully"
: status === 'error'
? "Token refresh failed"
: "Token refresh available";
return (
<div
role="status"
aria-live="polite"
aria-label={ariaLabel}
className={`token-refresh-indicator ${status}`}
>
{status === 'refreshing' && (
<span aria-hidden="true">🔄</span>
)}
{message && (
<span className="message">{message}</span>
)}
{status === 'error' && (
<button
onClick={onRefresh}
aria-label="Retry token refresh"
className="retry-button"
>
Retry
</button>
)}
</div>
);
};
Screen reader compatibility
Screen reader considerations:
- All token refresh status messages are announced via
aria-live="polite" - Error messages include clear guidance for users to retry or contact support
- Success notifications provide confirmation that the refresh was completed successfully
- Visual indicators have appropriate text alternatives for screen reader users
- Focus management ensures screen readers don't miss important state changes
Keyboard navigation and focus restoration patterns
// TokenRefreshManager.tsx - Focus management example
const useTokenRefreshFocus = () => {
const [lastFocusedElement, setLastFocusedElement] = useState<HTMLElement | null>(null);
const handleRefreshStart = () => {
// Store the currently focused element
setLastFocusedElement(document.activeElement as HTMLElement);
// Announce refresh start to screen readers
announceToScreenReader("Refreshing authentication token");
};
const handleRefreshComplete = () => {
// Restore focus to the previously focused element
if (lastFocusedElement && lastFocusedElement.focus) {
lastFocusedElement.focus();
}
};
return { handleRefreshStart, handleRefreshComplete };
};
ARIA state management for refresh operations
// TokenRefreshStatus.tsx - Complete accessibility implementation
const TokenRefreshStatus = () => {
const [status, setStatus] = useState<'idle' | 'refreshing' | 'success' | 'error'>('idle');
const [message, setMessage] = useState<string>('');
const handleRefresh = useCallback(() => {
setStatus('refreshing');
setMessage('Refreshing authentication token...');
// Announce to screen readers
announceToScreenReader("Authentication token refresh initiated");
// Refresh logic here...
}, []);
return (
<div
role="status"
aria-live="polite"
aria-label={status === 'refreshing'
? "Token refresh in progress"
: status === 'success'
? "Authentication token refreshed successfully"
: "Token refresh failed - please try again"
}
className={`token-refresh-status ${status}`}
>
<div
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={status === 'refreshing' ? 50 : 100}
aria-label="Token refresh progress"
className="progress-bar"
>
{status === 'refreshing' && (
<div className="progress-fill" style={{ width: '50%' }} />
)}
</div>
<span className="status-message">
{message ||
(status === 'success' ? "Token refreshed successfully" :
status === 'error' ? "Token refresh failed" :
"Ready to refresh token")}
</span>
{status === 'error' && (
<button
onClick={handleRefresh}
aria-label="Retry token refresh"
className="retry-button"
>
Retry
</button>
)}
</div>
);
};
Focus trapping and restoration
// useFocusTrap.ts - Custom hook for focus management
const useFocusTrap = (isActive: boolean, containerRef: React.RefObject<HTMLElement>) => {
useEffect(() => {
if (!isActive) return;
const handleFocusTrap = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
const focusableElements = containerRef.current?.querySelectorAll(
'button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
) || [];
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
if (event.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
event.preventDefault();
} else if (!event.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
event.preventDefault();
}
}
};
document.addEventListener('keydown', handleFocusTrap);
return () => document.removeEventListener('keydown', handleFocusTrap);
}, [isActive]);
};
Screen reader announcement helper
// announceToScreenReader.ts - Utility for screen reader announcements
const announceToScreenReader = (message: string, priority: 'polite' | 'assertive' = 'polite') => {
// Create a live region element if it doesn't exist
let liveRegion = document.getElementById('live-region');
if (!liveRegion) {
liveRegion = document.createElement('div');
liveRegion.id = 'live-region';
liveRegion.setAttribute('aria-live', priority);
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.style.position = 'absolute';
liveRegion.style.left = '-9999px';
document.body.appendChild(liveRegion);
}
// Update the content
liveRegion.textContent = message;
// Clear after a short delay to ensure announcement
setTimeout(() => {
liveRegion.textContent = '';
}, 1000);
};
Accessibility testing considerations
- Test with screen readers (NVDA, JAWS, VoiceOver)
- Verify keyboard navigation through all token refresh states
- Validate ARIA attributes and roles in browser developer tools
- Test focus management with keyboard-only navigation
- Ensure proper announcement of status changes to screen reader users
- Validate color contrast for all token refresh status indicators
10. Security Considerations
XSS Prevention Strategy
Content Sanitization: All user-generated content must be sanitized before rendering to prevent script injection. Implement a robust HTML sanitizer like DOMPurify for any content that comes from external sources.
// Example of XSS prevention in a component
import DOMPurify from 'dompurify';
const sanitizeAndRender = (htmlContent: string) => {
const cleanHtml = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em'],
ALLOWED_ATTR: []
});
return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
};
Content Security Policy (CSP): Implement strict CSP headers to prevent inline script execution and restrict external resource loading.
CSRF Protection
Token-Based Protection: Implement anti-CSRF tokens for all state-changing requests:
- Generate unique CSRF tokens per session
- Include tokens in request headers or form data
- Validate tokens server-side before processing requests
// Example CSRF token implementation
const useCSRFToken = () => {
const [csrfToken, setCsrfToken] = useState<string | null>(null);
useEffect(() => {
// Fetch CSRF token on component mount
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setCsrfToken(data.token));
}, []);
return csrfToken;
};
Content Security Policy (CSP) Headers
Header Configuration: Configure CSP headers to restrict:
- Script sources (only allow trusted CDNs and local scripts)
- Image sources (prevent loading from untrusted domains)
- Frame sources (prevent framing attacks)
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
img-src 'self' data: https:;
frame-ancestors 'none';
Sensitive Data Handling in Frontend State
Token Storage Security:
- Never store refresh tokens in localStorage or cookies with HTTPOnly flag disabled
- Use secure, HttpOnly cookies for refresh tokens (if server-side storage is preferred)
- Store access tokens in memory only (in-memory cache or React Context)
- Implement automatic token clearing on logout and session timeout
// Secure token management pattern
class TokenManager {
private accessToken: string | null = null;
private refreshToken: string | null = null;
setTokens(accessToken: string, refreshToken: string) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
// Store in secure context, not localStorage
// Use React Context or Redux for access token
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
// Clear from any in-memory stores
// Remove from React Context or Redux state
}
}
Input Sanitization Approach
Input Validation:
- Validate all user inputs on both client and server
- Implement strict input sanitization for:
- Usernames (no special characters in certain contexts)
- Email addresses (format validation)
- Text content (length limits, character restrictions)
// Input sanitization example
const sanitizeInput = (input: string): string => {
// Remove potentially dangerous characters
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
Additional Security Considerations
Secure Communication:
- All API requests must use HTTPS only
- Implement HSTS headers to enforce secure connections
Session Management:
- Implement automatic session timeout after inactivity
- Validate token expiration on every request
- Implement proper logout handling to clear all session data
Rate Limiting:
- Implement client-side request throttling to prevent abuse
- Handle rate limit errors gracefully with user feedback
Token Refresh Security:
- Refresh tokens should be short-lived and rotated
- Implement token revocation on logout
- Use secure random generation for token values
11. Internationalization (i18n)
Translation Key Structure and Naming Convention
Following BEM-style naming with feature-scoped keys to prevent collisions:
{
"auth": {
"login": {
"title": "Login to Your Account",
"email_label": "Email Address",
"password_label": "Password",
"submit_button": "Sign In"
},
"logout": {
"confirm_message": "Are you sure you want to log out?",
"yes_button": "Yes, Log Out",
"no_button": "Cancel"
}
},
"common": {
"loading": "Loading...",
"error": "An error occurred",
"back_button": "Back"
}
}
RTL Support Considerations
RTL support is implemented through CSS classes and direction properties:
- CSS
direction: rtlapplied conditionally via theme context - Component-level RTL-aware layout components (
Flex,Grid) that flip margins/padding - Text alignment properties (
text-align: rightfor RTL) - Scroll behavior adjustments (right-aligned scrollbars in RTL mode)
Date, Number, Currency Formatting Strategy
Using Intl API for locale-aware formatting:
// Format date with timezone awareness
const formatDate = (date: Date, locale: string, options?: Intl.DateTimeFormatOptions) => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
...options
}).format(date);
};
// Format currency with proper symbol placement
const formatCurrency = (amount: number, locale: string, currency: string) => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
minimumFractionDigits: 2
}).format(amount);
};
// Format numbers with grouping separators
const formatNumber = (num: number, locale: string) => {
return new Intl.NumberFormat(locale).format(num);
};
Locale Detection and Switching Strategy
Automatic locale detection using browser navigator:
const detectLocale = (): string => {
const browserLang = navigator.language || 'en-US';
const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'de-DE'];
// Match exact locale or fallback to language code
const matched = supportedLocales.find(locale =>
locale === browserLang ||
locale.startsWith(browserLang.split('-')[0])
);
return matched || 'en-US';
};
Manual locale switching via user preference:
const useLocaleSwitcher = () => {
const [currentLocale, setCurrentLocale] = useState<string>('en-US');
const switchLocale = useCallback((newLocale: string) => {
setCurrentLocale(newLocale);
// Update localStorage and apply to document
localStorage.setItem('preferred-locale', newLocale);
document.documentElement.lang = newLocale;
}, []);
return { currentLocale, switchLocale };
};
Translation Key Management
Translation files organized by locale:
/src/locales/
├── en-US.json
├── fr-FR.json
├── es-ES.json
└── de-DE.json
i18n Hook Implementation
interface TranslationContextType {
t: (key: string, params?: Record<string, unknown>) => string;
locale: string;
setLocale: (locale: string) => void;
}
const TranslationContext = createContext<TranslationContextType>({
t: () => '',
locale: 'en-US',
setLocale: () => {},
});
const useTranslation = (): TranslationContextType => {
const context = useContext(TranslationContext);
if (!context) {
throw new Error('useTranslation must be used within TranslationProvider');
}
return context;
};
const TranslationProvider: React.FC<{
children: React.ReactNode;
initialLocale?: string;
}> = ({ children, initialLocale = 'en-US' }) => {
const [locale, setLocale] = useState<string>(initialLocale);
const [translations, setTranslations] = useState<Record<string, any>>({});
useEffect(() => {
const loadTranslations = async () => {
try {
const translationModule = await import(`../locales/${locale}.json`);
setTranslations(translationModule.default);
} catch (error) {
console.error(`Failed to load translations for ${locale}`, error);
}
};
loadTranslations();
}, [locale]);
const t = useCallback((key: string, params?: Record<string, unknown>): string => {
const keys = key.split('.');
let value = translations;
for (const k of keys) {
value = value?.[k];
if (value === undefined) return key;
}
if (typeof value === 'string' && params) {
return Object.entries(params).reduce((str, [key, val]) =>
str.replace(`{{${key}}}`, String(val)), value);
}
return value as string;
}, [translations]);
return (
<TranslationContext.Provider value={{ t, locale, setLocale }}>
{children}
</TranslationContext.Provider>
);
};
Component Integration Example
const LoginForm: React.FC = () => {
const { t, locale } = useTranslation();
return (
<div dir={locale.includes('ar') || locale.includes('he') ? 'rtl' : 'ltr'}>
<h1>{t('auth.login.title')}</h1>
<label htmlFor="email">{t('auth.login.email_label')}</label>
<input id="email" type="email" />
<label htmlFor="password">{t('auth.login.password_label')}</label>
<input id="password" type="password" />
<button type="submit">{t('auth.login.submit_button')}</button>
</div>
);
};
Testing Strategy
- Translation key coverage validation using JSON schema
- RTL layout testing with visual regression tools
- Locale switching behavior tests in CI pipeline
- Missing translation detection during build process
12. Monitoring & Observability
Real User Monitoring (RUM) Metrics to Track
For automatic token refresh, we must monitor key performance indicators that directly impact user experience:
- Token Refresh Success Rate: Percentage of successful token refresh operations vs total attempts
- Refresh Latency: Time from token expiration to successful refresh completion (target: <200ms)
- Concurrent Refresh Requests: Number of simultaneous refresh requests to prevent race conditions (target: <3 concurrent)
- Failed Refresh Rate: Percentage of refresh attempts that result in authentication errors (target: <0.1%)
- Token Expiry Window: Time between token expiration and refresh initiation (target: <5 seconds)
Error Tracking Integration
We integrate with Sentry for comprehensive error tracking:
- Authentication Errors: Capture failed refresh attempts, expired tokens, and authentication failures
- Network Errors: Track failed refresh requests due to connectivity issues or server errors
- Rate Limiting Events: Monitor when API returns 429 responses during refresh operations
- Critical Path Failures: Flag refresh failures that prevent user actions (e.g., failed refresh prevents data fetch)
Custom Events and User Journey Tracking
We implement custom tracking for token refresh workflows:
// Token refresh lifecycle events
const trackTokenRefreshEvent = (event: 'start' | 'success' | 'failure', metadata?: any) => {
// Send to analytics service (e.g., Mixpanel, Amplitude)
analytics.track('token_refresh', {
event,
timestamp: Date.now(),
...metadata
});
};
Performance Monitoring and Regression Detection
- Refresh Request Duration: Track average time for refresh requests with 95th percentile monitoring
- Memory Usage: Monitor heap usage during refresh operations to detect memory leaks
- CPU Utilization: Track refresh operation impact on main thread performance
- Bundle Size Impact: Monitor token refresh utility bundle size changes over time
Implementation Example
// Performance monitoring hook for token refresh
import { useEffect, useRef } from 'react';
const useTokenRefreshMonitoring = () => {
const refreshStartTime = useRef<number | null>(null);
const startRefreshTimer = () => {
refreshStartTime.current = performance.now();
trackTokenRefreshEvent('start');
};
const endRefreshTimer = (success: boolean, error?: Error) => {
if (refreshStartTime.current) {
const duration = performance.now() - refreshStartTime.current;
trackTokenRefreshEvent(success ? 'success' : 'failure', {
duration,
error: error?.message
});
// Alert on slow refreshes (>500ms)
if (duration > 500) {
console.warn(`Token refresh took ${duration}ms`, { duration });
}
}
};
return { startRefreshTimer, endRefreshTimer };
};
Alerting Strategy
- High-Failure Rate: Trigger alerts when >1% of refresh attempts fail
- Slow Response: Alert on 95th percentile refresh times >500ms
- Concurrent Failures: Alert when >3 simultaneous refresh failures occur
- Critical Path Degradation: Monitor for 20%+ increase in refresh failure rate over 5-minute window
Data Collection and Privacy Considerations
All monitoring data is collected with user consent and follows privacy regulations:
- Anonymous tracking of refresh success/failure rates
- No PII collected in monitoring data
- User opt-out capability for analytics tracking
- Data retention policies aligned with privacy regulations
13. File & Folder Structure
Proposed Directory Structure
/src
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ └── (protected)/
│ ├── layout.tsx
│ └── dashboard/
│ ├── page.tsx
│ └── components/
├── features/
│ ├── auth/
│ │ ├── index.ts
│ │ ├── hooks/
│ │ │ ├── useAuth.ts
│ │ │ └── useTokenRefresh.ts
│ │ ├── services/
│ │ │ └── authService.ts
│ │ ├── utils/
│ │ │ └── tokenUtils.ts
│ │ └── types/
│ │ └── auth.types.ts
├── shared/
│ ├── components/
│ ├── hooks/
│ ├── utils/
│ └── types/
├── lib/
│ ├── api/
│ │ ├── index.ts
│ │ ├── client.ts
│ │ └── interceptors/
│ ├── store/
│ │ └── authStore.ts
│ └── constants/
├── types/
│ └── global.types.ts
└── styles/
└── globals.css
Module Boundaries and Import Rules
- Feature isolation: Auth feature is self-contained with its own hooks, services, and types
- No cross-feature imports: Auth module cannot directly import from other feature modules
- Shared components: Only presentational components that are truly reusable can be in
/shared - Barrel exports: Each feature has an
index.tsto export the public API
Shared vs Feature-Specific Code Separation
Shared code (reusable across features):
- Generic utility functions (
tokenUtils.ts) - Common hooks (
useAuth.ts) - Reusable components (
Button,Modal) - Global types (
global.types.ts)
Feature-specific code:
- Auth-specific services and logic
- Feature-level components and hooks
- Route-specific business logic
This structure supports clean separation of concerns while enabling easy maintenance and scalability. The feature-based organization makes it simple to understand the auth module's scope and dependencies, while the shared directory ensures proper reuse without tight coupling.
14. Dependencies & Third-Party Libraries
Key Libraries Selected with Justification
axios - Primary HTTP client for API requests
- Why: Excellent request/response interception, built-in cancelation support, and robust error handling capabilities
- Bundle size: ~15KB gzipped
- Features used: Interceptors, request/response transformation, cancelation tokens
react-query - Data fetching and state management
- Why: Superior caching strategy, automatic refetching, background updates, and built-in stale-while-revalidate patterns
- Bundle size: ~12KB gzipped
- Features used: QueryClient, useQuery, useMutation, cache invalidation
zustand - Lightweight state management for non-essential app state
- Why: Simple API with minimal boilerplate, better performance than Redux for smaller applications, easy to test
- Bundle size: ~2KB gzipped
- Features used: Middleware support, async state updates
date-fns - Date/time manipulation library
- Why: Tree-shakable, immutable, and better performance than moment.js for date operations
- Bundle size: ~2KB gzipped (with tree-shaking)
- Features used: Parsing, formatting, timezone handling
i18next - Internationalization framework
- Why: Production-ready with excellent ecosystem support, lazy loading capabilities, and strong community adoption
- Bundle size: ~5KB gzipped (core + react integration)
- Features used: Translation loading, namespace support, fallback mechanisms
Alternatives Considered and Rejected
Redux Toolkit - Rejected due to:
- Higher boilerplate overhead for simple state management needs
- More complex setup and configuration than Zustand for our use case
- Not necessary given the scope of state we need to manage
react-redux - Rejected due to:
- Complex provider setup and context management
- More verbose code for simple state access patterns
- Not justified by our application's complexity
moment.js - Rejected due to:
- Large bundle size (~20KB) with no tree-shaking support
- Mutable date objects causing unexpected side effects
- Performance issues in memory-constrained environments
swr - Rejected due to:
- Less flexible caching strategy than react-query for our specific use case
- More limited built-in support for optimistic updates and complex mutations
Licensing and Bundle Size Impact Assessment
axios: MIT license, minimal impact on bundle size react-query: MIT license, moderate bundle impact but justified by feature set zustand: MIT license, minimal impact on bundle size date-fns: MIT license, minimal impact with tree-shaking enabled i18next: MIT license, acceptable bundle size for internationalization needs
Performance Considerations
All selected libraries support tree-shaking to minimize final bundle size. The combination of react-query and zustand provides optimal performance characteristics for our application's state management needs while maintaining clean, maintainable code structure. The chosen libraries have been vetted for compatibility with modern React patterns and provide excellent developer experience with comprehensive documentation and community support.
15. Open Questions, Risks & Trade-offs
Decisions that need product/engineering alignment
- Token expiration window: What's the acceptable time window between token expiry and refresh? This directly impacts user experience vs. security trade-offs
- Refresh token rotation policy: Should refresh tokens be rotated on each use? This affects security posture vs. system complexity
- Concurrent request handling: How should the system handle multiple concurrent requests when a token refresh is in progress? Should they queue or fail?
Known trade-offs made in this design
- Optimistic refresh vs. blocking: The design prioritizes user experience by attempting optimistic refresh before request failure, but this introduces potential race conditions and complexity in error recovery
- Client-side token storage: Using localStorage for tokens trades convenience for security - more secure approaches like HttpOnly cookies would require backend changes
- Automatic refresh window: The 5-minute refresh window provides good UX but increases potential attack surface during that window
Risks and mitigations
- Token leakage risk: Storing tokens in localStorage makes them vulnerable to XSS attacks. Mitigation: Implement Content Security Policy (CSP) headers and sanitize all user inputs
- Race condition during refresh: Multiple concurrent requests may trigger multiple refresh operations. Mitigation: Implement request queuing and token sharing mechanism
- Refresh token theft: If refresh tokens are compromised, attackers can continuously generate new access tokens. Mitigation: Implement refresh token rotation and device fingerprinting
- Network failure during refresh: Refresh request may fail, leaving the app in an inconsistent state. Mitigation: Implement exponential backoff retry logic and fallback to login flow
Technical debt acknowledged
- Hardcoded refresh window: The 5-minute window is hardcoded and not configurable, creating technical debt for future feature adjustments
- Simplified error handling: Current design assumes refresh token is always valid, but real-world scenarios may require more sophisticated error categorization and handling
- No refresh token validation: The system doesn't validate refresh tokens against backend before using them, which could lead to invalid token usage if tokens are revoked server-side
- No token expiration monitoring: The system doesn't actively monitor token expiration to trigger refreshes, relying on time-based windowing instead of real-time status checks