Overview
Re-render triggers, derived state, and duplicated state are closely related because they determine when React calls components again and whether the next UI is calculated from a clean source of truth. React rendering is not manual DOM mutation. A render is React calling your component functions to figure out what the UI should look like for the current props, state, and context.
The most common render triggers are:
- The initial render.
- A component's state update.
- A parent re-rendering and rendering its children.
- A context value used by a component changing.
Derived state is data that can be calculated from existing props or state. Duplicated state is the same information stored in multiple places. Both are frequent causes of bugs because they create multiple sources of truth.
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = `${firstName} ${lastName}`;
Here fullName should be derived during render, not stored in state. If it were stored separately, every name update would need to remember to update it too.
For interviews, this topic matters because strong React developers know when to store state, when to derive values, why re-renders happen, how state snapshots and batching work, when memoization helps, and how to avoid contradictory or duplicated state models.
Core Concepts
What Rendering Means
Rendering means React calls your component function to calculate React elements for the current inputs.
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}</h1>;
}
Rendering does not mean the browser DOM definitely changes. After rendering, React compares the new output with the previous output and commits the necessary DOM updates.
React's process can be thought of as:
- Trigger: something asks React to render.
- Render: React calls components.
- Commit: React applies necessary changes to the DOM.
This distinction matters because render logic should be pure. Side effects belong in event handlers or effects, not in the component body.
Initial Render
The first render happens when the app root is mounted.
createRoot(document.getElementById("root")!).render(<App />);
React calls the root component and builds the initial UI tree. Frameworks often hide this bootstrapping code, but the concept is the same.
After the initial render, updates happen when state, context, or parent rendering causes React to call components again.
State Updates Trigger Renders
Calling a state setter schedules a render for that component.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((current) => current + 1)}>
{count}
</button>
);
}
When setCount runs, React schedules Counter to render again with the next state.
State updates should be immutable:
setUser((current) => ({
...current,
name: "Ava",
}));
Mutating existing state and passing the same reference can make changes hard for React and humans to reason about.
Parent Renders and Child Renders
When a parent renders, React normally renders its children too.
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((count) => count + 1)}>
{count}
</button>
<Child />
</>
);
}
Clicking the button updates parent state, so Parent renders. Child is called again because it is part of the parent's output.
This is normal. A re-render is not automatically a performance problem. React still commits only the DOM changes that are needed.
If a child is expensive and often receives the same props, memo may help. But memoization is a performance optimization, not a correctness tool.
Props and Context
Props are not updated independently. A child receives new props because its parent rendered and passed them.
function Parent() {
const [name, setName] = useState("Ava");
return <Greeting name={name} />;
}
When name changes, Parent renders and passes a new name prop to Greeting.
Context can also trigger renders. If a component reads a context value and that provider value changes, React re-renders the consumers.
const ThemeContext = createContext("light");
function ThemeLabel() {
const theme = useContext(ThemeContext);
return <span>{theme}</span>;
}
Large context values that change frequently can cause broad re-rendering. Split context by responsibility and update frequency when needed.
State as a Snapshot
Each render sees a fixed snapshot of state.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // Old value from this render.
}
return <button onClick={handleClick}>{count}</button>;
}
Calling setCount schedules a future render; it does not change the count variable in the current handler.
When the next value depends on previous state, use a functional update:
setCount((current) => current + 1);
This is especially important when queueing multiple updates:
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
The final result is three increments.
Batching
React batches multiple state updates during the same event so it can render once with the final result instead of rendering after every setter call.
function handleClick() {
setFirstName("Ava");
setLastName("Nguyen");
setStatus("saved");
}
React can process these together and render once.
Batching is good for performance, but it means you should not expect state variables in the current handler to immediately reflect the queued updates. Use functional updates when the next state depends on previous state.
Derived Values
A derived value is calculated from props or state.
function CartSummary({ items }: { items: CartItem[] }) {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return <p>Total: ${total.toFixed(2)}</p>;
}
total does not need to be state because it can be calculated during render from items.
Bad:
const [items, setItems] = useState<CartItem[]>([]);
const [total, setTotal] = useState(0);
Now every item update must also update total. If one code path forgets, the UI becomes inconsistent.
Avoiding Redundant State
Redundant state stores something already available from existing props or state.
Bad:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");
Better:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = `${firstName} ${lastName}`;
Derived values are recalculated when the component renders. This avoids extra state updates and prevents values from getting out of sync.
Avoiding Duplicated State
Duplicated state stores the same information in multiple places.
Bad:
const [items, setItems] = useState<Item[]>(initialItems);
const [selectedItem, setSelectedItem] = useState<Item | null>(initialItems[0]);
If an item is edited in items, selectedItem may still contain the old object. Better store the selected ID:
const [items, setItems] = useState<Item[]>(initialItems);
const [selectedId, setSelectedId] = useState<string | null>(initialItems[0].id);
const selectedItem = items.find((item) => item.id === selectedId) ?? null;
Now the item data lives in one place, and the selection stores only the identity of the selected item.
Avoiding Contradictory State
Contradictory state allows impossible combinations.
Bad:
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
This can accidentally produce both isSending and isSent as true.
Better:
type Status = "typing" | "sending" | "sent" | "error";
const [status, setStatus] = useState<Status>("typing");
const isSending = status === "sending";
const isSent = status === "sent";
One state variable represents the real finite states. Boolean flags are then derived during render.
Mirroring Props in State
Mirroring props in state usually creates stale data.
Bad:
function Message({ color }: { color: string }) {
const [messageColor, setMessageColor] = useState(color);
return <p style={{ color: messageColor }}>Hello</p>;
}
If the parent later passes a new color, the state does not automatically update. The initial state only uses the prop on the first render.
Better:
function Message({ color }: { color: string }) {
return <p style={{ color }}>Hello</p>;
}
Mirroring props is only appropriate when you intentionally want to capture the initial value and ignore later prop changes. Use names like initialColor or defaultValue to make that contract clear.
You Might Not Need an Effect
Do not use effects to calculate values that can be calculated during render.
Bad:
function Form({ firstName, lastName }: Props) {
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}
This causes an unnecessary render with stale fullName, then another render after the effect updates it.
Better:
function Form({ firstName, lastName }: Props) {
const fullName = `${firstName} ${lastName}`;
return <p>{fullName}</p>;
}
Effects are for synchronizing with external systems, not for normal derived rendering.
Expensive Derived Values
Most derived values should be calculated directly during render. If a calculation is expensive and inputs often do not change, use useMemo.
function ProductList({
products,
query,
}: {
products: Product[];
query: string;
}) {
const filteredProducts = useMemo(
() => filterProducts(products, query),
[products, query]
);
return <List products={filteredProducts} />;
}
useMemo caches a calculation result until its dependencies change. It is a performance optimization. The code should still be correct without it.
Do not use useMemo to fix incorrect data flow. First remove duplicated state and make the source of truth clear.
Memoization and Re-renders
memo can skip re-rendering a component when its props are unchanged.
const UserCard = memo(function UserCard({ user }: { user: User }) {
return <h2>{user.name}</h2>;
});
Memoization helps only when:
- The component re-renders often with the same props.
- Rendering is expensive enough to matter.
- Props are stable.
This defeats memo:
<UserCard user={{ id: user.id, name: user.name }} />
The object is new on every render. Prefer passing stable values or memoizing derived objects only when there is a real performance need.
Identity and Re-renders
Objects, arrays, and functions created during render have new identity each time.
function Parent({ user }: { user: User }) {
const options = { showEmail: true };
return <UserCard user={user} options={options} />;
}
If UserCard is memoized, the new options object can still cause it to re-render. Fixes include:
- Pass primitive props.
- Move object creation into the child if possible.
- Use
useMemowhen stable identity is actually needed. - Avoid premature memoization if rendering is cheap.
State Preservation and Reset
React preserves state while a component remains in the same position in the rendered tree. Changing a key tells React to treat it as a different component and reset its state.
function UserEditor({ userId }: { userId: string }) {
return <EditForm key={userId} userId={userId} />;
}
This is useful when changing userId should create a fresh form.
Avoid random keys:
<EditForm key={Math.random()} />
That resets state every render and usually hides a data-flow problem.
Common Mistakes
Common mistakes include:
- Treating every re-render as a bug.
- Storing values that can be calculated from props or state.
- Mirroring props in state without intending to ignore later prop updates.
- Keeping both selected object and selected ID in state.
- Keeping multiple booleans that can contradict each other.
- Using effects to calculate derived values.
- Adding
memo,useMemo, oruseCallbackbefore fixing state design. - Creating new object or function props and expecting
memoto skip rendering. - Using random keys to force state resets.
- Mutating state and expecting React to reliably detect meaningful changes.
Best Practices
Use these rules of thumb:
- Store the minimal source of truth.
- Derive everything else during render when possible.
- Avoid duplicated and contradictory state.
- Use one status field for mutually exclusive states.
- Keep state flat when nested updates become awkward.
- Use functional state updates when next state depends on previous state.
- Use effects for external synchronization, not normal derived data.
- Treat memoization as a performance optimization, not a correctness fix.
- Use stable keys to intentionally preserve or reset state.