One of the biggest challenges when a React app starts to grow, is the management of the state. In large applications, React alone is not sufficient to handle the complexity. That’s when developers begin to look for third-party libraries, like Redux, Jotai, Zustand, XState, etc
But before installing third-party libraries from the beginning and ramping up the complexity of the app from the get-go, we must know the answer to the following question:
What is State?
State is data that can change in response to something. One concept we must grasp is that not all state is the same, there are different kinds of states.
Types of State
There are many different ways we can think of state types, but I am going to break them down into a couple of specific ones:
- Server State
- Client / Application State
Server State is data that the server has and we need to display, things like a user profile, products in stock for our e-commerce site, etc. Most of our time as frontend developers is spent working with this kind of state.
While most traditional state management libraries are great for working with client state, they are not so good for working with async or server state. This is because the server state is different. For starters, server state:
- is persisted remotely in a location you do not control or own
- requires asynchronous APIs for fetching and updating
- implies shared ownership and can be changed by other people without your knowledge
- can potentially become “out of date” in your application if you’re not careful
Once we understand the nature of the server state, a lot more challenges will arise as we go, EG:
- deduping multiple requests for the same data into a single request
- updating stale data in the background
- knowing when data is stale
- reflecting updates as quickly as possible
- performance optimizations such as lazy loading, pagination, prefetching, etc
All of these are problems beyond the scope of a library such as Redux or MobX and are incredibly hard to solve with our custom code, that’s when React-Query or RTK Query comes into play.
Let’s talk a little about the aforementioned challenges, with some real examples of how we could tackle them:
Caching, updating, and reflecting new data
Caching is one of the hardest problems to solve in software, and in React development that is not an exception.
An example where caching can be useful is storing the result of an initial request and providing that result to the following request made to that endpoint, avoiding unnecessary calls to the server. With React-Query we get that functionality out of the box:
First, we define the QueryClientProvider, which is a HOC that gives access to the stored data to its child components.
Now, we define the “Todos” Component
A lot is going on in here, so let’s break it down:
The useQuery hook returns several properties, but in the example we use “data, isLoading, and error”. These properties are the data returned from the endpoint and two boolean values to use if we want to show a spinner if the request is running or to display the error if something went wrong.
To the hook, we pass the “Query Key” which is important, because it tells React-Query that this API call provides data to be cached under that key and all further requests will consume the cached data instead of making a new call. The second argument is the async API call.
Now with the mutation and cache invalidation (cleaning up invalid or stale data and forcing a refetch to update the cache).
Here we use the “useMutation” hook, to which we pass the API post call and a callback function that invalidates the cached data under the QueryKey “todos”. That’s saying to React-Query that these mutations invalidate the cached data under that key and it will perform in the background a new request to retrieve from the server the newly updated data to be displayed.
Deduping concurrent requests for the same data
If two or more components render at the same time and call the same endpoint to get data, that will cause two equal requests at the same time to the same endpoint, retrieving the same data twice, wasting memory and network resources. React-Query takes care of this in the background, if two or more requests under the same QueryKey are sent at the same time, they will be deduped into a single request under the hood.
In the previous snippet, we can see that both components use the same query, and if they are mounted at the same time, they will make a request for the data, React-Query will dedup those requests into a single one and provide the data to both of the components.
This is the state directly related to the application, for example: what page are you on?, Is your microphone connected?, Is the menu tab open? etc. Most of us are familiar with this kind of state, and it’s easily managed with the built-in functionality that React provides us, such as the useState hook or useContext. We should be mindful in what cases it is worth installing a state management library such as Redux because the app’s complexity will skyrocket and every team has a different convention on how to set it up and use it.
In most cases, a developer sees fit to use redux when some global state is needed, for example in a notification component that needs to be called from anywhere in the app. But we can solve this with a simple React Context without adding any dependencies.
If you still think that you will need a state management library, one of the best options available is Redux Toolkit, the “official” way of using redux. This lib provides an efficient, simple and opinionated way of using redux, and it’s developed by the redux maintainers.
Comparing Redux and Context API
|Built-in tool that ships with React||Additional installation Required, driving up the final bundle size|
|Requires minimal setup||Requires extensive setup to integrate it with a React Application|
|Specifically designed for static data, that is not often refreshed or updated||Works great with both static and dynamic data|
|Adding new contexts requires creation from scratch||Easily extendible due to the ease of adding new data/actions after the initial setup|
|Debugging can be hard in highly nested component structures, even with React Dev Tools||Incredibly powerful Redux Dev Tools to ease debugging|
|UI login and State management logic are in the same component||Better code organization with separate UI logic and State Management Logic|
Both are excellent tools for their specific niche. Redux is overkill just to pass data from parent to child & Context API truly shines in this case. When you have a lot of dynamic data, Redux is the best choice. Analyzing the table, we can conclude that Redux is better for larger applications, whilst the Context API shines when the scale of the project is smaller or there are few cases when global state is needed.
We can conclude that managing client state is a complicated matter that can quickly go out of hand and become a mess to maintain. That is why we must use the right tool for the job and be mindful of how we use and update our state.