WebDevPro #130: Rethinking State Management in Modern React
Crafting the Web: Tips, Tools, and Trends for Developers
Most Spring Boot projects stop at REST APIs
Real systems require service discovery, API gateways, centralized configuration, and built-in resilience. In this live, hands-on workshop, you’ll build a working microservices system end-to-end, define service boundaries, wire up discovery, configure a gateway, and handle failures properly.
Welcome to this week’s issue of WebDevPro!
If you’ve worked with React for a while, you’ve probably run into the same recurring question: how should we manage state in this application?
State management is one of those topics that almost every React team ends up debating sooner or later. Should everything live in context? Do we need a global store? Would something like Zustand make things simpler?
Those conversations usually focus on tools. But in many cases the real problem shows up earlier than that.
In practice, React applications rarely become difficult to maintain because the wrong library was chosen. They become difficult to maintain because different kinds of state get handled in the same way.
A form input, an API response, a UI filter, and a shared user object often end up managed with identical patterns. That is where complexity starts to creep in.
In this article, we will look at the main categories of state that appear in React applications and how each category is best managed. By the end, you will have a clearer way to decide where state belongs in a React app, whether that means local component state, shared state, server data tools, or even the URL itself.
Not all state belongs in the same place
State in React applications usually falls into a few clear categories.
Local state lives inside a single component. A dropdown menu, a modal visibility toggle, or a small UI interaction typically belongs here. Hooks such as useState or useReducer work well because the logic remains isolated.
Server state represents data fetched from APIs or databases. This includes user profiles, product listings, or analytics data. The key difference is that the source of truth lives outside the application.
Form state includes field values, validation errors, and submission status. Forms have their own lifecycle and benefit from specialized tools such as React Hook Form or React’s form hooks.
URL state stores small pieces of UI state in route or search parameters. Tabs, filters, pagination, and search queries often belong here.
Shared state exists when multiple components need access to the same data. Authentication details, application settings, or user preferences are common examples.
Understanding these categories clarifies an important point. React state is not a single problem. It is a set of related problems that require different solutions.
The easiest state to manage is the state you never store
One of the most common sources of complexity in React applications comes from duplicated state.
Consider a list of items where the interface displays only active entries. A common pattern stores both the original list and a filtered version of it.
const [items, setItems] = useState([...])
const [filteredItems, setFilteredItems] = useState([])
useEffect(() => {
setFilteredItems(items.filter(item => item.active))
}, [items])
This introduces synchronization logic that React must maintain.
A simpler approach derives the filtered list directly from the original state.
const [items, setItems] = useState([...])
const filteredItems = items.filter(item => item.active)
The application now maintains a single source of truth. React calculates the derived value when needed.
Derived state reduces bugs and simplifies reasoning about data flow. Many React components become easier to maintain once unnecessary state variables disappear.
Shared state is where React applications become complicated
Local state remains predictable because it stays within a component. Shared state changes that dynamic because multiple components read and update the same values.
Developers often encounter this situation when user data, permissions, or global settings must appear across different sections of the interface.
The simplest approach begins with prop drilling. A parent component holds the state and passes it through props to child components.
This technique often receives criticism, yet it works well for a small number of adjacent components. It also keeps data flow explicit and easy to trace.
Problems appear when the component tree becomes deeper. Intermediate components may receive props they do not use, simply to pass them further down the hierarchy.
At this stage, many teams introduce React context.
Context allows components to access shared values without passing them through each level of the tree. A provider component stores the shared state and exposes it to any descendant component.
This pattern simplifies access but introduces a different trade-off. When context values change, every consumer beneath the provider may re-render. For small applications this rarely matters. In larger interfaces the rendering behavior becomes harder to control.
Libraries such as Zustand approach shared state from a different angle. Zustand creates a centralized store and allows components to subscribe only to the pieces of state they require.
const userName = useUserStore(state => state.userName)
Components update only when the subscribed value changes. This selective subscription reduces unnecessary renders and keeps the shared state logic compact.
Context and Zustand both solve shared state challenges, but they operate at different scales. Context works well for moderate application state. Zustand becomes attractive once shared state spreads across larger parts of the interface.
Server state follows a different lifecycle
Data fetched from external services introduces a different category of state entirely.
Server data involves caching, background refetching, loading indicators, and stale data management. Handling these concerns with useState and useEffect often leads to repetitive code and fragile synchronization.
Libraries such as TanStack Query address this layer directly. They treat server data as cached resources rather than ordinary component state.
Components request data using a shared query key.
const { data } = useQuery({
queryKey: [’user’, userId],
queryFn: fetchUser
})
TanStack Query stores the response in a client cache. Other components requesting the same query receive the cached result rather than triggering another network request.
This model separates server data concerns from UI logic. React components remain focused on rendering rather than managing asynchronous data lifecycles.
Some state belongs in the URL
Another overlooked location for state is the browser address bar.
Interfaces often contain UI state that describes how a page is being viewed. Active tabs, filter selections, search queries, and pagination indexes all fall into this category.
Storing this information in URL search parameters creates several benefits.
The state becomes shareable through links. Refreshing the page preserves the interface configuration. Browser navigation also restores previous UI states naturally.
Framework hooks such as useSearchParams and routing utilities make this pattern straightforward.
const params = useSearchParams()
const activeTab = params.get(’tab’)
Many teams duplicate this information inside React state and attempt to synchronize it with the URL. Removing that duplication simplifies the architecture.
The address bar can act as a reliable source of truth for small pieces of UI state.
Choosing the right home for state
Modern React offers many tools for managing state, but the real skill lies in recognizing where each type of state belongs.
Local UI interactions usually remain simplest when handled with component state.
Values that can be computed from existing data should be derived instead of stored.
Data fetched from APIs benefits from tools designed for server state, such as TanStack Query, which handle caching and refetching automatically.
UI state that affects navigation, such as active tabs or filters, often works best when stored in URL parameters.
Shared client state can start with straightforward prop passing and evolve into solutions such as context or Zustand when multiple components need coordinated access.
The key takeaway is not which library to choose. It is learning to recognize the nature of the state problem in front of you. Once you can distinguish between local, shared, server, form, and URL state, the architecture decisions become much clearer. Instead of forcing every problem into the same pattern, each piece of state can live in the place where it is easiest to manage.
That shift in thinking leads to React applications that are easier to reason about, easier to scale, and far less prone to the state management issues that often slow teams down.
Build real skills in LLMs and agentic AI with this 20+ course Packt bundle, featuring titles like Learn Python Programming, 4E and The LLM Engineer’s Handbook. Learn how to design and deploy intelligent systems while supporting World Central Kitchen.
This Week in the News
🧠 TypeScript 6.0 RC prepares the ecosystem for the Go-powered compiler: TypeScript 6.0 RC has landed and it marks an important transition for the language. This release functions largely as a stepping stone toward TypeScript 7.0, which will introduce a new native compiler written in Go. The RC itself contains only a few small changes compared to the beta, but the bigger shift lies in the groundwork being laid for the next generation of the toolchain. Required updates to tsconfig.json help projects align with upcoming architecture changes, giving teams time to prepare before the performance gains of the Go-powered compiler arrive later this year.
⚛️ SolidJS 2.0 beta introduces first-class async and a redesigned reactive core: SolidJS has entered the 2.0 beta phase after several years of experimental work on its next-generation reactive system. The release introduces major architectural changes including a rewritten signals implementation, deterministic batching, and first-class async support built directly into the framework’s primitives. New patterns such as
actionand optimistic state helpers aim to make server mutations and UI updates easier to manage, while updates to control flow and rendering bring several breaking changes developers will notice quickly. The beta sets the stage for broader ecosystem updates ahead of the stable 2.0 release.☁️ Astro 6 introduces a Rust compiler and deeper Cloudflare alignment: Astro 6 arrives as the framework’s first major release since its acquisition by Cloudflare in January, and the platform direction is already becoming clearer. The update introduces an experimental Rust compiler that will eventually replace the original Go-based .astro compiler, promising faster builds and a modernized compilation pipeline. Development workflows also improve through Vite’s new Environment API, which allows developers to run the exact production runtime during development. Astro also introduces a new Fonts API that simplifies custom font handling across projects.
🌊 Cloudflare pushes for a simpler modern JavaScript Streams API: Cloudflare engineers are questioning the design of the Web Streams API, arguing that it reflects an earlier era of JavaScript before patterns like async iteration became common. The result is an abstraction that often feels overly complex, with specialized readers, locking mechanics, and extra boilerplate. Cloudflare proposes a simpler model built on modern JavaScript primitives that could improve both ergonomics and performance. Early benchmarks suggest potential speedups of up to 120× across runtimes such as Node.js, Deno, and Workers. In a related talk, James Snell explains how an async-iterator-driven approach could make streaming code easier to reason about for developers working with edge platforms and large data pipelines. 🎥 Watch the talk:
Beyond the Headlines
🤖 Literate programming may finally make sense in the AI agent era: Donald Knuth’s idea of literate programming asked developers to write code as a narrative meant for humans, with the compiler following along. For decades the concept remained mostly academic. This piece revisits the idea through the lens of AI-assisted development, where agents read, analyze, and generate code alongside developers. Clear explanations, structured reasoning, and intent-rich programs suddenly become far more valuable. The argument is simple but compelling: the rise of coding agents may finally make literate programming practical.
🐘 Just use Postgres and delay the infrastructure sprawl: Many modern stacks quickly grow into a collection of specialized tools: queues, search services, analytics systems, and caches. This article argues that much of that complexity arrives too early. PostgreSQL already includes powerful capabilities such as JSON support, full-text search, background jobs, and extensions that cover a surprising range of workloads. For many products, a single Postgres database can carry far more responsibility than teams assume. The takeaway is pragmatic: lean on Postgres longer and introduce new infrastructure only when the workload genuinely demands it.
⚡Building a real-time collaborative to-do app with Jazz and Vue: Real-time collaboration often brings complex backend logic, synchronization challenges, and WebSocket plumbing. This tutorial shows a simpler path by building a collaborative to-do app using Jazz and Vue. The stack manages shared state and synchronization automatically, allowing the interface to update instantly as multiple users interact with the same data. The walkthrough focuses on how collaborative state flows through the application and how the UI reflects updates in real time. It offers a practical introduction to modern tooling for building collaborative applications.
⚛️ Why React needed Fiber and what problem it actually solved: React Fiber is one of the biggest architectural changes in React’s history, yet its purpose is often misunderstood. This deep dive explains why the original reconciliation algorithm struggled with large or complex updates. Fiber introduced a scheduling model that allows React to pause, resume, and prioritize rendering work instead of processing everything in one blocking pass. That change laid the foundation for features such as concurrent rendering and smoother user experiences under heavy workloads.
Tool of the Week
🛠️ VMPrint deterministic PDF generation without headless browsers
Print-to-PDF pipelines often rely on headless Chrome, bringing along browser quirks and inconsistent rendering. VMPrint takes a different approach with a pure TypeScript typesetting engine that bypasses the DOM entirely and computes layout through typographic math. The result is deterministic output across browsers, Node.js, and edge runtimes like Cloudflare Workers. Given identical input, VMPrint guarantees identical layout down to the sub-point position of every glyph. For teams building document generation pipelines or publishing systems, it offers a precise and reproducible alternative to browser-based rendering.
That’s all for this week. Have any ideas you want to see in the next article? Hit Reply!
Cheers!
Editor-in-chief,
Kinnari Chohan




