The Plight of State Management in React
Table of contents
We have to give credit to where it is due. React did revolutionise frontend development by providing a declarative framework wherein the developer need not be concerned with DOM operations to update the user-interface (UI). Moreover, it effectively applied the principle of UI being a function of the current state i.e. UI = f(state). Also, through this mechanism there are two-fold benefits:
1. With the help of state, the interface logic stays centralised i.e. rather than store state in one place and update parts of the UI corresponding to the new state manually, React automatically does the necessary updates.
2. React guarantees that no part of the UI will be left unattended when a respective part of a state changes, no matter how deep a corresponding state attribute is attached to the UI in the component hierarchy.
The second point above is a matter of debate w.r.t. performance benefits, in the frontend community including the likes of other frameworks. And this is the subject of discussion in this article.
Before we dive into the intricacies of the second point, it must be noted that React is a UI library and not a framework. Unlike Vue or Angular, to make the most out of using React, one has to make use of available packages that cater to specific requirements, e.g. routing, global state management, CSS frameworks, security, and so on.
State management requires special attention, because that’s what drives the entire frontend and, to an extent, code organisation. For a long time (due to the way React is structured) prop-drilling was the only way to pass state around, even if an intermediate component made no use of it. And if a piece of state was required by an adjacent component, React recommends the state to be lifted up to the nearest common ancestor.
<a href="https://redux.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">Redux</a> entered the scene to alleviate this problem. Developed by the developers of React themselves, they had first hand knowledge of the matter. React’s one-way data binding was taken advantage of while implementing the concept of state updates. There will be one global store, accessible to the entire hierarchy. To update state: components would be connected to this store by a higher-order-component that would invisibly pass props to each component in the hierarchy. To trigger a state change: one now had to call dispatch, which would trigger an action, which would reduce (update) the state, which in turn would handover the new state to the global store and consequently React’s reconciliation algorithm to work upon, thereby updating components down the entire hierarchy.
Developers back then developed all sorts of update triggers around the concept of redux, including tying up keypress events in form states with the global store. Sooner or later, the inevitable happened. The UI would lag considerably and feature additions would take time to roll off because to insert/fix an action, one had to now go through changing 3 different files. Add to that, applying the concept of middleware like sagas, thunks, etc. which would add even more cognitive load on the developer, the problem statement would literally translate from solving the business problem to solving the way code be structured on account of using redux.
Although the new <a href="https://redux-toolkit.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">redux-toolkit</a> promises many benefits, especially conciseness (through utilising hooks), performance is still reminiscent of the entire hierarchy update (unless your browser supports Proxies which is used by selectors in react-redux).
It’s not Redux’s or MobX’s fault. It’s the way React is that authors of these libraries had to find ways to circumvent the problem of prop-drilling. It’s also the case where developers mishandled state through these libraries due to lack of proper understanding of where state should reside. It’s also the fact that authors of these libraries have spent countless hours optimising for performance and handling edge-cases but have been careful towards total API change lest they break compatibility. Period.
With the release of Context API as a stable feature, coupled with hooks, the solution to props-drilling got a fresh breath. Initially, it became apparent that this is the go-to solution and that Redux’s days are numbered. Well, applying global state management principles with the Context API came with it’s own set of problems, one glaring issue being that Contexts are not suitable for frequent updates and it never was developed keeping selective rendering in mind. It is most suitable for theming and for applications with comparatively low hierarchy depth. The reason for not using Context API for global state management being that even if a component is not consuming a piece of state but is wrapped somewhere down by a particular provider at the top, any state update will re-render all the components in the wrapped hierarchy irrespectively.
Innumerous articles and blogs have been written to inform the audience of alternative or hacky solutions. However, time and again, I find two crucial libraries never being mentioned at all:
1. <a href="https://react-tracked.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">react-tracked</a> — As the name suggests, it tracks state usage through the use of Proxies (link <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" target="_blank" style="color:#0057FF; text-decoration:none;">here</a>). Multiple global stores can be used. A global store can be used with or without a reducer. Moreover, react-tracked can also be used in conjunction with either useReducer(), <a href="https://react-tracked.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">react-tracked</a> or <a href="https://www.npmjs.com/package/zustand" target="_blank" style="color:#0057FF; text-decoration:none;">zustand</a> depending on the developer’s familiarity with these tools. What’s beautiful about react-tracked is that component(s) in the hierarchy will not be updated unless a (part of) state that the component consumes changes even when the component accesses that (part of a) global store. This ensures that the UI doesn’t lag (due to React’s diffing algorithm) as there will not be a diff on the respective component(s).
2. <a href="https://jotai.pmnd.rs/" target="_blank" style="color:#0057FF; text-decoration:none;">jotai</a> — It is one of the few libraries in the family of atom models for state updates. Atoms are individual entities that pertain to a piece of state. Atoms can be viewed as orthogonal pieces of states w.r.t. the component hierarchy. Although it is advised to setup a provider at the base of the hierarchy, it is not a mandate in the case of jotai. Moreover, atom updates in any part of the hierarchy will automatically update the UI corresponding to the components that are consumers of that atom. Jotai also provides additional utility functions that allow one to use atoms with Storage, URLs, hashes, reducers, etc. What’s appealing about jotai and the atom based state management tools is that global state need not be one single store: they are now discrete atoms available globally. Also jotai allows developers to derive an atom from another atom to form a new store for a particular need. Jotai also has <a href="https://atomictool.dev/" target="_blank" style="color:#0057FF; text-decoration:none;">atomic-devtools</a>, a visualiser library extension akin to redux-devtools, although as of this writing, the extension is yet to be approved in the extension marketplace.
We have to give credit to where it is due. React did revolutionise frontend development by providing a declarative framework wherein the developer need not be concerned with DOM operations to update the user-interface (UI). Moreover, it effectively applied the principle of UI being a function of the current state i.e. UI = f(state). Also, through this mechanism there are two-fold benefits:
1. With the help of state, the interface logic stays centralised i.e. rather than store state in one place and update parts of the UI corresponding to the new state manually, React automatically does the necessary updates.
2. React guarantees that no part of the UI will be left unattended when a respective part of a state changes, no matter how deep a corresponding state attribute is attached to the UI in the component hierarchy.
The second point above is a matter of debate w.r.t. performance benefits, in the frontend community including the likes of other frameworks. And this is the subject of discussion in this article.
Before we dive into the intricacies of the second point, it must be noted that React is a UI library and not a framework. Unlike Vue or Angular, to make the most out of using React, one has to make use of available packages that cater to specific requirements, e.g. routing, global state management, CSS frameworks, security, and so on.
State management requires special attention, because that’s what drives the entire frontend and, to an extent, code organisation. For a long time (due to the way React is structured) prop-drilling was the only way to pass state around, even if an intermediate component made no use of it. And if a piece of state was required by an adjacent component, React recommends the state to be lifted up to the nearest common ancestor.
<a href="https://redux.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">Redux</a> entered the scene to alleviate this problem. Developed by the developers of React themselves, they had first hand knowledge of the matter. React’s one-way data binding was taken advantage of while implementing the concept of state updates. There will be one global store, accessible to the entire hierarchy. To update state: components would be connected to this store by a higher-order-component that would invisibly pass props to each component in the hierarchy. To trigger a state change: one now had to call dispatch, which would trigger an action, which would reduce (update) the state, which in turn would handover the new state to the global store and consequently React’s reconciliation algorithm to work upon, thereby updating components down the entire hierarchy.
Developers back then developed all sorts of update triggers around the concept of redux, including tying up keypress events in form states with the global store. Sooner or later, the inevitable happened. The UI would lag considerably and feature additions would take time to roll off because to insert/fix an action, one had to now go through changing 3 different files. Add to that, applying the concept of middleware like sagas, thunks, etc. which would add even more cognitive load on the developer, the problem statement would literally translate from solving the business problem to solving the way code be structured on account of using redux.
Although the new <a href="https://redux-toolkit.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">redux-toolkit</a> promises many benefits, especially conciseness (through utilising hooks), performance is still reminiscent of the entire hierarchy update (unless your browser supports Proxies which is used by selectors in react-redux).
It’s not Redux’s or MobX’s fault. It’s the way React is that authors of these libraries had to find ways to circumvent the problem of prop-drilling. It’s also the case where developers mishandled state through these libraries due to lack of proper understanding of where state should reside. It’s also the fact that authors of these libraries have spent countless hours optimising for performance and handling edge-cases but have been careful towards total API change lest they break compatibility. Period.
With the release of Context API as a stable feature, coupled with hooks, the solution to props-drilling got a fresh breath. Initially, it became apparent that this is the go-to solution and that Redux’s days are numbered. Well, applying global state management principles with the Context API came with it’s own set of problems, one glaring issue being that Contexts are not suitable for frequent updates and it never was developed keeping selective rendering in mind. It is most suitable for theming and for applications with comparatively low hierarchy depth. The reason for not using Context API for global state management being that even if a component is not consuming a piece of state but is wrapped somewhere down by a particular provider at the top, any state update will re-render all the components in the wrapped hierarchy irrespectively.
Innumerous articles and blogs have been written to inform the audience of alternative or hacky solutions. However, time and again, I find two crucial libraries never being mentioned at all:
1. <a href="https://react-tracked.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">react-tracked</a> — As the name suggests, it tracks state usage through the use of Proxies (link <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" target="_blank" style="color:#0057FF; text-decoration:none;">here</a>). Multiple global stores can be used. A global store can be used with or without a reducer. Moreover, react-tracked can also be used in conjunction with either useReducer(), <a href="https://react-tracked.js.org/" target="_blank" style="color:#0057FF; text-decoration:none;">react-tracked</a> or <a href="https://www.npmjs.com/package/zustand" target="_blank" style="color:#0057FF; text-decoration:none;">zustand</a> depending on the developer’s familiarity with these tools. What’s beautiful about react-tracked is that component(s) in the hierarchy will not be updated unless a (part of) state that the component consumes changes even when the component accesses that (part of a) global store. This ensures that the UI doesn’t lag (due to React’s diffing algorithm) as there will not be a diff on the respective component(s).
2. <a href="https://jotai.pmnd.rs/" target="_blank" style="color:#0057FF; text-decoration:none;">jotai</a> — It is one of the few libraries in the family of atom models for state updates. Atoms are individual entities that pertain to a piece of state. Atoms can be viewed as orthogonal pieces of states w.r.t. the component hierarchy. Although it is advised to setup a provider at the base of the hierarchy, it is not a mandate in the case of jotai. Moreover, atom updates in any part of the hierarchy will automatically update the UI corresponding to the components that are consumers of that atom. Jotai also provides additional utility functions that allow one to use atoms with Storage, URLs, hashes, reducers, etc. What’s appealing about jotai and the atom based state management tools is that global state need not be one single store: they are now discrete atoms available globally. Also jotai allows developers to derive an atom from another atom to form a new store for a particular need. Jotai also has <a href="https://atomictool.dev/" target="_blank" style="color:#0057FF; text-decoration:none;">atomic-devtools</a>, a visualiser library extension akin to redux-devtools, although as of this writing, the extension is yet to be approved in the extension marketplace.
React is first and foremost a UI library, and, first and foremost, is used to solve Facebook’s problems.
It is asked of the broader community to:
1. Research about modern packages that doesn’t dictate code organisation (one specifically being due to global state management) in a way such that the pace of feature release or code refactoring takes a toll
2. Stop ending the discussion at Redux, it being the only tool that can handle global state
3. Take advantage of modern JS features that aid in making the frontend lighter and more responsive
4. Appreciate the fact that React is flexible and the community being big, one can find a huge number of libraries at one’s disposal
5. Use the right tool for the right job. Check the 3 + 1 parameters (popularity, quality and maintenance, plus size) in the <a href="https://npmjs.com/" target="_blank" style="color:#0057FF; text-decoration:none;">NPM registry</a> against any package before blindly adding that as a dependency in any project
Give your product vision the talent it deserves
building your dream engineering team.