Skip to the solution
HardFrontend7 min read

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

877 views

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.

  • react
  • reconciliation
  • state

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactSwitching records three ways: no reset, an effect, and a key
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 84 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

How do Suspense data-fetching waterfalls happen, and how do you avoid them?

Continue