Open nearly any React codebase and you will spot the same reflex. A developer needs to track a value, so they reach for useState. Need a counter? useState. A temporary input value? useState. A boolean to flip a modal? useState. Before long, a single component holds a dozen separate hooks, each managing a sliver of data that may or may not need to persist across renders. The result is noisy code, extra re-renders, and state scattered like loose change across a component.
The habit is understandable. useState is the first hook most of us learn, and it works. But working is not the same as fitting. Treating every piece of data as reactive state creates problems that only show up once the component grows.
Just Because It Changes Does Not Mean It Needs State
Not every variable that shifts over time belongs in useState. Some values are merely the result of something else you already have. If you store a user's full name in state simply because you concatenate firstName and lastName, you now have two sources of truth. When firstName updates because the parent re-renders, your fullName state sits stale until you run another effect to sync it. You do not need a sync effect. You need a derived value.
const fullName = `${firstName} ${lastName}`;
Compute it during render. If the derivation is expensive, memoize it. But do not give it its own useState hook unless the user can edit that full name independently of the parts.
The same rule applies to filtered lists. If you hold both allItems and filteredItems in state, you have doubled your maintenance surface. Filter during render. Keep the source array and the filter text in state, then derive the visible list. This guarantees that the filtered list is never out of sync with the source.
Some Values Should Never Trigger Re-renders
useState exists specifically to tell React that something changed and the DOM might need updating. If a value changes but no part of the UI cares about that change, useRef is the better tool.
Timers and intervals are the classic example. Storing setInterval IDs in state causes a re-render every time you start or stop a timer, even though the user cannot see the interval ID. A ref holds that value without notifying React. The same logic applies to tracking previous props, measuring DOM nodes before paint, or storing the latest callback for a custom hook. Ask yourself: does this value need to appear on screen? If the answer is no, it probably does not need useState.
DOM nodes themselves also belong in refs. While you can store a DOM element in state, doing so triggers a re-render after the ref callback runs. In most cases, you only need the node for an imperative method or a measurement, not to render it differently.
The Boolean Trap
Related UI concerns tend to sprawl when every flag gets its own hook. You see components with isLoading, isError, and isSuccess defined as three separate booleans. The problem is that these three states are not independent. If isLoading and isSuccess are both true, your UI is in an impossible condition, yet TypeScript and React will let you render it anyway.
Grouping related state prevents these invalid combinations. Instead of three booleans, track a single status string: 'idle', 'loading', 'success', or 'error'. Only one can be active at a time, which eliminates impossible states at the type level. If the data is more complex, an object with a discriminated union cleans things up even further. When you find yourself updating several useState calls inside the same event handler, that is a signal that those values belong together.
Reach for useReducer, Not Another useState
There is a point where state updates become a game of whack-a-mole. You call setA, then setB, then conditionally setC, all inside one function. The next developer to read that code must trace through the sequence to understand what the component actually does.
useReducer shines here. It does not replace useState because it is more advanced; it replaces useState because the logic demands it. A reducer centralizes how state changes. Instead of sprinkling imperatives across event handlers, you dispatch an intention: dispatch({ type: 'submitted' }). The reducer decides what the next state looks like. This makes testing trivial, because your state logic is a pure function. It also makes debugging easier, because every change leaves a traceable action.
Vous n'avez pas besoin de Redux pour justifier l'utilisation d'un reducer. Si vous avez trois variables d'état ou plus qui se mettent à jour simultanément, ou si votre état suivant dépend fortement du précédent, un reducer simplifie considérablement le composant.
Où se trouve réellement l'état
Parfois, le problème n'est pas la manière dont vous stockez l'état, mais l'endroit où il se trouve. Une erreur courante consiste à remonter l'état vers un parent simplement parce qu'il pourrait être nécessaire ailleurs. Si un seul composant terminal utilise une partie de l'état, gardez-la là. C'est ce qu'on appelle la colocalisation, et cela réduit le rayon d'impact des modifications. Ne forcez pas le parent à se re-render parce qu'un enfant a ouvert
