React State Best Practices
Table of Contents + โ
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-renderfunction 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 setterfunction 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 3const addThree = () => { setCount(count + 1); setCount(count + 1); setCount(count + 1);};
// โ
Functional updater - each call gets the latest value, ends at 3const 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 syncfunction 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 statefunction 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 spreadconst [form, setForm] = useState({ name: "", isModalOpen: false, theme: "light" });
// โ
Independent values, each in its own state - simple updatesconst [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 itfunction App() { const [query, setQuery] = useState(""); // nobody else here needs this return <SearchBar query={query} setQuery={setQuery} />;}
// โ
Keep it inside the component that actually uses itfunction 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 syncconst [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 listconst [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
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
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
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
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.