m()` is fundamentally insecure for any value that requires unpredictability, such as tokens or session identifiers.
Core Solution
To resolve non-determinism, you must decouple value generation from the render cycle. The goal is to compute the value exactly once per instance and persist it across re-renders, while ensuring server and client outputs align.
1. Freezing Instance-Specific Values with Lazy State
When a component requires a unique identifier that persists for its lifetime (e.g., a trace ID for analytics or a session token), use useState with a lazy initializer. The initializer function executes only during the initial mount, caching the result.
Implementation Pattern:
import { useState } from 'react';
interface DataGridRowProps {
recordId: string;
content: string;
}
export function DataGridRow({ recordId, content }: DataGridRowProps) {
// Lazy initializer ensures crypto.randomUUID runs once on mount.
// Subsequent renders return the cached value without re-execution.
const [auditToken] = useState(() => {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
// Fallback for environments lacking crypto.randomUUID
return `audit-${Math.random().toString(36).substring(2, 10)}`;
});
return (
<tr data-audit={auditToken}>
<td>{recordId}</td>
<td>{content}</td>
</tr>
);
}
Rationale: Passing crypto.randomUUID() directly to useState would invoke the function during every render before the hook processes the argument. The arrow function wrapper defers execution until React initializes the state, guaranteeing idempotency. The fallback handles legacy environments but should be audited for SSR consistency.
2. SSR-Safe DOM Identifiers with useId
For accessibility attributes, form labels, and DOM IDs, React 18 provides useId. This hook generates deterministic identifiers that match between server and client, eliminating hydration mismatches entirely.
Implementation Pattern:
import { useId } from 'react';
export function SecureFormSection() {
const inputId = useId();
const checkboxId = useId();
return (
<fieldset>
<label htmlFor={inputId}>API Key</label>
<input id={inputId} type="password" autoComplete="off" />
<label htmlFor={checkboxId}>
<input id={checkboxId} type="checkbox" />
Enable encryption
</label>
</fieldset>
);
}
Rationale: useId uses an internal counter mechanism that produces stable IDs like :r0: or :r1:. These IDs are deterministic based on the component tree structure, ensuring the server HTML and client hydration produce identical attributes. This is the only safe approach for DOM-bound IDs in SSR applications.
3. Module-Level Constants for Shared Values
If a value must be identical across all instances of a component (e.g., a build ID or global configuration token), define it at the module scope. This avoids per-instance computation and ensures consistency.
Implementation Pattern:
// config.ts
export const BUILD_FINGERPRINT = typeof crypto !== 'undefined' && crypto.randomUUID
? crypto.randomUUID()
: `build-${Date.now()}`;
// Widget.tsx
import { BUILD_FINGERPRINT } from './config';
export function SystemWidget() {
return <div data-build={BUILD_FINGERPRINT}>System Status</div>;
}
Rationale: Module-level execution happens once when the module loads. This is efficient for static values but unsuitable for per-instance uniqueness.
Pitfall Guide
1. The Eager Initialization Trap
Explanation: Passing Math.random() directly to useState executes the function on every render.
Fix: Always wrap the generator in an arrow function: useState(() => Math.random()).
2. The Key Prop Fallacy
Explanation: Using random values for key props in lists prevents React from reconciling items efficiently. Keys must be stable across renders.
Fix: Use stable identifiers from your data source. If generating temporary items, assign a stable ID at creation time, not during render.
3. SSR/Client Divergence
Explanation: Math.random() produces different values on the server and client, causing hydration errors.
Fix: Use React.useId() for DOM IDs or ensure crypto.randomUUID() is available and consistent in both environments. Avoid Math.random() in SSR paths.
4. The Effect Dependency Loop
Explanation: Including a random value in a useEffect dependency array causes the effect to run on every render.
Fix: Memoize the value or use a ref if the value must persist without triggering effects. Ensure dependencies are stable.
5. Security Entropy Weakness
Explanation: Math.random() uses a predictable pseudo-random algorithm. It is unsuitable for tokens, passwords, or session IDs.
Fix: Use crypto.getRandomValues() or crypto.randomUUID() for any security-sensitive generation.
6. Strict Mode Blindness
Explanation: Developers may ignore Strict Mode warnings, assuming they are development-only.
Fix: Treat Strict Mode double-renders as production simulations. If a component fails in Strict Mode, it will fail in Concurrent Mode.
7. Test Flakiness
Explanation: Tests involving Math.random() produce non-deterministic results, causing intermittent failures.
Fix: Mock Math.random in tests or use seedable random libraries like pure-rand for reproducible test suites.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Form Labels / ARIA IDs | React.useId() | SSR-safe, deterministic, zero config. | Negligible |
| Session / Trace IDs | useState(() => crypto.randomUUID()) | Secure, stable per mount, unique. | Microseconds |
| List Item Keys | Stable Data ID | Enables efficient reconciliation. | None |
| Security Tokens | crypto.getRandomValues() | Cryptographic entropy required. | Microseconds |
| Test Reproducibility | pure-rand (Seeded) | Deterministic output for assertions. | Low |
| Global Config IDs | Module Constant | Shared across instances, efficient. | None |
Configuration Template
A reusable hook for generating stable, secure identifiers with SSR awareness.
import { useState } from 'react';
/**
* Generates a stable, secure identifier that persists across re-renders.
* Uses crypto.randomUUID when available, with a fallback for legacy environments.
*
* @param prefix - Optional prefix for the generated ID.
* @returns A stable string identifier.
*/
export function useSecureId(prefix: string = 'id'): string {
const [stableId] = useState(() => {
// Check for crypto API availability
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return `${prefix}-${crypto.randomUUID()}`;
}
// Fallback: Note that this may cause SSR mismatches if not handled carefully.
// In SSR apps, prefer useId() or polyfills for crypto.
const fallback = Math.random().toString(36).substring(2, 10);
return `${prefix}-${fallback}`;
});
return stableId;
}
Quick Start Guide
- Install Linting Rules: Add
eslint-plugin-react-hooks to enforce purity rules and catch direct Math.random usage in renders.
- Replace DOM IDs: Convert all
Math.random-based DOM IDs to React.useId().
- Freeze Instance IDs: Wrap
crypto.randomUUID() in useState lazy initializers for per-component identifiers.
- Audit Security: Replace
Math.random in auth flows with crypto.getRandomValues().
- Verify SSR: Run the application with SSR enabled and check for hydration warnings. Ensure all IDs match between server and client.