State Security and Best Practices
1. Sensitive Data Handling in State
Implement secure patterns for handling sensitive data in application state to prevent exposure and leaks.
| Security Practice | Description | Use Case |
|---|---|---|
| Minimize state storage | Don't store sensitive data in state unnecessarily | Passwords, credit cards, SSN |
| Memory cleanup | Clear sensitive data on unmount | Authentication tokens, PII |
| Avoid localStorage | Don't persist sensitive data in browser storage | Passwords, personal information |
| Use secure contexts | Isolate sensitive state in protected contexts | Payment info, user credentials |
| Redact in DevTools | Prevent sensitive data in dev tools | Development debugging |
Example: Secure Sensitive Data Handling
// ❌ BAD: Storing sensitive data in state
function BadPaymentForm() {
const [creditCard, setCreditCard] = useState({
number: '',
cvv: '',
expiryDate: ''
});
// This data is visible in React DevTools and can be logged
return (
<form>
<input
value={creditCard.number}
onChange={(e) => setCreditCard({ ...creditCard, number: e.target.value })}
/>
</form>
);
}
// ✅ GOOD: Using uncontrolled inputs for sensitive data
function GoodPaymentForm() {
const cardNumberRef = useRef(null);
const cvvRef = useRef(null);
const expiryRef = useRef(null);
const handleSubmit = async (e) => {
e.preventDefault();
// Only access values when needed
const sensitiveData = {
cardNumber: cardNumberRef.current.value,
cvv: cvvRef.current.value,
expiry: expiryRef.current.value
};
// Send immediately without storing in state
await processPayment(sensitiveData);
// Clear immediately after use
cardNumberRef.current.value = '';
cvvRef.current.value = '';
expiryRef.current.value = '';
};
return (
<form onSubmit={handleSubmit}>
<input
ref={cardNumberRef}
type="text"
autoComplete="cc-number"
placeholder="Card Number"
/>
<input
ref={cvvRef}
type="text"
autoComplete="cc-csc"
placeholder="CVV"
/>
<input
ref={expiryRef}
type="text"
autoComplete="cc-exp"
placeholder="MM/YY"
/>
<button type="submit">Submit</button>
</form>
);
}
Example: Secure Context for Sensitive State
// SecureStateContext.jsx
import { createContext, useContext, useState, useEffect, useRef } from 'react';
const SecureStateContext = createContext(null);
export function SecureStateProvider({ children }) {
// Use ref to avoid DevTools exposure
const secureDataRef = useRef(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
// Store sensitive data in closure, not state
const setSensitiveData = (data) => {
secureDataRef.current = data;
};
const getSensitiveData = () => {
return secureDataRef.current;
};
const clearSensitiveData = () => {
if (secureDataRef.current) {
// Overwrite with zeros before clearing
if (typeof secureDataRef.current === 'object') {
Object.keys(secureDataRef.current).forEach(key => {
secureDataRef.current[key] = null;
});
}
secureDataRef.current = null;
}
};
// Clear on unmount
useEffect(() => {
return () => {
clearSensitiveData();
};
}, []);
// Clear on page visibility change (tab switch)
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
// Optionally clear when tab is hidden
// clearSensitiveData();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
const value = {
isAuthenticated,
setIsAuthenticated,
setSensitiveData,
getSensitiveData,
clearSensitiveData
};
return (
<SecureStateContext.Provider value={value}>
{children}
</SecureStateContext.Provider>
);
}
export function useSecureState() {
const context = useContext(SecureStateContext);
if (!context) {
throw new Error('useSecureState must be used within SecureStateProvider');
}
return context;
}
// Usage
function LoginForm() {
const { setSensitiveData, setIsAuthenticated } = useSecureState();
const passwordRef = useRef(null);
const handleLogin = async (e) => {
e.preventDefault();
const password = passwordRef.current.value;
// Don't store password in state!
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ password })
});
if (response.ok) {
const { token } = await response.json();
setSensitiveData({ token }); // Store in ref, not state
setIsAuthenticated(true);
passwordRef.current.value = ''; // Clear immediately
}
};
return (
<form onSubmit={handleLogin}>
<input ref={passwordRef} type="password" />
<button>Login</button>
</form>
);
}
Warning: Never store passwords, credit card numbers, or other highly sensitive data in React
state. Use uncontrolled inputs with refs, and transmit data directly to the server without storing it.
2. State Sanitization and Validation
Validate and sanitize all state data to prevent malicious input and ensure data integrity.
| Validation Pattern | Description | Use Case |
|---|---|---|
| Input validation | Validate before storing in state | Forms, user input |
| Type checking | Ensure correct data types | TypeScript, PropTypes |
| Sanitization | Remove dangerous characters/scripts | HTML content, user text |
| Schema validation | Validate against schema (Zod, Yup) | Complex objects, API responses |
| Whitelisting | Only allow known safe values | Enums, dropdown selections |
Example: State Validation with Zod
// userSchema.ts
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(['user', 'admin', 'moderator']),
bio: z.string().max(500).optional()
});
type User = z.infer<typeof userSchema>;
// UserForm.tsx
import { useState } from 'react';
import { userSchema } from './userSchema';
function UserForm() {
const [user, setUser] = useState<Partial<User>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const handleChange = (field: keyof User, value: any) => {
// Validate individual field
const fieldSchema = userSchema.shape[field];
const result = fieldSchema.safeParse(value);
if (result.success) {
setUser(prev => ({ ...prev, [field]: value }));
setErrors(prev => {
const { [field]: _, ...rest } = prev;
return rest;
});
} else {
setErrors(prev => ({
...prev,
[field]: result.error.errors[0].message
}));
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate entire object before submission
const result = userSchema.safeParse(user);
if (result.success) {
// Safe to use validated data
await saveUser(result.data);
setErrors({});
} else {
// Set all validation errors
const fieldErrors: Record<string, string> = {};
result.error.errors.forEach(err => {
if (err.path[0]) {
fieldErrors[err.path[0] as string] = err.message;
}
});
setErrors(fieldErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={user.name || ''}
onChange={(e) => handleChange('name', e.target.value)}
/>
{errors.name && <span className="error">{errors.name}</span>}
<input
value={user.email || ''}
onChange={(e) => handleChange('email', e.target.value)}
/>
{errors.email && <span className="error">{errors.email}</span>}
<button type="submit">Save</button>
</form>
);
}
Example: Input Sanitization for HTML Content
// sanitization.ts
import DOMPurify from 'dompurify';
// Sanitize HTML content
export function sanitizeHTML(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target']
});
}
// Sanitize plain text (remove HTML)
export function sanitizeText(input: string): string {
return input
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/[<>"']/g, '') // Remove dangerous characters
.trim()
.slice(0, 1000); // Limit length
}
// Validate and sanitize URL
export function sanitizeURL(url: string): string | null {
try {
const parsed = new URL(url);
// Only allow http and https protocols
if (!['http:', 'https:'].includes(parsed.protocol)) {
return null;
}
return parsed.href;
} catch {
return null;
}
}
// RichTextEditor.tsx
import { useState } from 'react';
import { sanitizeHTML } from './sanitization';
function RichTextEditor() {
const [content, setContent] = useState('');
const [sanitizedContent, setSanitizedContent] = useState('');
const handleChange = (rawHTML: string) => {
// Always sanitize before storing in state
const clean = sanitizeHTML(rawHTML);
setContent(rawHTML); // Original for editing
setSanitizedContent(clean); // Safe for rendering
};
const handleSave = async () => {
// Use sanitized version for API calls
await fetch('/api/content', {
method: 'POST',
body: JSON.stringify({ content: sanitizedContent })
});
};
return (
<div>
<textarea
value={content}
onChange={(e) => handleChange(e.target.value)}
/>
{/* Safe to render sanitized content */}
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
<button onClick={handleSave}>Save</button>
</div>
);
}
Example: Custom Validation Hook
// useValidatedState.ts
import { useState, useCallback } from 'react';
type Validator<T> = (value: T) => string | null;
export function useValidatedState<T>(
initialValue: T,
validators: Validator<T>[]
) {
const [value, setValue] = useState<T>(initialValue);
const [error, setError] = useState<string | null>(null);
const [touched, setTouched] = useState(false);
const validate = useCallback((newValue: T): boolean => {
for (const validator of validators) {
const errorMessage = validator(newValue);
if (errorMessage) {
setError(errorMessage);
return false;
}
}
setError(null);
return true;
}, [validators]);
const setValidatedValue = useCallback((newValue: T) => {
const isValid = validate(newValue);
if (isValid) {
setValue(newValue);
}
}, [validate]);
const forceSetValue = useCallback((newValue: T) => {
setValue(newValue);
validate(newValue);
}, [validate]);
const handleBlur = useCallback(() => {
setTouched(true);
validate(value);
}, [value, validate]);
return {
value,
setValue: setValidatedValue,
forceSetValue,
error: touched ? error : null,
isValid: !error,
touched,
setTouched,
handleBlur
};
}
// Validators
const required = (value: string) =>
value.trim() === '' ? 'This field is required' : null;
const minLength = (min: number) => (value: string) =>
value.length < min ? `Must be at least ${min} characters` : null;
const maxLength = (max: number) => (value: string) =>
value.length > max ? `Must be at most ${max} characters` : null;
const emailFormat = (value: string) =>
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'Invalid email format' : null;
// Usage
function SignupForm() {
const email = useValidatedState('', [required, emailFormat]);
const password = useValidatedState('', [required, minLength(8), maxLength(100)]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (email.isValid && password.isValid) {
console.log('Valid!', { email: email.value, password: password.value });
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email.value}
onChange={(e) => email.forceSetValue(e.target.value)}
onBlur={email.handleBlur}
/>
{email.error && <span>{email.error}</span>}
<input
type="password"
value={password.value}
onChange={(e) => password.forceSetValue(e.target.value)}
onBlur={password.handleBlur}
/>
{password.error && <span>{password.error}</span>}
<button type="submit">Sign Up</button>
</form>
);
}
Note: Always validate user input on both client and server. Client-side validation improves
UX,
but server-side validation is essential for security.
3. XSS Prevention in Dynamic State Rendering
Prevent cross-site scripting (XSS) attacks when rendering dynamic content from state.
| XSS Prevention | Description | Use Case |
|---|---|---|
| Automatic escaping | React escapes text content by default | Most text rendering |
| Avoid dangerouslySetInnerHTML | Don't use unless absolutely necessary | Rich text, Markdown |
| Sanitize HTML | Use DOMPurify before rendering HTML | User-generated HTML content |
| CSP headers | Content Security Policy headers | Server-side protection |
| Validate URLs | Check href/src before rendering | Links, images from user input |
Example: Safe vs Unsafe State Rendering
// ❌ UNSAFE: Direct rendering of user input in dangerouslySetInnerHTML
function UnsafeComment({ comment }) {
// If comment.text contains: <script>alert('XSS')</script>
// This will execute the script!
return (
<div dangerouslySetInnerHTML={{ __html: comment.text }} />
);
}
// ✅ SAFE: React automatically escapes text content
function SafeComment({ comment }) {
// React escapes HTML entities automatically
// <script> becomes <script> and won't execute
return <div>{comment.text}</div>;
}
// ✅ SAFE: Sanitize before using dangerouslySetInnerHTML
import DOMPurify from 'dompurify';
function SafeRichComment({ comment }) {
const [sanitizedHTML, setSanitizedHTML] = useState('');
useEffect(() => {
const clean = DOMPurify.sanitize(comment.html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
ALLOWED_URI_REGEXP: /^https?:\/\//
});
setSanitizedHTML(clean);
}, [comment.html]);
return (
<div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
);
}
// ❌ UNSAFE: User-controlled href
function UnsafeLink({ url, text }) {
// url could be: javascript:alert('XSS')
return <a href={url}>{text}</a>;
}
// ✅ SAFE: Validate URL protocol
function SafeLink({ url, text }) {
const [safeUrl, setSafeUrl] = useState('#');
useEffect(() => {
try {
const parsed = new URL(url);
if (['http:', 'https:'].includes(parsed.protocol)) {
setSafeUrl(parsed.href);
}
} catch {
setSafeUrl('#');
}
}, [url]);
return (
<a
href={safeUrl}
target="_blank"
rel="noopener noreferrer" // Prevent window.opener attacks
>
{text}
</a>
);
}
Example: Safe Markdown Rendering from State
// MarkdownRenderer.tsx
import { useState, useEffect } from 'react';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// Configure marked to be more secure
marked.setOptions({
headerIds: false,
mangle: false
});
function MarkdownRenderer({ content }) {
const [safeHTML, setSafeHTML] = useState('');
useEffect(() => {
// Convert markdown to HTML
const rawHTML = marked.parse(content);
// Sanitize the HTML
const clean = DOMPurify.sanitize(rawHTML, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'strong', 'em', 'u',
'ul', 'ol', 'li',
'a', 'code', 'pre',
'blockquote'
],
ALLOWED_ATTR: ['href', 'title'],
ALLOWED_URI_REGEXP: /^https?:\/\//,
// Add hooks to modify links
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false
});
setSafeHTML(clean);
}, [content]);
return (
<div
className="markdown-content"
dangerouslySetInnerHTML={{ __html: safeHTML }}
/>
);
}
// Usage with state
function BlogPost() {
const [post, setPost] = useState({ markdown: '' });
useEffect(() => {
// Fetch post from API
fetch('/api/posts/1')
.then(r => r.json())
.then(data => setPost(data));
}, []);
return (
<div>
<h1>{post.title}</h1>
{/* Safely render user-generated markdown */}
<MarkdownRenderer content={post.markdown} />
</div>
);
}
Example: CSP Integration with React State
// Create nonce for inline scripts
// server.js (Next.js, Express, etc.)
const crypto = require('crypto');
function generateCSPNonce() {
return crypto.randomBytes(16).toString('base64');
}
app.use((req, res, next) => {
const nonce = generateCSPNonce();
res.locals.cspNonce = nonce;
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'; ` +
`style-src 'self' 'nonce-${nonce}'; ` +
`img-src 'self' https:; ` +
`connect-src 'self' https://api.example.com; ` +
`default-src 'self'`
);
next();
});
// React component
function SecureComponent({ nonce }) {
const [data, setData] = useState(null);
useEffect(() => {
// CSP allows this fetch because connect-src includes the domain
fetch('https://api.example.com/data')
.then(r => r.json())
.then(setData);
}, []);
return (
<div>
{/* React automatically escapes - safe from XSS */}
<p>{data?.userInput}</p>
{/* Inline script requires nonce */}
<script nonce={nonce}>
{`console.log('This script has the correct nonce')`}
</script>
</div>
);
}
Warning: Never trust user input. Always sanitize HTML content before rendering with
dangerouslySetInnerHTML. Use DOMPurify or similar libraries to remove malicious code.
4. State Encryption for Client-side Storage
Encrypt sensitive state data before storing in localStorage, sessionStorage, or IndexedDB.
| Encryption Technique | Description | Use Case |
|---|---|---|
| Web Crypto API | Browser-native encryption | Secure client-side encryption |
| AES-GCM | Authenticated encryption algorithm | Strong encryption standard |
| Key derivation | Derive keys from user passwords | Password-based encryption |
| Secure key storage | Never store keys in localStorage | Memory or session only |
Example: Encrypted State Storage Hook
// encryption.ts
class StateEncryption {
private key: CryptoKey | null = null;
async generateKey(password: string): Promise<void> {
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(password);
// Derive key from password using PBKDF2
const keyMaterial = await crypto.subtle.importKey(
'raw',
passwordBuffer,
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
this.key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode('unique-app-salt'), // Use unique salt per app
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
}
async encrypt(data: any): Promise<string> {
if (!this.key) throw new Error('Key not initialized');
const encoder = new TextEncoder();
const plaintext = encoder.encode(JSON.stringify(data));
// Generate random IV
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
this.key,
plaintext
);
// Combine IV and ciphertext
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(ciphertext), iv.length);
// Convert to base64
return btoa(String.fromCharCode(...combined));
}
async decrypt(encryptedData: string): Promise<any> {
if (!this.key) throw new Error('Key not initialized');
// Decode base64
const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
// Extract IV and ciphertext
const iv = combined.slice(0, 12);
const ciphertext = combined.slice(12);
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
this.key,
ciphertext
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(plaintext));
}
clearKey(): void {
this.key = null;
}
}
export const stateEncryption = new StateEncryption();
// useEncryptedStorage.ts
import { useState, useEffect, useCallback } from 'react';
import { stateEncryption } from './encryption';
export function useEncryptedStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => Promise<void>, () => void] {
const [state, setState] = useState<T>(initialValue);
// Load encrypted data on mount
useEffect(() => {
const loadEncryptedData = async () => {
try {
const encrypted = localStorage.getItem(key);
if (encrypted) {
const decrypted = await stateEncryption.decrypt(encrypted);
setState(decrypted);
}
} catch (error) {
console.error('Failed to decrypt data:', error);
localStorage.removeItem(key);
}
};
loadEncryptedData();
}, [key]);
// Save encrypted data
const setEncryptedState = useCallback(async (value: T) => {
setState(value);
try {
const encrypted = await stateEncryption.encrypt(value);
localStorage.setItem(key, encrypted);
} catch (error) {
console.error('Failed to encrypt data:', error);
}
}, [key]);
// Clear encrypted data
const clearEncryptedState = useCallback(() => {
setState(initialValue);
localStorage.removeItem(key);
}, [key, initialValue]);
return [state, setEncryptedState, clearEncryptedState];
}
// Usage
function UserPreferences() {
const [preferences, setPreferences, clearPreferences] = useEncryptedStorage(
'user_preferences',
{ theme: 'light', notifications: true }
);
useEffect(() => {
// Initialize encryption key from user password
// (In real app, get this after user login)
stateEncryption.generateKey('user-password-or-session-token');
return () => {
// Clear key on unmount
stateEncryption.clearKey();
};
}, []);
const handleSave = async () => {
await setPreferences({
...preferences,
theme: 'dark'
});
};
return (
<div>
<p>Theme: {preferences.theme}</p>
<button onClick={handleSave}>Save Encrypted</button>
<button onClick={clearPreferences}>Clear</button>
</div>
);
}
Note: Client-side encryption provides defense-in-depth but is not a substitute for
server-side
security. The encryption key must be protected and should not be stored in localStorage.
5. Authentication State and Token Management
Securely manage authentication tokens and user session state in React applications.
| Auth Pattern | Description | Use Case |
|---|---|---|
| HTTP-only cookies | Store tokens in secure cookies | Most secure for web apps |
| Memory storage | Store tokens in state/memory only | Single-page sessions |
| Token refresh | Automatic token renewal | Long-lived sessions |
| Secure token transmission | HTTPS only, Authorization header | API calls |
| Auto logout | Clear state on inactivity/expiry | Security timeout |
Example: Secure Authentication State Management
// AuthContext.tsx
import { createContext, useContext, useState, useEffect, useRef } from 'react';
interface User {
id: string;
email: string;
role: string;
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refreshToken: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
// Store token in memory (ref), NOT localStorage
const accessTokenRef = useRef<string | null>(null);
const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);
// Login function
const login = async (email: string, password: string) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Send HTTP-only refresh token cookie
body: JSON.stringify({ email, password })
});
if (!response.ok) throw new Error('Login failed');
const { accessToken, user } = await response.json();
// Store access token in memory only
accessTokenRef.current = accessToken;
setUser(user);
setIsAuthenticated(true);
// Schedule token refresh
scheduleTokenRefresh();
} catch (error) {
console.error('Login error:', error);
throw error;
}
};
// Refresh access token using HTTP-only refresh token
const refreshToken = async () => {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include' // Send refresh token cookie
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const { accessToken } = await response.json();
accessTokenRef.current = accessToken;
scheduleTokenRefresh();
} catch (error) {
console.error('Token refresh error:', error);
await logout();
}
};
// Schedule automatic token refresh (before expiry)
const scheduleTokenRefresh = () => {
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
// Refresh 5 minutes before expiry (15 minutes if token expires in 20)
const refreshInterval = 15 * 60 * 1000; // 15 minutes
refreshTimerRef.current = setTimeout(refreshToken, refreshInterval);
};
// Logout function
const logout = async () => {
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
} catch (error) {
console.error('Logout error:', error);
} finally {
// Clear all auth state
accessTokenRef.current = null;
setUser(null);
setIsAuthenticated(false);
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
refreshTimerRef.current = null;
}
}
};
// Auto logout on inactivity
useEffect(() => {
let inactivityTimer: NodeJS.Timeout;
const resetInactivityTimer = () => {
clearTimeout(inactivityTimer);
if (isAuthenticated) {
// Logout after 30 minutes of inactivity
inactivityTimer = setTimeout(logout, 30 * 60 * 1000);
}
};
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
events.forEach(event => {
document.addEventListener(event, resetInactivityTimer);
});
resetInactivityTimer();
return () => {
events.forEach(event => {
document.removeEventListener(event, resetInactivityTimer);
});
clearTimeout(inactivityTimer);
};
}, [isAuthenticated]);
// Try to restore session on mount
useEffect(() => {
const restoreSession = async () => {
try {
// Try to refresh token using HTTP-only cookie
await refreshToken();
// Fetch user info
const response = await fetch('/api/auth/me', {
credentials: 'include'
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
setIsAuthenticated(true);
}
} catch (error) {
console.error('Session restore failed:', error);
}
};
restoreSession();
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
};
}, []);
// Create API client with automatic token injection
const apiClient = {
get: async (url: string) => {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessTokenRef.current}`
},
credentials: 'include'
});
// Handle token expiry
if (response.status === 401) {
await refreshToken();
// Retry request
return fetch(url, {
headers: {
'Authorization': `Bearer ${accessTokenRef.current}`
},
credentials: 'include'
});
}
return response;
}
};
const value = {
user,
isAuthenticated,
login,
logout,
refreshToken
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
Warning: Never store authentication tokens in localStorage or sessionStorage as they are
vulnerable to XSS attacks. Use HTTP-only cookies or in-memory storage (with automatic refresh).
6. State Audit Logging and Compliance
Implement audit logging for state changes to meet compliance requirements and track user actions.
| Audit Pattern | Description | Use Case |
|---|---|---|
| Action logging | Record all state-changing actions | Compliance, debugging |
| User tracking | Associate changes with user IDs | Accountability, forensics |
| Timestamp tracking | Record when changes occurred | Timeline reconstruction |
| Data retention | Store audit logs securely | GDPR, HIPAA compliance |
| Immutable logs | Prevent log tampering | Legal requirements |
Example: Audit Logging Middleware for Redux
// auditMiddleware.ts
import { Middleware } from '@reduxjs/toolkit';
interface AuditLog {
id: string;
userId: string | null;
action: string;
payload: any;
timestamp: string;
previousState: any;
nextState: any;
ipAddress?: string;
userAgent?: string;
}
const auditMiddleware: Middleware = (store) => (next) => async (action) => {
const previousState = store.getState();
const timestamp = new Date().toISOString();
// Execute action
const result = next(action);
const nextState = store.getState();
// Create audit log entry
const auditLog: AuditLog = {
id: crypto.randomUUID(),
userId: previousState.auth?.user?.id || null,
action: action.type,
payload: sanitizePayload(action.payload),
timestamp,
previousState: sanitizeState(previousState),
nextState: sanitizeState(nextState),
ipAddress: await getClientIP(),
userAgent: navigator.userAgent
};
// Send to audit logging service (async, non-blocking)
sendAuditLog(auditLog).catch(error => {
console.error('Failed to send audit log:', error);
});
// Store in local buffer for offline scenarios
storeLocalAuditLog(auditLog);
return result;
};
// Sanitize sensitive data before logging
function sanitizePayload(payload: any): any {
if (!payload) return payload;
const sanitized = { ...payload };
const sensitiveKeys = ['password', 'token', 'creditCard', 'ssn', 'cvv'];
Object.keys(sanitized).forEach(key => {
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
sanitized[key] = '[REDACTED]';
}
});
return sanitized;
}
function sanitizeState(state: any): any {
// Only log relevant parts of state, redact sensitive info
return {
user: state.auth?.user ? { id: state.auth.user.id } : null,
route: state.router?.location?.pathname,
// Add other non-sensitive state slices
};
}
async function sendAuditLog(log: AuditLog): Promise<void> {
await fetch('/api/audit-logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(log)
});
}
function storeLocalAuditLog(log: AuditLog): void {
const logs = JSON.parse(localStorage.getItem('audit_logs_buffer') || '[]');
logs.push(log);
// Keep only last 100 logs locally
if (logs.length > 100) {
logs.shift();
}
localStorage.setItem('audit_logs_buffer', JSON.stringify(logs));
}
async function getClientIP(): Promise<string | undefined> {
try {
const response = await fetch('https://api.ipify.org?format=json');
const data = await response.json();
return data.ip;
} catch {
return undefined;
}
}
export default auditMiddleware;
Example: GDPR-Compliant State Audit Hook
// useAuditedState.ts
import { useState, useCallback, useEffect } from 'react';
interface AuditEntry<T> {
timestamp: string;
action: 'SET' | 'RESET' | 'DELETE';
previousValue: T | null;
newValue: T | null;
reason?: string;
}
interface AuditedStateOptions {
userId?: string;
component: string;
sendToServer?: boolean;
retentionDays?: number; // GDPR data retention
}
export function useAuditedState<T>(
initialValue: T,
options: AuditedStateOptions
) {
const [state, setState] = useState<T>(initialValue);
const [auditTrail, setAuditTrail] = useState<AuditEntry<T>[]>([]);
const createAuditEntry = useCallback((
action: AuditEntry<T>['action'],
previousValue: T | null,
newValue: T | null,
reason?: string
): AuditEntry<T> => {
return {
timestamp: new Date().toISOString(),
action,
previousValue,
newValue,
reason
};
}, []);
const setAuditedState = useCallback((
newValue: T | ((prev: T) => T),
reason?: string
) => {
setState(prev => {
const resolvedValue = typeof newValue === 'function'
? (newValue as Function)(prev)
: newValue;
const entry = createAuditEntry('SET', prev, resolvedValue, reason);
setAuditTrail(trail => [...trail, entry]);
// Send to server if configured
if (options.sendToServer) {
sendAuditToServer({
userId: options.userId,
component: options.component,
entry
});
}
return resolvedValue;
});
}, [createAuditEntry, options]);
const resetState = useCallback((reason?: string) => {
setState(prev => {
const entry = createAuditEntry('RESET', prev, initialValue, reason);
setAuditTrail(trail => [...trail, entry]);
if (options.sendToServer) {
sendAuditToServer({
userId: options.userId,
component: options.component,
entry
});
}
return initialValue;
});
}, [createAuditEntry, initialValue, options]);
const deleteState = useCallback((reason?: string) => {
setState(prev => {
const entry = createAuditEntry('DELETE', prev, null, reason);
setAuditTrail(trail => [...trail, entry]);
if (options.sendToServer) {
sendAuditToServer({
userId: options.userId,
component: options.component,
entry
});
}
return initialValue;
});
}, [createAuditEntry, initialValue, options]);
// GDPR compliance: Auto-delete old audit entries
useEffect(() => {
if (!options.retentionDays) return;
const retentionMs = options.retentionDays * 24 * 60 * 60 * 1000;
const cutoffDate = new Date(Date.now() - retentionMs);
setAuditTrail(trail =>
trail.filter(entry => new Date(entry.timestamp) > cutoffDate)
);
}, [options.retentionDays]);
// Export audit trail (for GDPR data export requests)
const exportAuditTrail = useCallback(() => {
return {
component: options.component,
userId: options.userId,
entries: auditTrail,
exportedAt: new Date().toISOString()
};
}, [auditTrail, options]);
return {
state,
setState: setAuditedState,
resetState,
deleteState,
auditTrail,
exportAuditTrail
};
}
async function sendAuditToServer(data: any): Promise<void> {
try {
await fetch('/api/audit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} catch (error) {
console.error('Failed to send audit log:', error);
}
}
// Usage
function UserProfile() {
const {
state: profile,
setState: setProfile,
auditTrail,
exportAuditTrail
} = useAuditedState(
{ name: '', email: '' },
{
userId: 'user123',
component: 'UserProfile',
sendToServer: true,
retentionDays: 90 // GDPR: Keep logs for 90 days
}
);
const handleUpdate = () => {
setProfile(
{ name: 'John Doe', email: 'john@example.com' },
'User updated profile information'
);
};
const handleExport = () => {
const auditData = exportAuditTrail();
console.log('Audit trail:', auditData);
// Download or send to user for GDPR data export request
};
return (
<div>
<input
value={profile.name}
onChange={(e) => setProfile({ ...profile, name: e.target.value })}
/>
<button onClick={handleUpdate}>Update</button>
<button onClick={handleExport}>Export Audit Trail</button>
<div>
<h3>Audit Trail ({auditTrail.length} entries)</h3>
{auditTrail.map((entry, i) => (
<div key={i}>
{entry.timestamp}: {entry.action} - {entry.reason}
</div>
))}
</div>
</div>
);
}
Note: Audit logs must comply with data protection regulations (GDPR, HIPAA, SOC 2). Implement
proper retention policies, encrypt logs at rest, and provide data export/deletion capabilities.
Section 19 Key Takeaways
- Sensitive data - Don't store passwords/credit cards in state, use refs for immediate transmission, clear on unmount
- Validation - Validate all user input before storing, use Zod/Yup for schema validation, sanitize HTML content
- XSS prevention - React auto-escapes text, sanitize before dangerouslySetInnerHTML, validate URLs, use CSP headers
- Encryption - Use Web Crypto API for client-side encryption, never store keys in localStorage, AES-GCM for authenticated encryption
- Auth tokens - Use HTTP-only cookies or memory storage, implement automatic token refresh, auto-logout on inactivity
- Audit logging - Log state changes with user ID and timestamp, sanitize sensitive data, comply with GDPR retention policies