The term “state” means data that changes over time.
In React applications, we typically work with two main types of state: client state and server state.
Client State
Client state refers to the state that is owned and managed entirely within your application. It exists only on the client side. For example, UI states (modal open/closed, form input values) and user preferences (theme selection, language settings) fall into this category.
Here are two characteristics that define client state:
- Synchronous: You can access client state using synchronous APIs. The data is immediately available and does not require network requests.
- Temporary: Client state may get lost upon page reload (unless explicitly persisted) and is generally non-persistent between sessions.
React provides several built-in mechanisms for managing client state. The most fundamental is the useState hook, which allows components to maintain local state. For more complex scenarios, React offers useReducer for state logic and Context API for sharing state across components without prop drilling.
While React’s built-in tools handle basic client state needs, larger applications often benefit from dedicated state management libraries. Among client-state management libraries, Zustand remains a popular choice. Other options include Redux Toolkit and Jotai, each offering different approaches to client state management.
Server State
Server state refers to the state that originates from and is owned by an external system (an API or a database). User accounts and profiles, business data (orders, transactions, inventory, customer records) and content management (articles, media files, user-generated content) are examples of server state.
Here are two characteristics that define server state:
- Asynchronous: The data is not immediately available as it must be fetched over the network.
- Persistent: Server state lives on the remote server. As a result, it survives page reloads and browser sessions. When the page loads again, the browser simply refetches the existing data.
Challenges of Server State
Managing server state is more complex than client state. It introduces a set of challenges that client state simply doesn’t have.
Network reliability and error handling
Server state introduces unpredictable failure scenarios. Network connections can fail, servers can return errors, and timeouts can occur at any moment. Managing these scenarios gracefully while maintaining a smooth user experience requires sophisticated error handling strategies.
Loading states and user experience
While waiting for remote data, applications must handle loading states effectively. This involves showing appropriate loading indicators, implementing fallback UIs when components aren’t available, and managing the transition between loading, success, and error states.
Data consistency and synchronization
When server state is cached locally, your application essentially maintains two sources of truth: the remote server and the local cache. These can diverge in several ways.
Other users might modify the same data you’re viewing. Another tab in your browser could make updates that your current tab doesn’t know about. Or your own pending requests might arrive at the server out of order, causing race conditions.
Keeping these sources synchronized requires answering difficult questions: How do you detect when remote data has changed? How do you handle conflicts when local and server state disagree? How do you ensure users always see accurate information without constantly refetching everything?
Cache invalidation and staleness
Determining when cached data becomes stale and needs refreshing introduces significant complexity. You need mechanisms to receive updates when remote data changes, perform strategic refetches, and handle cache invalidation across different parts of your application.
Optimistic updates and rollback scenarios
Modern applications often implement optimistic updates to improve perceived performance. However, this introduces additional complexity.
When a user makes a change, you can update the UI optimistically assuming the server call will succeed. But if it doesn’t, you’ll need a way to roll back those changes in your frontend state. Managing these rollback scenarios requires careful state tracking and recovery mechanisms.
The challenges we discussed above are the reason why it’s recommended to use purpose-built libraries such as TanStack Query and SWR to manage server state. They abstract away much of this complexity, providing battle-tested solutions for caching, synchronization, and error handling that would be extremely difficult to implement correctly from scratch.