Skip to solution
hardFrontend

How can the `key` prop be used to deliberately reset component state?

877 views
01

Understand the problem

Question presented to candidate: "A profile form keeps the previous user's unsaved edits when you switch records. How would you fix it without an effect?"

What a strong answer should cover:

  • key is component identity. Same key means React reuses the instance and its state; a different key means a different component, so React unmounts the old one and mounts a fresh one.
  • So <ProfileForm key={userId} /> resets everything inside when userId changes — no effect, no manual clearing, no risk of missing a field.
  • The alternative — an effect that watches the prop and calls setters — is worse: it renders once with stale state before the effect runs, it has to enumerate every piece of state, and it grows a bug every time someone adds a field.
  • This is the same mechanism as index keys corrupting a list, used deliberately: React matches children by key, and a changed key is a different child.
  • Scope it correctly: the key resets everything below it, including uncontrolled DOM state, refs and child component state. That is usually what you want and occasionally too much.
  • The key should be stable per identity — a record id, not Math.random(), which would remount on every render.
  • Reach for it when all state below should be discarded. Prefer a derived value or lifting state up when only part of it should change.

Clarifying questions expected:

  • "Should absolutely everything below reset, or just one field?" — the key is all-or-nothing.
  • "Is there an id that identifies the record?"

Code / implementation expected: Optional. It is a one-line change; the value is explaining why the effect version is worse.

reactreconciliationstate
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes reconciliation. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Switching records three ways: no reset, an effect, and a key
Run Playground
import { useState, useEffect } from "react";

const USERS = [
  { id: 1, name: "Ada" },
  { id: 2, name: "Grace" },
  { id: 3, name: "Alan" },
];

// The form under test. Draft is local state that must NOT survive a switch.
function ProfileForm({ user, resetWithEffect, onLifecycle }) {
  const [draft, setDraft] = useState("");
  const [touched, setTouched] = useState(false);

  useEffect(() => {
    onLifecycle("mount " + user.name);
    return () => onLifecycle("unmount " + user.name);
  }, []);

  // ❌ The effect approach. Runs AFTER the commit, so there is one render
  //    showing the new user with the previous user's draft. And every new
  //    piece of state has to be added here by hand, forever.
  useEffect(() => {
    if (!resetWithEffect) return;
    setDraft("");
    setTouched(false);
  }, [user.id, resetWithEffect]);

  return (
    <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12 }}>
      <div style={{ fontSize: 13 }}>
        editing <strong>{user.name}</strong>
        {touched && <span style={{ color: "#a33" }}> · unsaved</span>}
      </div>
      <input
        value={draft}
        onChange={(e) => { setDraft(e.target.value); setTouched(true); }}
        placeholder="type a note, then switch user"
        style={{ width: "100%", marginTop: 6 }}
      />
    </div>
  );
}

export default function App() {
  const [id, setId] = useState(1);
  const [mode, setMode] = useState("none");
  const [events, setEvents] = useState([]);
  const user = USERS.find((u) => u.id === id);
  const log = (s) => setEvents((l) => [...l, s].slice(-6));

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 520 }}>
      <div style={{ fontSize: 13, marginBottom: 6 }}>reset strategy:</div>
      <div style={{ display: "flex", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
        {[["none", "❌ none"], ["effect", "⚠️ effect"], ["key", "✅ key"]].map(([m, label]) => (
          <button key={m} onClick={() => { setMode(m); setEvents([]); }}
            style={{ fontWeight: mode === m ? "bold" : "normal" }}>
            {label}
          </button>
        ))}
      </div>

      <div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
        {USERS.map((u) => (
          <button key={u.id} onClick={() => setId(u.id)}
            style={{ fontWeight: u.id === id ? "bold" : "normal" }}>
            {u.name}
          </button>
        ))}
      </div>

      {/* The ONLY difference between the working version and the broken one. */}
      <ProfileForm
        key={mode === "key" ? user.id : "static"}
        user={user}
        resetWithEffect={mode === "effect"}
        onLifecycle={log}
      />

      <pre style={{ background: "#f6f6f8", padding: 10, borderRadius: 8, fontSize: 12, marginTop: 12, minHeight: 70 }}>
{events.length ? events.join("\n") : "lifecycle events appear here"}
      </pre>

      <p style={{ fontSize: 13, color: "#666" }}>
        Type a note, then switch user. With <strong>none</strong> the draft
        follows you to the next record. With <strong>effect</strong> it clears —
        but watch the lifecycle log: no unmount happened, so anything you forgot
        to reset would persist. With <strong>key</strong> you get a real unmount
        and mount, and everything below starts fresh with nothing enumerated.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 84 of 119 decoded in the React.js track. One more won't hurt.

Back to track