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.

Вам не потрібен Redux, щоб виправдати використання редьюсера. Якщо у вас є три або більше змінних стану, які оновлюються одночасно, або якщо ваш наступний стан значною мірою залежить від попереднього, редьюсер значно спрощує компонент.

Де насправді зберігається стан

Іноді проблема не в тому, як ви зберігаєте стан, а в тому, де саме. Поширена помилка — підняття стану до батьківського компонента лише тому, що він може знадобитися деінде. Якщо лише один кінцевий компонент використовує частину стану, залиште її там. Це колокація, і вона зменшує радіус ураження змін. Не змушуйте батьківський компонент перерендеритися лише тому, що дитина відкрила