DEV Community

Cover image for React Component Breaks UI Re-Render - Why Math.random Violates Purity Rules
AiVIS Cite Ledger
AiVIS Cite Ledger

Posted on

React Component Breaks UI Re-Render - Why Math.random Violates Purity Rules

The bug nobody notices until production

→ Component re-renders → new random → broken key/ID/animation

React throws hydration errors and unstable UI updates when you call Math.random directly in component bodies because it returns different values on every render cycle breaking the idempotency requirement that components must always produce identical output for identical inputs. The fix is wrapping randomness in useState with lazy initialization or switching to crypto.randomUUID for stable unique identifiers that only compute once during component mount and persist across re-renders without causing server client mismatches in SSR environments.

Why This Breaks Your App Before You Even Notice

Every time React re-renders your component the Math.random call executes again producing a completely different value. This violates the core purity rule documented at react.dev/reference/rules/components-and-hooks-must-be-pure where components must be idempotent meaning identical inputs produce identical outputs. When you deploy to production with server-side rendering the server generates one random value during HTML generation but the client generates a different random value during hydration causing the infamous hydration mismatch error that crashes your app.

React Strict Mode in development intentionally double renders components to expose these purity violations. If you see different random values printed twice in console logs during development that signals your component will fail unpredictably in production when Concurrent Mode or Suspense triggers unexpected re-renders.

The technical reason goes deeper into how React reconciliation works. React expects that given the same props state and context a component returns the same JSX tree. When you inject non-deterministic functions like Math.random or Date.now into the render path React cannot reliably determine what changed between renders leading to unnecessary DOM updates memory leaks and visual flickers.

How useState and useMemo Actually Solve This

Both hooks freeze the random value at mount time preventing it from changing on subsequent re-renders. The difference is when and how they compute the initial value.

useState with lazy initialization calls the function exactly once when the component mounts then stores that result in React internal state. Every re-render returns the same stored value without re-executing Math.random. The lazy initializer syntax useState(() => Math.random()) is critical because passing Math.random() directly would call it during every render before useState even receives the value.

const [id] = useState(() => Math.random());
Enter fullscreen mode Exit fullscreen mode

This pattern works because the arrow function acts as a factory that React invokes only during the initial mount phase. Subsequent re-renders skip the factory and return the cached state value.

useMemo with an empty dependency array also runs the computation once and caches the result but the timing differs slightly. useMemo executes during the render phase while useState lazy initializers run before render making useState slightly more predictable for initialization logic.

const id = useMemo(() => Math.random(), []);
Enter fullscreen mode Exit fullscreen mode

Both approaches satisfy React purity requirements by ensuring the component returns the same output for the same inputs across all renders.

Top 10 Critical Issues Using Math.random in React

1. SSR Hydration Mismatch

Server renders one random value client renders different value causing React to discard server HTML and re-render everything from scratch. This destroys performance benefits of SSR and creates visible content flashes.

2. Strict Mode Double Execution

Development mode intentionally calls your component twice to detect impure functions. Math.random returns different values each time making your component fail the purity test and exposing instability before production.

3. Concurrent Mode Interruptions

React 18 Concurrent Mode can pause and restart renders mid-execution. Each restart re-runs Math.random producing inconsistent component state that causes visual bugs and broken UI interactions.

4. Unstable Keys in Lists

Using Math.random for React key props destroys reconciliation. React cannot track which items changed because keys differ on every render forcing complete list re-renders and losing focus state in inputs.

5. Inconsistent useEffect Dependencies

If you pass a Math.random value into useEffect dependency array the effect re-runs on every render because the dependency always changes. This creates infinite loops and performance degradation.

6. Race Conditions in Async Code

Components that use Math.random for request IDs or cache keys fail when re-renders happen during async operations. The ID changes mid-request causing responses to be ignored or applied to wrong components.

7. Test Instability

Unit tests fail randomly because Math.random produces different outputs each run. Tests become flaky and unreliable forcing developers to add arbitrary sleeps or retry logic that masks real bugs.

8. State Initialization Bugs

Passing Math.random() directly to useState without lazy initialization calls it every render before useState even receives the value. The state appears stable but the random call still executes causing side effects and performance hits.

9. Snapshot Testing Failures

Jest snapshot tests always fail because rendered output includes random values that change every test run. Developers waste time updating snapshots or disabling tests instead of catching real regressions.

10. Browser Extension Conflicts

Some browser extensions inject scripts that trigger extra re-renders. Components using Math.random produce different values during these unintended re-renders creating visual inconsistencies that only appear for users with specific extensions installed.

Crypto.getRandomValues vs Math.random Security and Performance

Math.random uses a pseudo-random number generator with a fixed algorithm and internal seed state making the sequence theoretically predictable. This implementation is deterministic meaning if an attacker discovers the seed they can reproduce the entire sequence of random numbers.

Crypto.getRandomValues uses the operating system entropy pool pulling randomness from hardware events like mouse movements disk timings and thermal noise. This makes it cryptographically secure because no algorithm can predict the next value even with knowledge of all previous values.

const array = new Uint32Array(1);
crypto.getRandomValues(array);
const randomNumber = array[0];
Enter fullscreen mode Exit fullscreen mode

The performance difference is negligible for UI use cases. Crypto operations add microseconds of overhead which becomes irrelevant compared to React render times and DOM updates. For generating component IDs authentication tokens or any security-sensitive values always use crypto over Math.random.

Math.random returns a single float between 0 and 1 requiring manual scaling and rounding to get integers. Crypto.getRandomValues fills typed arrays with integers directly avoiding floating point precision issues.

// Math.random requires manual conversion
const id = Math.floor(Math.random() * 1000000);

// crypto.randomUUID gives clean strings
const id = crypto.randomUUID();
Enter fullscreen mode Exit fullscreen mode

Better Alternatives Ranked by Use Case

React useId for Accessibility

React 18 introduced useId specifically for generating stable unique IDs that work in SSR. This hook produces deterministic IDs that match between server and client eliminating hydration mismatches completely.

function FormField() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Name</label>
      <input id={id} type="text" />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Use this for linking labels to inputs ARIA attributes and any DOM ID requirements. It requires zero configuration and never causes hydration errors.

crypto.randomUUID for Unique Identifiers

Native browser API available in all modern environments returns RFC 4122 compliant UUIDs as strings. No dependencies required and works in both browser and Node.js environments.

const [sessionId] = useState(() => crypto.randomUUID());
Enter fullscreen mode Exit fullscreen mode

Perfect for session IDs tracking tokens and user-facing identifiers where format consistency matters.

nanoid for Custom ID Requirements

NPM package that generates URL-safe unique strings with configurable length and alphabet. Smaller output than UUID and 60% faster.

import { nanoid } from 'nanoid';
const id = nanoid(); // "V1StGXR8_Z5jdHi6B-myT"
const shortId = nanoid(10); // "IRFa-VaY2b"
Enter fullscreen mode Exit fullscreen mode

Install with npm i nanoid and use when you need compact IDs for URLs database keys or high-volume ID generation.

pure-rand for Reproducible Testing

Seedable random number generator that produces identical sequences from identical seeds enabling deterministic tests. Critical for snapshot testing game simulations and debugging random failures.

import { pureRand } from 'pure-rand';
const rng = pureRand(12345); // fixed seed
const [value, nextRng] = rng.next();
Enter fullscreen mode Exit fullscreen mode

Use in test suites where you need reproducible randomness to verify algorithm correctness. Production code should never use seeded randomness for security-sensitive operations.

Module-Level Generation for Static Values

Define random values outside component scope if they never need to change across component instances.

const STATIC_ID = crypto.randomUUID();

function Component() {
  return <div data-session={STATIC_ID} />;
}
Enter fullscreen mode Exit fullscreen mode

Only works when all component instances share the same value. Not suitable for per-instance uniqueness.

Decision Matrix for Choosing the Right Approach

  • Need stable accessible DOM IDs for forms use React useId hook.
  • Need unique per-component instance IDs use useState(() => crypto.randomUUID()).
  • Need security tokens auth keys or password resets use crypto.getRandomValues or crypto.randomUUID.
  • Need reproducible randomness for tests or simulations use pure-rand with fixed seeds.
  • Need high-performance compact IDs for URLs or databases use nanoid.
  1. Never use bare Math.random in component render paths.
  2. Never pass Math.random() directly to useState without lazy initialization.
  3. Never use Math.random for security-sensitive values like tokens or session IDs.

Reference Sources

Key Takeaways

  • Math.random in React component bodies violates purity rules causing hydration mismatches and unstable re-renders
  • useState with lazy initialization useState(() => Math.random()) freezes random values at mount time
  • crypto.randomUUID provides cryptographically secure UUIDs without external dependencies
  • React useId hook solves accessibility ID requirements with zero hydration risk
  • nanoid offers compact URL-safe IDs 60% faster than UUID for high-volume generation
  • pure-rand enables reproducible testing with seeded random sequences
  • Never use Math.random for security tokens authentication or any cryptographic purpose
  • SSR environments require deterministic ID generation to prevent server-client mismatches
  • Strict Mode double rendering exposes impure function calls during development
  • Concurrent Mode can restart renders causing Math.random to produce inconsistent values

Top comments (0)