Hi, my name is Kostiantyn. With over 10 years of experience in developing single-page applications, I've always found Redux state management to be overly complex. However, while working on large Angular projects where state is stored in services, I realized that Redux could be quite suitable. Managing state with numerous RxJs Subjects scattered throughout the application often leads to "spaghetti code," making the logic hard to follow. Although Redux and Redux Thunks seem complicated, they were the best options available until server state libraries like TanStack (React) Query emerged.
The Props Drilling Problem
Let’s give an overview of a modern approach to state management in React applications.
When you learn the React framework, the first state management approach you use is setState(useState). Your component starts to have its state, but it gets bigger, and you need to reuse the state with children's components. The state gets passed as props. And if we need to reuse state in the components without direct child-parent connection, we end up having what is called “Props drilling”:
How to Solve the Prop Drilling Issue
As you can see, every component in the tree has props that it does not need. The purpose of the “message” prop is only to pass it down the tree. This creates a close coupling of overcomplicated component interfaces with mixed own and passing-down props. Another issue with this approach is that as our application grows, we need a central place to store all our application data to debug it easily.
Here, we use Context + useReducer to store the application state. Thus, we solved the issue with props draining, and also, our application state is stored in the context, it is global, and it can be easily debugged in developer tools. If our application has many values in the context state and our component is only concerned with some part of it, it will still be re-rendered on each context value change. So if our context storage grows, we need to split it so that each application part depends on the needed context slice. Maybe we also need some tools for historical updates.
Redux helps you manage a "global" state that is needed across many parts of your application. In most projects I have worked on, the “global” state comes from the back-end. So Redux suggests saving the global state to store, and the local component state can be saved inside the component’s local React State.
What is React Query?
There is another library called React Query, which creators argue that we should not keep a state that does not belong to the front end, in-store calling such a state “Server state” which means that it belongs to the server and the server database is a single source of truth for it. So they approached this using a stale-while-revalidate caching mechanism. Let’s say our grandpa’s message comes from the back-end:
What we see in the console:
Only one request, meaning React Query, stores our grandpa’s message and propagates it through our tree of components. It will also re-fetch data and update the cache when it is stale. It happens on timeout or window focus, which can be adjusted in settings. Since most front-end applications state is “owned by server” it is wise to store server state in the same place where it got fetched.
This way appears to have the least boilerplate, as we have a state manager and caching library.
React Query Integrations
Official React Query documentation says that for large client states, you can still use state libraries like Redux or Zustand:
Redux has its caching library RTK Query, so it is more sensible to use Zustand as a state manager for the React Query application because it is small and simple. In the following example, we store key query in the Zustand store manager. In turn, to switch between React queries dynamically, we take queryKey from the Zustand store.
React Query Drawbacks
What are the drawbacks of using React Query? The main is library size:
Library | Size (minified) |
---|---|
Zustand | 3.1 Kb |
Redux | 3.7 Kb |
SWR | 15 Kb |
Mobx | 15.7 Kb |
React Query | 57.2 Kb |
React Query is a feature that goes for the cost of increased bundle size. In case you are developing a desktop application, it is not a problem, but it may be a reason to choose a different library for a mobile app, where a small bundle size is essential.
Summary
Based on my experience, Redux is still a very popular approach to state management, especially considering that the React Toolkit removed a lot of boilerplate code, but in my opinion, it is still quite complicated. With the appearance of server state libraries like React Query, the usage of such Flux libraries was significantly narrowed.
Nowadays, the time of developers is the most expensive part of application development, so businesses can use simple free tools like React Query without needing a complicated boilerplate to start an application, and later (if needed) they add Redux or other store libraries. All this makes it faster and cheaper to bootstrap your project, a newly hired developer can easily get on board, and the application starts to have less code and, thus, fewer bugs. Also, the store scheme becomes simpler as React Query stores server code.
So, for small apps and startups where project requirements are unclear and frequently change, I would start front-end development using React Query.