DEV_NET_CORE
GET_STARTED
ReactComponents, props, state, and rendering behavior

Rerender triggers, derived state, and avoiding duplicated state

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.

Code
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.

Code
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.

Code
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.

Code
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:

Code
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.

Code
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.

Code
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.

Code
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.

Code
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:

Code
setCount((current) => current + 1);

This is especially important when queueing multiple updates:

Code
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.

Code
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.

Code
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:

Code
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:

Code
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");

Better:

Code
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:

Code
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:

Code
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:

Code
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);

This can accidentally produce both isSending and isSent as true.

Better:

Code
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:

Code
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:

Code
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:

Code
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:

Code
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.

Code
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.

Code
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:

Code
<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.

Code
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 useMemo when 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.

Code
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:

Code
<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, or useCallback before fixing state design.
  • Creating new object or function props and expecting memo to 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.

Interview Practice

PreviousProps flow, local state, and lifting state upNext Up“You might not need an effect” patterns