Home / Articles

Rethinking React State: Where Your Data Should Actually Live

Fewer React bugs from less useState: move state into the URL, the DOM or derived values, and decide who owns each piece before reaching for state management.

Publisher: REACTAPP.TOP17 min read3931 words

This article is published in English.

Sources: Stop Adding React State: It’s Creating Bugs You Don’t Need

A sales representative at a growing SaaS company spends a good chunk of the morning digging through a customer database containing ten thousand accounts.

After careful filtering, they isolate enterprise clients based in Europe whose subscriptions haven't been renewed in the past ninety days.

The result is a precise list of thirty accounts that need immediate attention.

They grab the URL straight from the browser and send it over Slack to their regional director with a note:

"Here are the priority accounts for this morning."

The director opens the link.

The page renders.

But instead of the filtered list, it shows the default view: all ten thousand accounts, unfiltered, unsorted.

All that careful filtering vanished because the engineer who built the page had stored every filter, sort field, and pagination value inside local component state using useState.

This scenario illustrates a pattern that shows up constantly in React codebases.

We tend to reach for state simply because React makes it so easy to create.

Need a filter value?

useState.

Need to track which tab is active?

useState.

Need a counter?

useState.

Need to hold onto a form field?

useState.

Need to track the different phases of an API call?

That's three more useState calls.

Before long, a single component is juggling dozens of values that never really needed to live in React state to begin with.

State itself isn't the enemy here. Every non-trivial React app needs some.

The real issue is placing state somewhere it doesn't belong.

Each extra state variable is one more thing your app has to keep in sync with everything else.

Left unchecked, this tends to produce a range of headaches, among them:

  • Extra work spent keeping duplicate values aligned
  • Data that goes stale in a cache somewhere
  • Values shown on screen that no longer match reality
  • Renders firing when nothing meaningful actually changed
  • Timing bugs where two updates collide unpredictably
  • Effects that grow tangled trying to keep everything consistent
  • Bugs that take much longer to trace back to their source
  • A page that forgets everything the moment it's reloaded
  • Screens nobody can link to or save for later
  • Combinations of flags that produce a UI state that should never exist

Writing excellent React isn't really about mastering complex state logic.

Often, it's about recognizing when not to create that state at all.

1. The URL Is Your Application's Best State Manager

Picture a typical data-dense application screen.

It probably needs to track:

  • Search
  • Filters
  • Sorting
  • Pagination
  • Active tabs
  • Selected categories
  • Date ranges

The go-to approach is usually to stuff all of this into component state.

function ProductList() {
  const [activeCategory, setActiveCategory] = useState('all');
  const [currentPage, setCurrentPage] = useState(1);
  const [searchQuery, setSearchQuery] = useState('');
  // Fetch and filter data...
}

At first glance, this seems perfectly fine.

Then the user hits refresh.

Every value snaps back to its default.

Or they share the link with a colleague.

That colleague lands on a totally different view.

Or they click the browser's Back button.

The filtering they had set up may not come back, because React state has no built-in connection to browser navigation history.

useState itself isn't at fault here.

The real issue is that the URL already exists as a mechanism for representing navigational state, and it's being bypassed entirely.

Rather than writing:

const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [category, setCategory] = useState('all');

you could encode that same information directly in the URL.

Something like:

/products?category=enterprise&page=3&query=react

Using a routing library like Next.js, React Router, or TanStack Router, you can read those parameters straight from the URL:

import { useSearchParams } from 'next/navigation';
function ProductList() {
  const searchParams = useSearchParams();  const category = searchParams.get('category') || 'all';
  const page = Number(searchParams.get('page')) || 1;
  const query = searchParams.get('query') || '';  // Fetch data using the URL state...
}

This small shift changes the lifecycle of that state dramatically.

Refresh the page?

Filters stay intact.

Share the link?

Whoever opens it sees the exact same view.

Save it as a bookmark?

It'll load back to that same state later.

Hit Back?

The browser correctly steps through previous states.

That's a meaningful architectural win.

URL state gives you more than persistence

Surviving a page refresh is only part of the benefit.

The bigger win is that the URL turns into a portable snapshot of your app's current state that can be shared with anyone.

Take a customer-support dashboard as an example:

/support/tickets?status=open&priority=high&team=billing

One manager could send that exact link to another manager instantly.

A sales-focused dashboard might look like:

/accounts?region=europe&plan=enterprise&renewal=90

An analytics tool might use something like:

/analytics?range=30d&metric=revenue&groupBy=region

None of these are arbitrary query strings.

Each one is a deep link pointing to a specific, reconstructable state of the app.

That's precisely the job URLs were built for.

When URL state makes sense

URL parameters turn out to be a great fit for things such as:

  • What the user typed into a search box
  • Active filter selections
  • The column and direction data is sorted by
  • Which page of results is showing
  • The currently selected tab
  • A chosen date range
  • Which saved view is currently open
  • Settings for a generated report
  • Custom configuration on a dashboard
  • Any UI state a user might want to hand off to someone else

That said, not everything belongs there.

Passwords obviously shouldn't go in the URL.

Neither should a fleeting modal animation state.

Nor should a large, deeply nested object, generally speaking.

The guiding question is straightforward:

Ask yourself whether a user would reasonably expect this view to be shareable, bookmarkable, or restorable — if so, the URL is probably the right home for that piece of state.

2. Stop Storing Values You Could Simply Compute

A quick win when trimming down React state is to quit persisting values that are actually derived from other values.

Take this example:

const [items, setItems] = useState([]);
const [itemCount, setItemCount] = useState(0);

And elsewhere in the component:

setItems(newItems);
setItemCount(newItems.length);

You now have two separate state variables describing the same underlying fact.

If items gets updated but itemCount doesn't follow along, your interface will display something inaccurate.

You've manufactured a syncing headache that never needed to exist.

A cleaner version looks like this:

const [items, setItems] = useState([]);
const itemCount = items.length;

There's now a single source of truth.

The count gets computed each time the component renders.

This same logic applies when filtering collections.

Rather than writing:

const [items, setItems] = useState([]);
const [activeItems, setActiveItems] = useState([]);

you can typically just write:

const activeItems = items.filter(item => item.active);

Or:

const totalPrice = items.reduce(
  (total, item) => total + item.price,
  0
);

Or:

const hasErrors = fields.some(field => field.error);

Or:

const completedCount = todos.filter(todo => todo.completed).length;

The question worth asking isn't:

"Could I technically store this in state?"

Sure, you could.

What you should actually ask is:

"Does this value carry its own separate lifecycle?"

If it doesn't, it probably has no business being state.

3. Stop Wiring Every Form Field to State

Another habit worth breaking is treating each form field as its own state variable.

A small form built this way might look like:

function ContactForm() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();    submitData({
      firstName,
      lastName,
      email,
      message
    });
  };  return (
    <form onSubmit={handleSubmit}>
      <input
        value={firstName}
        onChange={e => setFirstName(e.target.value)}
      />      <input
        value={lastName}
        onChange={e => setLastName(e.target.value)}
      />      <input
        value={email}
        onChange={e => setEmail(e.target.value)}
      />      <textarea
        value={message}
        onChange={e => setMessage(e.target.value)}
      />      <button type="submit">Send</button>
    </form>
  );
}

Nothing here is technically incorrect.

Controlled inputs earn their keep when your app genuinely needs to react as the user types. A few situations where that matters:

  • Checking a field's validity as it's entered
  • Offering autocomplete-style suggestions
  • Reformatting text on the fly
  • Counting remaining characters
  • Showing or hiding other fields based on input
  • Rating how strong a password is
  • Linking one field's behavior to another's value

But often your app has no reason to care what the user typed until they hit Submit.

In those cases, there's no need for React to track every character as it's typed.

The browser is already doing that job.

Let the browser handle it

Native HTML forms have been managing input values for decades.

You can lean on that built-in behavior with FormData:

function ContactForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);    const payload = {
      firstName: formData.get('firstName') as string,
      lastName: formData.get('lastName') as string,
      email: formData.get('email') as string,
      message: formData.get('message') as string,
    };    submitData(payload);
  };  return (
    <form onSubmit={handleSubmit}>
      <input name="firstName" required />
      <input name="lastName" required />
      <input name="email" type="email" required />
      <textarea name="message" required />      <button type="submit">Send</button>
    </form>
  );
}

With this pattern, React no longer needs a dedicated state slot for each field.

The browser retains what the user entered.

React simply reads the values once the form gets submitted.

This becomes especially valuable for large forms.

Picture a 30-field enterprise onboarding flow.

Would you actually want 30 separate state variables just to hold what someone typed?

Probably not.

Controlled and uncontrolled inputs can coexist

None of this means controlled components are a mistake.

They aren't.

There are plenty of situations where controlling the input is exactly right.

The point is simply to drop the assumption that React must own every field's value by default.

If your app doesn't need the data until submission, letting the native form hold it is often sufficient.

Making that distinction can simplify large forms considerably.

4. Why Boolean Flags Break Down

Another recurring issue shows up constantly in real-world codebases.

Consider a typical API call.

A common pattern is to track it with three separate booleans:

const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

At the start, all three are false:

loading = false
success = false
error = false

Once the request begins:

loading = true

Once it finishes successfully:

loading = false
success = true

This seems reasonable at first glance.

But suppose an error-handling branch fails to reset isLoading.

You could end up with:

loading = true
error = true

Or a sloppy retry mechanism might leave you with:

success = true
error = true

At that point, your component is in a state that doesn't make sense logically.

Should it render the success message?

Should it render the error state?

Should it still show a spinner?

Technically, all three booleans could justify a "yes" answer simultaneously.

That ambiguity is exactly the bug.

Treat it as a finite state machine instead

A cleaner approach:

type RequestStatus =
  | 'idle'
  | 'loading'
  | 'success'
  | 'error';
const [status, setStatus] =
  useState<RequestStatus>('idle');

Now every valid state is explicit and mutually exclusive.

The request can only be one of:

idle
loading
success
error

There's no way to accidentally represent "loading" and "succeeded" at once.

Rendering also becomes far simpler to follow:

if (status === 'loading') {
  return <Spinner />;
}
if (status === 'error') {
  return <ErrorMessage />;
}if (status === 'success') {
  return <SuccessBanner />;
}return <DefaultForm />;

This isn't just a nice-to-have from TypeScript.

It reflects a broader design rule:

When two pieces of state can never be true independently of one another, they shouldn't be modeled as two separate variables.

5. Avoid Using Effects to Keep State in Sync

This is another place where extra state variables generate extra, avoidable code.

Take this setup:

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');

Paired with:

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

What you've built here is a synchronization mechanism for a value that never needed its own state in the first place.

The simpler fix:

const fullName = `${firstName} ${lastName}`;

The change may look minor, but the architectural impact is significant.

The original version needs:

  • Three separate state variables
  • An effect to keep them aligned
  • An implicit sync relationship between values
  • An extra render cycle triggered by the effect
  • A dependency array that has to stay accurate

The revised version needs only:

  • Two real values
  • One value derived from them
  • No effect
  • No sync logic to maintain

A good habit when reviewing React components is to ask:

"Why does this value need to live outside the render calculation?"

If the honest answer is "to stay in sync with something else," that's usually a sign the value shouldn't be state at all.

6. Three Questions to Ask Before Adding State

Before writing this line:

const [value, setValue] = useState(...);

run through three checks first.

Question 1: Could this be computed while rendering?

If the inputs needed to produce this value already exist, there's no reason to store a duplicate.

Rather than:

const [itemCount, setItemCount] = useState(0);

write:

const itemCount = items.length;

Rather than:

const [hasErrors, setHasErrors] = useState(false);

write:

const hasErrors = fields.some(field => field.error);

Avoid state for anything that can be derived on the spot.

Question 2: Should this survive a page reload?

If it should, consider whether the URL is the right owner for it.

Typical candidates:

  • Search terms
  • Pagination state
  • Active filters
  • Sort order
  • Selected tabs
  • Date ranges

Ask yourself: if a link to this page is shared, should the recipient land on the exact same view?

If so, the URL is usually the better home for that data.

Question 3: Can the browser hold this until the form is submitted?

If a field's value only matters at submission time, an uncontrolled input may be all you need.

Instead of:

const [email, setEmail] = useState('');

you might just need:

<input name="email" type="email" />

and read it later with:

const formData = new FormData(e.currentTarget);

Let the browser manage what it's already built to manage.

7. What Genuinely Belongs in React State

None of this is an argument for eliminating component state altogether.

Trying to remove it entirely would be overcorrecting.

Plenty of values are legitimately suited to living in state.

For instance:

const [isOpen, setIsOpen] = useState(false);

A temporary dropdown or a modal window doesn't have to be reflected in the URL at all.

Interactive state

const [activeStep, setActiveStep] = useState(1);

When a value drives an interaction that stays entirely within one component, keeping it in local state is often the right call.

Values that change independently

If something truly has its own lifecycle, and there's no way to compute it from data you already have, then state is the appropriate home for it.

Optimistic UI

Occasionally you need to show something on screen before the server has actually confirmed it happened.

That's a legitimate use of state too.

The point isn't to chase this rule:

"Use less useState no matter what."

The point is this one instead:

"Make sure every piece of state has the right owner."

Sometimes the right owner is React.

Sometimes it's the URL.

Sometimes it's the DOM.

Sometimes it's the server.

Sometimes it's a dedicated data-fetching or state-management layer.

That ownership decision is what really shapes your architecture.

8. Focus on Who Owns State, Not Just How Much of It You Have

A well-built React application isn't defined by having a small number of state variables.

What actually matters is giving each piece of state a clear, correct owner.

Take a dashboard that tracks:

Search query
Filters
Pagination
Sort order
Form values
Server data
Loading status
Selected row
Modal visibility
Derived totals

It's tempting to shove every one of these into useState without a second thought.

But these values don't share the same job, so they shouldn't share the same home.

A more useful mental model splits them up like this:

URL

  • Search
  • Filters
  • Pagination
  • Sort order
  • Shareable tabs

DOM

  • Form values that only matter once the form is submitted

Server / Data Layer

  • Customer records
  • API responses
  • Cached server data

Derived Values

  • Counts
  • Totals
  • Filtered lists
  • Validation summaries

React State

  • Modal visibility
  • Temporary selections
  • Animation state
  • Short-lived interactions

Once you distribute state this way, the component is left with far less to keep in sync.

That reduction is where the real payoff comes from.

9. Duplicated State Is Where Tomorrow's Bugs Come From

The riskiest state in an application isn't necessarily the most complex state.

Often it's the state that exists in more than one place at once.

Imagine you have:

const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);

Now you're on the hook for making sure filteredProducts always matches whatever is in products.

Add one more filter and you get:

const [filteredProducts, setFilteredProducts] = useState([]);
const [searchResults, setSearchResults] = useState([]);

Now there's an additional relationship you have to keep consistent by hand.

Sooner or later, someone updates the underlying data but forgets to update one of the derived variables that depends on it.

The interface quietly falls out of sync with reality.

A user eventually reports something like:

"The count says 42 products, but only 39 show on the screen."

That mismatch usually isn't a flaw in the filtering logic itself.

It's a symptom of the app having more than one source of truth for the same information.

If instead you derive the value:

const filteredProducts = products.filter(...);

you wipe out an entire class of bugs before it can happen.

This is why cutting down on state isn't purely about render performance.

It's really about shrinking the number of things that are capable of drifting out of sync.

10. The Cost of State Goes Beyond Re-renders

When people discuss state, the conversation usually centers on performance: "will this trigger another render?"

That's a fair concern, but it's not the most expensive part of the equation.

The real cost is complexity.

Every additional state variable is one more thing your team has to keep in their heads.

Then you inevitably add a setter call.

setSomething(...)

Then, probably, a useEffect to react to it.

useEffect(...)

Then a dependency array listing the values it watches.

[firstValue, secondValue]

Then some kind of reset logic to bring it back to its initial value.

setSomething(initialValue)

Then an edge case where that state has to stay aligned with a navigation change.

Then a bug report because the state didn't get cleared after some mutation.

Bit by bit, the codebase turns into a web of synchronization logic.

That's substantially harder to reason about than a component whose data flows in cleanly from a handful of well-defined sources.

The Real Skill in React Isn't Managing More State

It's understanding where each piece of state belongs.

A less experienced developer looking at a UI might ask:

"What state does this need?"

A better architectural instinct leads to a different question:

"Which of these values genuinely need to be state at all?"

And the sharpest version of the question is:

"Who is the rightful owner of each piece of state?"

Shifting your thinking this way can change how you approach building React applications altogether.

The URL can be responsible for navigation state.

The browser can be responsible for form state.

The server can be responsible for server-owned data.

Derived values can stay derived instead of being duplicated.

TypeScript unions can model states that are mutually exclusive.

And React itself can concentrate on the short-lived, interactive state that genuinely belongs to the UI layer.

That division of labor produces a much cleaner architecture.

Summary

Having more state isn't an achievement in itself.

A larger number of state variables doesn't make an application more capable.

Usually it just means more synchronization work, more dependencies to track, and more chances for the data to become inconsistent.

Before reaching for another useState, run through these questions:

  1. Can this value be computed while rendering? If so, derive it rather than storing it separately.
  2. Does it need to survive a page refresh, or be shareable via a link? If so, put it in the URL's search parameters.
  3. Does this form field need to be tracked on every single keystroke? If not, an uncontrolled input paired with FormData may be enough.
  4. Are several boolean flags actually describing one underlying lifecycle? Collapse them into a single status value or state machine.
  5. Is this state being created only to keep it synchronized with another piece of state? If so, you're probably storing something you could just derive instead.
  6. Does this value truly have a lifecycle of its own? If not, it likely doesn't need to be state at all.

The objective was never to minimize the raw count of useState calls.

It's to make sure that as few values as possible are responsible for defining the truth of your application.

Because every unnecessary piece of state is one more thing your app has to keep synchronized.

And every synchronization step you avoid writing is a bug you never have to fix.