React State Best Practices

In the last lesson you learned React Multiple State Variables, so now you can add state freely. State is easy to add but easy to get wrong, and this lesson is a short set of habits that keep it clean and bug-free.

๐Ÿšซ Never mutate state directly

State in React is not a normal variable you change in place. Here is the rule and why it matters:

  • You always replace state with a new value through the setter. Changing the existing object or array directly is called mutation, and React will not notice it.
  • React decides when to re-render by checking if the value is a new one. So if you change the same object, React sees the same reference and skips the update.
  • That means your screen shows old data even though the data โ€œchangedโ€, right? It looks like a bug with no cause.
  • Always go through the setter with a fresh value, object, or array. That is how React knows something is different.

This pair shows the mistake next to the safe way for an array.

// โŒ Mutating state - React won't re-render
function TodoList() {
const [todos, setTodos] = useState(["Buy milk"]);
const add = () => {
todos.push("Walk dog"); // changing the same array
setTodos(todos); // same reference, React sees no change
};
}
// โœ… Make a new array and pass that to the setter
function TodoList() {
const [todos, setTodos] = useState(["Buy milk"]);
const add = () => {
setTodos([...todos, "Walk dog"]); // brand new array
};
}

A new value, every time

The same rule applies to objects. Donโ€™t do user.name = "Riya". Spread it into a new object instead: setUser({ ...user, name: "Riya" }). Mutating the old one will leave your UI stuck on stale data.

โž• Use the functional updater when the next state depends on the previous

Sometimes the new state is built from the old state, like adding one to a counter. Here is what to do then:

  • Pass a function to the setter instead of a plain value. React calls that function with the latest state and uses what you return. People call this the functional updater.
  • React may batch several updates together, so if you read the old value directly you can be reading a stale one.
  • The updater function always gets the freshest state, so your math is correct.
  • This matters a lot when you update the same state more than once in a row.

This pair shows three quick increases done the wrong way and the right way.

// โŒ Reads the same stale count three times - ends up at 1, not 3
const addThree = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
// โœ… Functional updater - each call gets the latest value, ends at 3
const addThree = () => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};

Easy rule of thumb

If the new state uses the old state, pass a function: setCount(prev => prev + 1). If the new state is a fresh value that does not depend on the old one, a plain value is fine: setName("Alex").

๐Ÿงฎ Donโ€™t store what you can calculate

Keep your state as small as possible. Here is why deriving beats storing:

  • If a value can be worked out from state you already have, or from props, donโ€™t put it in state. Just calculate it while the component renders. A value you compute instead of store is called derived state.
  • Two pieces of state that must stay in sync are a trap. Update one and forget the other, and now they disagree.
  • A calculated value is always correct, because it is built fresh from the real source every render.
  • Less state means less code, fewer setters, and fewer bugs.

This pair stores a full name in state versus building it during render.

// โŒ fullName is extra state that can drift out of sync
function Profile() {
const [firstName, setFirstName] = useState("Alex");
const [lastName, setLastName] = useState("Kumar");
const [fullName, setFullName] = useState("Alex Kumar"); // duplicate info
// now every name change must also update fullName... easy to forget
}
// โœ… Derive it during render - always correct, no extra state
function Profile() {
const [firstName, setFirstName] = useState("Alex");
const [lastName, setLastName] = useState("Kumar");
const fullName = `${firstName} ${lastName}`; // computed, not stored
}

A quick test

Before adding a new piece of state, ask: can I get this from state or props I already have? If yes, compute it during render. State is only for data the component cannot work out on its own.

๐ŸŽฏ Pick the right shape for your state

State can be a single value, an object, or an array. Picking the right shape comes down to a simple guideline:

  • Match the shape to how the values relate to each other. That keeps updates simple.
  • Values that change on their own and have nothing to do with each other should each get their own useState.
  • Values that always belong together, and always change together, can sit in one object or array.
  • Donโ€™t force unrelated things into one object just to have fewer lines. It makes every update harder.

This pair shows unrelated values jammed into one object versus split apart.

// โŒ Unrelated values forced into one object - every update must spread
const [form, setForm] = useState({ name: "", isModalOpen: false, theme: "light" });
// โœ… Independent values, each in its own state - simple updates
const [name, setName] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const [theme, setTheme] = useState("light");

Group only what travels together

An object is great when the fields really move as a group, like the x and y of a position, or all the fields of one form. For values that have nothing to do with each other, separate state variables are cleaner.

๐Ÿ“ Keep state close to where it is used

Put a piece of state in the component that actually needs it. Here is the habit:

  • Donโ€™t push state up to a far away parent just in case. You only move it up when two or more components truly need to share it. People call moving it up lifting state.
  • State high up in the tree makes more components re-render than needed.
  • It is also harder to follow, because the data lives far from where it is used.
  • So lift state only when sharing forces you to. Until then, keep it local.

This pair keeps a search boxโ€™s state local instead of dumping it in the top component.

// โŒ State lives way up in App, but only SearchBar uses it
function App() {
const [query, setQuery] = useState(""); // nobody else here needs this
return <SearchBar query={query} setQuery={setQuery} />;
}
// โœ… Keep it inside the component that actually uses it
function SearchBar() {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Lift only when you must

Start with state local. The day two sibling components both need the same value, move it up to their closest shared parent - and not a step higher than that.

๐Ÿ”‘ Keep a single source of truth

Every piece of data should live in exactly one place in your state. Here is why duplication causes pain:

  • Donโ€™t copy the same information into two state variables. The one place that owns the data is its single source of truth.
  • Once the same fact lives in two places, they can disagree. That is where confusing bugs come from.
  • You have to remember to update both copies every time, and one day you will miss one.
  • With a single source, you update one value and everything that reads it stays correct.

This pair duplicates the selected item versus storing only its id.

// โŒ Two copies of the same fact - selectedItem can fall out of sync
const [items, setItems] = useState([{ id: 1, name: "Pen" }]);
const [selectedItem, setSelectedItem] = useState({ id: 1, name: "Pen" });
// โœ… Store only the id, find the item from the one real list
const [items, setItems] = useState([{ id: 1, name: "Pen" }]);
const [selectedId, setSelectedId] = useState(1);
const selectedItem = items.find((item) => item.id === selectedId);

Store the reference, not a copy

When you need to โ€œrememberโ€ one item from a list, store its id, not a full copy of the item. Then look it up from the list. The list stays the one true source, and nothing can drift apart.

๐Ÿงฉ What Youโ€™ve Learned

  • โœ… Never mutate state - always pass a new value, object, or array to the setter
  • โœ… Use the functional updater (prev => ...) when the next state depends on the previous one
  • โœ… Donโ€™t store what you can calculate - derive values during render instead
  • โœ… Pick the right shape: separate state for independent values, an object or array for grouped ones
  • โœ… Keep state close to where it is used, and lift it up only when components must share it
  • โœ… Keep a single source of truth - never duplicate the same data in two state variables

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Why does calling todos.push(...) and then setTodos(todos) fail to update the screen?

    Why: React checks if the value is a new reference. Mutating the existing array keeps the same reference, so React thinks nothing changed. Pass a new array like [...todos, item] instead.

  2. 2

    When should you use the functional updater form, setCount(prev => prev + 1)?

    Why: Use the functional updater when the new state depends on the old one. It always receives the latest state, which avoids stale values when updates are batched.

  3. 3

    You have firstName and lastName in state and want a full name. What is the best approach?

    Why: fullName can be derived from existing state, so compute it during render. Storing it as separate state creates a duplicate that can drift out of sync.

  4. 4

    To remember which item from a list is selected, what should you store in state?

    Why: Store just the id and look the item up from the one real list. That keeps a single source of truth, so the selection can never disagree with the list.

๐Ÿš€ Whatโ€™s Next?

You now know how to keep state clean - no mutation, minimal state, derived values, the functional updater, and one single source of truth. So far everything has stayed inside one component. But real apps are many components working together, and they need to talk to each other. Up next you will learn how a parent component sends data and actions down to its children.

React Parent to Child Communication

Share & Connect