React architecture

React State Management: Server State, UI State & Forms Guide (2026)

Most React state problems begin when one value tries to represent several things at once. Give each kind of state a clear owner, and components become easier to change, test, and trust.

Choose the owner first

Server stateData fetched from, cached by, and synchronized with a backend.
UI and URL stateLocal interaction details and shareable navigation choices.
Form stateTemporary user input, validation, and submission progress.

The smallest correct owner is usually the most maintainable one.

Classify state before choosing a library

State management is an ownership decision, not a contest between tools. Before adding a context, store, reducer, or cache, ask where the value originates, who needs it, whether it must survive navigation, and what can make it stale. The answers usually point to a simpler home than a global client store.

Remote data

Products, permissions, search results, and account details have a backend source of truth.

Ephemeral UI

A modal, expanded row, active tab, or drag target only matters to a small part of the screen.

Workflow input

Draft fields, validation errors, and submit status belong to a form until the server accepts them.

A value can move as its needs change. A filter may start as local state, move to the URL when it should be bookmarked, and become server input when it changes the query. Make that move consciously rather than keeping three unsynchronized copies.

Treat remote data as server state, not a permanent client copy

Server state is owned by a system outside the React tree. It can be changed by another user, another browser tab, a background process, or a later request. Model it around fetching, loading, failure, caching, invalidation, and refetching instead of copying every response into a global store.

function ProjectList({ accountId }) {
  const { data, isLoading, error } = useProjects(accountId);

  if (isLoading) return <ProjectListSkeleton />;
  if (error) return <RetryProjects accountId={accountId} />;
  return <Projects projects={data.projects} />;
}

Keep cache keys tied to the inputs that define the result, such as account, filters, and cursor. After a mutation, update or invalidate the affected query deliberately. A broad refresh can hide an ownership problem, create request storms, and replace a user's optimistic change with unrelated stale data.

Useful boundary: server responses should enter the UI through one query or data-loading path. Components can transform the data for display, but should not silently become a second database.

Keep UI state close to the UI that changes it

Start with useState when a value has one nearby owner. Lift it only to the nearest common parent when sibling components need to coordinate. This keeps changes readable: a developer can see the state, the events that change it, and the rendered result in one small area.

Use a reducer when a workflow has several related transitions, such as a multi-step editor or a complex selection model. The benefit is not fewer lines; it is explicit events and valid transitions. For broad cross-cutting values such as a theme or authenticated session, context can be a good delivery mechanism, but avoid turning it into a high-churn bucket for every interaction.

Performance follows boundaries too. A frequently changing global value can cause many consumers to reconsider rendering. Split contexts by change frequency, pass focused props where practical, and profile before adding memoization. Our React rendering-performance guide explains how to validate those decisions with real interactions.

Put shareable navigation choices in the URL

A search term, page number, sort order, selected tab, or report date range is often part of the page's address, not hidden client state. URL state survives refreshes, supports browser navigation, can be bookmarked, and gives support teams a reproducible link. Parse it at the route boundary, validate it, and use it to drive the data query.

// /projects?status=active&page=2
const status = validStatuses.includes(query.status) ? query.status : "all";
const page = Number.isInteger(Number(query.page)) ? Number(query.page) : 1;

useProjects({ status, page });

Do not place secrets, large drafts, or every pixel-level preference in the URL. The rule is practical: if a user expects a copied link or Back button to restore a meaningful view, model that choice as navigation state.

Make form state temporary, validated, and recoverable

Form state is a working copy of user intent. Keep it separate from the last confirmed server record so validation, cancellation, retry, and unsaved-change warnings remain possible. Decide whether fields are controlled by React, delegated to the browser, or managed by a form library based on the form's complexity—not because every input needs the same pattern.

  • Validate inexpensive rules near the field, then validate authorization and business rules on the server.
  • Disable duplicate submissions while a request is in flight and provide a clear pending state.
  • Map server validation failures back to the field or workflow step a user can fix.
  • Only replace the confirmed server data after a successful response; preserve a draft when a retry is useful.

For destructive or multi-step changes, make the transition explicit: draft, reviewing, submitting, succeeded, or failed. That model makes error recovery testable and prevents a stale response from overwriting newer input.

Derive values instead of storing duplicates

Duplicated state is a common source of impossible screens. If a total can be calculated from line items, calculate it during rendering. If a selected record can be found from an ID and a list, store the ID. If a button is disabled because a form is invalid or submitting, compute that condition from the source values.

const selectedProject = projects.find((project) => project.id === selectedId);
const canSubmit = form.isValid && !form.isSubmitting;
const openTasks = tasks.filter((task) => task.status === "open");

Memoize a derived calculation only when measuring shows it is expensive and its inputs are stable enough to benefit. Derived state should make correctness obvious first; optimization comes after a profile identifies a real cost.

Create clear state boundaries around features

Organize state around product features and data ownership, not a single application-wide folder. A billing screen can own its local invoice filters and form workflow while sharing only stable account identity and server-query utilities. This makes features easier to delete, move, and test without changing unrelated areas.

For React Server Components, fetch data on the server when possible, pass only the client interaction boundary that needs browser APIs, and keep mutations at deliberate entry points. The design pairs with the boundary guidance in our React Server Components guide. Whatever your rendering model, define who can write each value and how stale data is reconciled after a write.

React state-management checklist

✓ Identify the source of truth before storing a value

✓ Keep local UI state at the nearest useful owner

✓ Model remote data with loading, error, cache, and invalidation rules

✓ Put shareable filters and navigation choices in the URL

✓ Keep drafts separate from confirmed server data

✓ Derive totals, selections, and flags instead of duplicating them

✓ Split broad contexts by ownership and update frequency

✓ Test retry, back navigation, refresh, and stale-response behavior

Build React applications that stay understandable as they grow

Endurance Softwares helps teams design practical React and Next.js architecture, dependable data flows, and polished product experiences.

Discuss Your React Architecture

Shares
Get Quote
Let's build something powerful

Have a project idea? Let’s turn it into a scalable product.

Book Free Consultation

© 2026 Endurance Softwares. All rights reserved.