Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Beginner 12 min read

When React redraws

Why your value is sometimes “old” and the list refuses to move

In one sentence

React only redraws when you tell it via setState – and during a redraw your function gets a fixed value that does not change halfway through a click.

Three puzzles get solved in this lesson: “why is the old value still there?”, “why does nothing happen?” and “why is the text suddenly in the wrong row?”. All three share one root.

The value is frozen for this pass

On every RenderReact calls your component function and looks at what it returns. Nothing more happens yet.The result is a description (“this is how it should look”). Only afterwards does React decide what actually changes on the page. your function runs from top to bottom again. count is not a drawer you peek into – it is a constant for this particular pass.

snapshot.tsx
function Counter() {
  const [count, setCount] = useState(0);   // this pass: count = 0

  function handleClick() {
    setCount(count + 1);   // means: 0 + 1  ->  1
    setCount(count + 1);   // means: still 0 + 1  ->  1
    console.log(count);    // 0  – nothing changes here any more
  }

  return <button onClick={handleClick}>{count}</button>;
}

// After one click it says: 1. Not 2.
Put another way

Like a photograph: the click works with the picture taken during the last draw. You can point at it three times – the photo does not change.

When the new value depends on the old one
computes with the same 0 three times
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

// Result: 1
passes the running total along
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);

// Result: 3

On the right you hand over a function instead of a value. React calls it and feeds in the most recent state each time. Rule of thumb: if the old value appears on the right-hand side, use the function form.

Good to know That is why the value is still old right after setState

setCount(5) does not change a variable. It tells React: “please redraw, this time with 5”. Until that happens, your function keeps the old value. This is called BatchingReact collects several changes and redraws once afterwards, instead of after every single one. – React collects all the changes from one click and redraws once.

When React redraws at all

triggers.txt
It redraws when …
  • you call setState with a NEW value
  • the component above it redraws
  • a context value changes

NOTHING happens when …
  • you change an ordinary variable      (let x = 1; x = 2)
  • you modify an array/object in place  (list.push(...))
  • you set ref.current
  • you call setState with the same value
Pitfall The most common beginner mistake

list.push(x); setList(list) does nothing. React only checks: “is this still the same array as before?” – and yes, it is, you only put something inside it. Create a new one instead: setList([...list, x]).

In more depth: why isn't changing the contents enough? optional

In JavaScript a variable holding an object or array stores a ReferenceFor objects and arrays JavaScript stores an address, not the contents. Two addresses differ even when the contents look identical.That is why { a: 1 } === { a: 1 } is false in JavaScript. – the address where the contents live, not the contents themselves.

reference.js
const a = [1, 2];
const b = a;          // b points at THE SAME array
b.push(3);

a === b;              // true – one and the same array
console.log(a);       // [1, 2, 3]

const c = [...a];     // fresh copy, new address
a === c;              // false

React compares with Object.isThe comparison React uses to decide whether a value changed. It behaves like === (so for objects it compares addresses)., which behaves roughly like ===. So it only ever sees the address. Same address means “nothing happened” to React – no matter what you changed inside.

This is not pedantry, it is deliberate: comparing addresses takes nanoseconds, walking large objects field by field takes a long time. Hence the rule to be ImmutableNever change the old thing; always produce a changed copy. In React this is the rule for state. and always produce a changed copy.

Lists: React needs name tags

In a list React cannot tell which item is which from its position. That is what a keyA name tag on list items so React knows which item is which – even when the order changes. is for – a name tag taken from your data.

keys.tsx
{todos.map((todo) => <Row key={todo.id} todo={todo} />)}   // ✅

{todos.map((todo, i) => <Row key={i} todo={todo} />)}      // ⚠️ careful
Pitfall What goes wrong with the index

Delete the first entry and every position shifts up by one. React thinks: “item number 0 got new data” – and leaves everything that lived in that row in place: the typed text, the ticked checkbox, the cursor. The result: text attached to the wrong row.

Tip Very useful the other way round

Give a component a new key and React throws it away and rebuilds it from scratch – with empty state. <Form key={userId} /> is therefore the cleanest way to reset a form when a different user is selected.

Touch nothing while drawing

The body of your component must not contain anything that reaches outside: no data loading, no document.title = …, no changes to props. Just compute and return. Such things are called Side effectAnything that reaches beyond pure calculation: loading something, starting a timer, changing the page title. – they belong in click handlers or in useEffect.

Good to know Why does everything run twice in development?

StrictModeA checking mode that deliberately runs some things twice during development, to surface bugs early.None of this happens in the published app. deliberately calls your components twice. If that breaks something, you have a side effect hidden somewhere. It does not happen in the published app – it is purely a bug detector.

Does it stick?

5 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

Why is nothing happening?

The button appears to add nothing. The entry does arrive in the array – you just never see it. Find the reason and fix it.

  • After one click there is one more entry in the list
  • After three clicks there are three more
  • The existing array is replaced, not modified
import { useState } from "react";

export default function App() {
  const [items, setItems] = useState<string[]>(["First entry"]);

  function add() {
    // TODO: why can't you see the change?
    items.push("Entry " + (items.length + 1));
    setItems(items);
  }

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <button onClick={add}>Add</button>
      <ul>
        {items.map((item, i) => (
          <li key={i}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →