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 &lt;script&gt; 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