DEV_NET_CORE
GET_STARTED
ReactTesting, accessibility, and frontend debugging

Debugging rendering, hydration, and interaction issues

Overview

Debugging rendering, hydration, and interaction issues in React means understanding how React calculates UI, attaches event handlers, preserves state, runs effects, and coordinates server-rendered HTML with client-side React. These bugs often look mysterious at first: a component renders twice, an event handler sees stale state, a button does not click, a form submits unexpectedly, server-rendered markup changes after hydration, or a component loses state after a list update.

The practical debugging skill is to separate the problem into layers. Rendering issues usually involve props, state, context, keys, memoization, or effects. Hydration issues involve mismatches between server output and the first client render. Interaction issues involve event handlers, native HTML behavior, disabled elements, overlays, focus, propagation, async state, or stale closures.

This topic matters in interviews because React debugging reveals whether a candidate has a real mental model of React. Strong answers do not just say "use DevTools." They explain render vs commit, Strict Mode behavior, hydration parity, effect cleanup, state snapshots, event propagation, and a systematic way to reproduce and isolate the bug.

In production teams, these skills are used when debugging slow screens, SSR warnings, broken forms, inaccessible controls, disappearing state, flaky interactions, and issues that only happen after deployment.

Core Concepts

Render, Commit, and Paint

React updates UI in stages:

  • Trigger: initial render, state update, prop change, context change, or external store update.
  • Render: React calls components to calculate the next UI.
  • Commit: React applies necessary DOM changes.
  • Paint: the browser displays the result.

A component function running does not always mean the DOM changed. React may rerender a subtree, compare the result, and commit only the minimal necessary DOM updates.

Debugging implication:

  • If JSX is wrong, inspect render inputs: props, state, context, derived values.
  • If the DOM is wrong, inspect commit-related conditions: keys, conditional rendering, portals, hydration, or browser DOM mutations outside React.
  • If the screen feels slow, inspect expensive render work and commit cost separately.

Common Render Triggers

A component can render because:

  • Its own state changed.
  • Its parent rendered.
  • A context value it reads changed.
  • A subscribed external store changed.
  • Its key changed and React treated it as a new component.
  • It mounted for the first time.

Example debugging helper:

Code
function ProductRow({ product }: { product: Product }) {
  console.log("ProductRow render", product.id, product.name);

  return <li>{product.name}</li>;
}

Logs can help during exploration, but use React DevTools Profiler for serious performance debugging.

Strict Mode Development Behavior

In development, StrictMode intentionally checks for common bugs:

  • Components may render an extra time to detect impure rendering.
  • Effects may run setup, cleanup, and setup again to detect missing cleanup.
  • Ref callbacks may also be checked.

This can surprise developers who see duplicate logs or duplicate development network requests.

The fix is usually not to hide Strict Mode. The fix is to make render logic pure and make effects resilient to setup, cleanup, and setup.

Bad:

Code
useEffect(() => {
  socket.connect();
}, []);

Better:

Code
useEffect(() => {
  socket.connect();

  return () => {
    socket.disconnect();
  };
}, []);

If the effect cannot safely run twice in development, it probably has a missing cleanup or belongs in an event handler instead.

State as a Snapshot

State values inside a render are snapshots. Event handlers close over the values from the render that created them.

Code
function Counter() {
  const [count, setCount] = useState(0);

  function addThree() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  return <button onClick={addThree}>{count}</button>;
}

All three updates read the same count snapshot. Use updater functions when the next value depends on the previous value:

Code
function addThree() {
  setCount((current) => current + 1);
  setCount((current) => current + 1);
  setCount((current) => current + 1);
}

This mental model also explains many stale closure bugs in timers, event listeners, and async callbacks.

Keys and State Preservation

React associates state with a component's position in the render tree. Keys help React match list items across renders.

Bad:

Code
{todos.map((todo, index) => (
  <TodoRow key={index} todo={todo} />
))}

If items are inserted, removed, or reordered, index keys can cause state to move to the wrong row.

Better:

Code
{todos.map((todo) => (
  <TodoRow key={todo.id} todo={todo} />
))}

Debugging signs of key problems:

  • Input text appears under the wrong row.
  • Expanded state moves to another item.
  • Animations or focus jump after sorting.
  • State resets unexpectedly when conditionals change.

Use stable keys from data and keep component types stable.

Effects and Dependency Bugs

Effects synchronize React with external systems: subscriptions, browser APIs, timers, sockets, analytics, or imperative widgets. Dependency arrays should match the reactive values used by the effect.

Bad:

Code
useEffect(() => {
  fetchResults(query);
}, []);

If query changes, the effect still uses the initial query.

Better:

Code
useEffect(() => {
  fetchResults(query);
}, [query]);

For async requests, handle stale responses:

Code
useEffect(() => {
  let ignore = false;

  async function load() {
    const results = await searchProducts(query);

    if (!ignore) {
      setResults(results);
    }
  }

  load();

  return () => {
    ignore = true;
  };
}, [query]);

If adding a dependency creates a loop, do not suppress the linter by default. Change the code shape: move objects inside the effect, use updater functions, split unrelated effects, or move user-triggered logic into event handlers.

Infinite Render Loops

Common causes of render loops:

  • Calling a state setter during render.
  • An effect updates state and depends on a value recreated every render.
  • A parent passes a new object or function that causes a child effect to run repeatedly.
  • Derived state is recalculated in an effect instead of during render.

Bad:

Code
function ProductList({ products }: { products: Product[] }) {
  const [visibleProducts, setVisibleProducts] = useState<Product[]>([]);

  useEffect(() => {
    setVisibleProducts(products.filter((product) => product.active));
  }, [products]);

  return <List items={visibleProducts} />;
}

If the value is derived from props or state and does not require an external system, calculate it during render:

Code
function ProductList({ products }: { products: Product[] }) {
  const visibleProducts = products.filter((product) => product.active);

  return <List items={visibleProducts} />;
}

Use useMemo only if the calculation is expensive enough to matter.

Hydration

Hydration is the process where React attaches event handlers and component logic to HTML that was already generated on the server. The first client render must produce the same output as the server render.

Code
import { hydrateRoot } from "react-dom/client";

hydrateRoot(document.getElementById("root")!, <App />);

If the server HTML and the first client render differ, React may warn, recover slowly, or attach handlers incorrectly in severe cases.

Common Hydration Mismatch Causes

Common causes include:

  • Rendering Date.now(), new Date(), or random values during render.
  • Using Math.random() for IDs or text.
  • Reading window, document, localStorage, matchMedia, or browser-only APIs during render.
  • Using typeof window !== "undefined" to render different markup.
  • Rendering user-specific data differently on server and client.
  • Locale, timezone, or formatting differences.
  • Invalid HTML that the browser repairs differently than expected.
  • CSS-in-JS or style ordering mismatches.
  • IDs generated differently between server and client.
  • Browser extensions modifying the DOM before hydration.

Bad:

Code
function TimeStamp() {
  return <p>{new Date().toLocaleString()}</p>;
}

Better if the timestamp must be client-only:

Code
function TimeStamp() {
  const [time, setTime] = useState<string | null>(null);

  useEffect(() => {
    setTime(new Date().toLocaleString());
  }, []);

  return <p>{time ?? "Loading time..."}</p>;
}

The server and first client render both show the fallback. The client updates after hydration.

Fixing Hydration Mismatches

Good fixes include:

  • Make initial render deterministic.
  • Pass the same data to server and client.
  • Move browser-only reads into useEffect.
  • Use useId for accessibility IDs instead of random ID generation.
  • Use framework-supported data loading and serialization.
  • Validate HTML nesting.
  • Ensure CSS-in-JS setup follows the framework's SSR guidance.
  • Use suppressHydrationWarning only for narrow, unavoidable one-element mismatches.

Escape hatch:

Code
<time suppressHydrationWarning>
  {new Date().toLocaleString()}
</time>

This should be rare. It silences a warning; it does not make mismatched UI a good experience.

Debugging Interaction Issues

When a click, key, submit, or focus behavior does not work, check the browser layer before blaming React.

Questions to ask:

  • Is the element a real interactive element?
  • Is it disabled?
  • Is another element overlaying it?
  • Is CSS using pointer-events: none?
  • Is the handler passed correctly?
  • Is the handler accidentally called during render?
  • Is the element inside a form where a button defaults to submit?
  • Is preventDefault or stopPropagation blocking expected behavior?
  • Is focus going somewhere unexpected?
  • Is stale state used inside an async callback?

Bad:

Code
<button onClick={save()}>Save</button>

This calls save during render.

Good:

Code
<button onClick={save}>Save</button>

or:

Code
<button onClick={() => save(productId)}>Save</button>

Form Interaction Issues

Common form bugs:

  • A button submits a form because it defaults to type="submit".
  • Submit logic is attached only to button onClick, so pressing Enter does not work.
  • A controlled input switches between controlled and uncontrolled.
  • Validation errors are stored but not rendered accessibly.
  • The form relies on placeholder text instead of labels.
  • event.preventDefault() is missing or used in the wrong place.

Better pattern:

Code
function ProfileForm({ onSave }: { onSave: (name: string) => void }) {
  const [name, setName] = useState("");

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    onSave(name);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Name
        <input value={name} onChange={(event) => setName(event.target.value)} />
      </label>
      <button type="submit">Save profile</button>
      <button type="button">Cancel</button>
    </form>
  );
}

Use form semantics so clicking submit and pressing Enter go through the same path.

Event Propagation

React event handlers use a delegated event system and follow normal propagation concepts. A child click can bubble to a parent.

Example:

Code
function Card() {
  return (
    <article onClick={() => openDetails()}>
      <h2>Invoice 123</h2>
      <button
        type="button"
        onClick={(event) => {
          event.stopPropagation();
          downloadInvoice();
        }}
      >
        Download
      </button>
    </article>
  );
}

Use stopPropagation only when a nested interaction should not trigger the parent interaction. If you need it everywhere, the component design may be too broad.

Focus Issues

Focus bugs often appear after conditional rendering, dialogs, route changes, validation errors, or removed elements.

Debugging steps:

  • Check document.activeElement.
  • Confirm the focused element still exists after state changes.
  • Ensure modals move focus inside and restore it after close.
  • Ensure validation errors do not move focus unexpectedly.
  • Avoid remounting focused inputs with unstable keys.
  • Check whether an overlay or disabled state prevents focus.

Example focus restoration:

Code
function DeleteButton() {
  const buttonRef = useRef<HTMLButtonElement>(null);
  const [open, setOpen] = useState(false);

  function closeDialog() {
    setOpen(false);
    buttonRef.current?.focus();
  }

  return (
    <>
      <button ref={buttonRef} type="button" onClick={() => setOpen(true)}>
        Delete
      </button>
      <DeleteDialog open={open} onClose={closeDialog} />
    </>
  );
}

React DevTools

React Developer Tools helps inspect:

  • Component tree.
  • Props and state.
  • Context values.
  • Hook state.
  • Owner relationships.
  • Render performance through the Profiler.

Use Components view to inspect the current state and props. Use Profiler when the problem is slowness, excessive rerendering, or expensive commits. Combine it with browser DevTools for DOM, network, console, performance, accessibility tree, and event listener debugging.

Debugging Workflow

A reliable workflow:

  • Reproduce the bug with exact steps.
  • Check console warnings first.
  • Identify whether the symptom is rendering, hydration, interaction, data, or styling.
  • Reduce the example to the smallest component or state path.
  • Inspect props, state, context, and keys.
  • Check whether Strict Mode development behavior is involved.
  • Inspect effects and dependencies.
  • For SSR, compare server output and first client render assumptions.
  • For interactions, verify native element behavior, focus, event propagation, and CSS overlays.
  • Add a regression test once the cause is understood.

Common Mistakes

Common mistakes include:

  • Treating Strict Mode duplicate logs as a production bug.
  • Mutating props or state during render.
  • Using index keys for reorderable lists.
  • Suppressing effect dependency warnings.
  • Calculating browser-only values during SSR render.
  • Using createRoot for server-rendered HTML instead of hydrateRoot.
  • Relying on random IDs during hydration.
  • Calling handlers during render.
  • Forgetting type="button" inside forms.
  • Hiding focus outlines and then missing focus bugs.
  • Debugging React while ignoring CSS overlays or native browser behavior.

Best Practices

Best practices include:

  • Keep render logic pure.
  • Use stable keys from data.
  • Treat effects as synchronization with external systems.
  • Implement cleanup for subscriptions, timers, sockets, and imperative widgets.
  • Keep server and first client render deterministic.
  • Move browser-only work into effects or client-only boundaries.
  • Use semantic HTML for interactions.
  • Prefer form onSubmit over button-only submit logic.
  • Profile slow interactions in production-like builds.
  • Add regression tests for rendering, hydration, and interaction bugs.

Interview Practice

PrevioususeSyncExternalStore and subscribing to external stateNext UpKeyboard accessibility, semantic HTML, ARIA, and labels/IDs