Explain what typically breaks when a component treats the same piece of data as both “props-owned” and “state-owned” (multiple sources of truth). Cover common symptoms (stale UI, input jumps, infinite loops, memo bugs) and the correct patterns (single source of truth, controlled vs uncontrolled, derived state). Confusing state vs props leads to bugs and re-render issues. Test with prop changes and derived state updates.
What bugs appear when state and props responsibilities are mixed?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
Props are inputs owned by the parent. State is local memory owned by the component. When the same value is “owned” by both (prop + local state trying to represent the same truth), you create two sources of truth. React can only re-render correctly if ownership is clear: one writer, one authoritative value.
Bug class | What you see | Why it happens |
|---|---|---|
Stale UI / missed updates | Parent updates prop, child UI still shows old value | Child copied prop into state once (or with a wrong sync) so it stops reflecting prop changes |
Input “jumps” / cursor issues | User types, then text resets or cursor jumps | Both parent and child write to the input value; updates race and overwrite each other |
Infinite update loops | “Maximum update depth exceeded” or render thrashing | Effect syncs prop → state, state change triggers parent callback, parent changes prop again |
Controlled/uncontrolled warnings | React warning about switching controlled ↔ uncontrolled | Sometimes you pass value, sometimes you don’t; or you mix value + defaultValue |
Memo / shallow-compare lies | React.memo/PureComponent skips re-render when it shouldn’t (or vice versa) | Mutations or identity mismatches: prop reference stays same while internal state changes (or you mutate a prop object) |
Hard-to-debug coupling | “Works sometimes”, order/timing dependent behavior | Two writers + async scheduling = last write wins, but it’s not obvious who wrote last |
// ❌ Classic bug: copying props to state (stale after parent updates)
function ProfileEditor({ user }) {
const [name, setName] = React.useState(user.name); // copied once
// Later: parent changes user.name (e.g., refetch) but input stays stale
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
// This component now has two truths:
// - parent truth: user.name
// - child truth: name
// ❌ Sync effect + callback can create loops / jitter
function Child({ value, onChange }) {
const [local, setLocal] = React.useState(value);
// tries to "keep in sync" with prop
React.useEffect(() => {
setLocal(value);
}, [value]);
// tries to "keep parent in sync" with local
React.useEffect(() => {
onChange(local);
}, [local, onChange]);
return <input value={local} onChange={(e) => setLocal(e.target.value)} />;
}
Correct pattern | When to use | How it prevents bugs |
|---|---|---|
Single source of truth (controlled) | Parent owns the value; child just renders + emits events | One writer (parent). No local copy, no drift |
Uncontrolled input + ref | You don’t need every keystroke in React state | DOM owns value; React reads it only when needed (submit, blur) |
Derived data (no state) | Value can be computed from props/state | No syncing needed: compute during render (optionally memoize) |
Intentional “draft state” with explicit reset | You need editable local draft that can be reset by parent | You define a reset rule (key change / reset token) instead of ambiguous syncing |
// ✅ Controlled: parent owns value (single source of truth)
function Child({ value, onChange }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
function Parent() {
const [value, setValue] = React.useState('');
return <Child value={value} onChange={setValue} />;
}
// ✅ Uncontrolled: DOM owns value
function UncontrolledForm() {
const ref = React.useRef(null);
return (
<form
onSubmit={(e) => {
e.preventDefault();
alert(ref.current.value);
}}
>
<input ref={ref} defaultValue="" />
<button>Submit</button>
</form>
);
}
// ✅ Intentional draft with explicit reset signal
function Editor({ initialName, resetKey }) {
const [draft, setDraft] = React.useState(initialName);
// explicit reset (not "sync every time")
React.useEffect(() => {
setDraft(initialName);
}, [resetKey]);
return <input value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
Rule of thumb
If two different places can write the “same” value, you will eventually get drift, overwrites, loops, or UI jumps. Decide who owns it (parent via props, or child via local state/DOM), then design the API around that ownership.
Practical scenario
A child component both receives user data as props and stores local edits, creating conflicts.
Common pitfalls
- Duplicating source of truth in props and state.
- Out-of-sync updates when props change.
- Unnecessary re-renders from derived state.
Keep a single source of truth. Test by changing props and verifying UI updates correctly.