fbpx

The State of React State Management

The State of React State Management
Reading Time: 14 minutes

By Eric Cassidy, GAP’s Staff Software Engineer

Do you remember when Angular and React were new things? The web was slow, and that latency was losing you millions of dollars — said the tech billionaires. Mobile traffic was blasting into relevance. Manager-types shared articles and engineers experimented. It felt like the beginning of the age of SPA.

And it was. If you want to share a lot of dynamic information in a browser, these methodologies help present complex data while providing users with a natural, speedy experience.

We now have so many options for managing data in a browser. Soooo many options. And we typically use several of them at once in our applications. Right? Don’t we?

We regularly audit our code to make sure the usages are appropriate. Wait… have you ever done that?

If these questions make you nervous, you are not alone. We have solutions that work just fine, so we repeat the pattern for all sorts of things. If you suspect your app is too slow or jumpy, your root storage is too large, some API call is taking way too long, or maybe you’re calling the same APIs too often, perhaps it’s time to audit and strategize.

Even if you don’t yet detect these symptoms, it’s probably a good time to look at the landscape and reconsider past choices. And it is always a good idea to audit API endpoints to make sure they make sense, provide what is appropriate for presentation, and are not returning HUGE pieces of data — SPA does not necessitate cloning an entire database into the browser.

First and foremost, you need to audit how your application handles sensitive data, and make sure you keep your business logic separated from strictly UI concerns.

(Before we jump in, Syou might want to check out this blog post article for web architecture best practices.)

Which Problem Are You Solving?

Maybe the best place to start is to identify a use case and determine if you are handling it in a sensible way. Depending on the feature, you might be:

  • Getting something from an API to persist for a lot of views
  • Getting something from an API to persist for a single view
  • Recording a user interaction
  • Performing a calculation for view only
  • Recording a functional state (session management/analytics)

Each discrete piece of state might have a different “ideal solution.”. If you are building an application from scratch, you might try to bucket the pieces of state into the scope that they serve, and then use that analysis to determine which patterns you want to use for state management.

Auditing an existing application can be a bit more tricky. You may opt for a complete audit or rather, an iterative “fix it as you go” approach. But if you want to normalize and stabilize your state management processes, it will be beneficial to determine which problems you are solving before you start hacking away at code or choosing packages.

Consider these recommendations based on the scope of the data that you are manipulating.

Simple Solutions for Simple Problems

You may have a use case for data that needs to persist outside of the React instance, such as a server session token, or a 3rd party analytics implementation. A user might want to record or share a set of interactions, such as a search query or data filter settings. Or maybe there is a user view preference or “I already dismissed this warning,” such as a GDPR agreement that you want to record on the client only. 

  • Query String Parameters
    Query string parameters can be very useful for bookmarking or linking other users to a view with preloaded interactions, such as a view with a search query or filter settings. Some solutions pack URLs with thousands of characters in order to track clicks across devices and users. Though this solution is in the “simple solutions” category, beware: your routing solutions need to pass these values around, and that can be tricky. If you redirect a user to a login screen, you must persist the parameters all the way through to the view that needs them.
  • Local/Session Storage
    Local storage provides a solid technique for recording local interactions that will be consumed by a browser. And it is also a helpful way to record user interactions that might need to live beyond your “session.” Examples include a modal for GDPR, “I agree,” age gate, or even local view preferences that a user wants to save. And in most instances, you might find them useful to track long, multi-stage form progress. This storage can be wiped out if the user chooses to do so, and it is not useful for sharing across browsers/devices.

    If you want to record something that truly only belongs in a single session, choose session storage… especially if it is a lot of data. But note: if you are using session storage, it is possible this data instead belongs in the React state management scope. In fact, be very critical of what you choose to save outside React; you might be misusing local storage or overcomplicating something that React is natively very good for. Be mindful of the amount of data you are storing here, and unload it when you don’t need it anymore. 


    Some devices will moan when they have to deal with a lot of storage. And the process for looking up and storing these values is synchronous — it will delay any downstream processing of your application. So only use local storage sparingly, and make sure to isolate the lookups to the very specific instances when you need them.
  • Cookies
    1st party session cookies provide a means to store a small amount of data across browser instances that is impervious to hard-refresh. This is the de-facto solution for storing a server session token. You can store information in cookies that is only needed by the front end of your solution, but that is not an ideal usage for cookies. Cookie information is passed along with http requests. You should use local storage, rather than cookies, if your server has no use for the information.

    On the other hand, cookies are an excellent way to save a shopping cart, so the user does not lose their progress between sessions. This can save a round-trip from the frontend to the server, to render your cart. Though not as ephemeral as query string parameters, you must make sure your application consumes and mutates the cookie values appropriately. Otherwise, you might set yourself up for a confusing user experience. Make sure you set an appropriate TTL for your cookies.

    3rd party cookies are mostly useful for 3rd party analytics libraries. And you might have a situation where you want to share a preference across multiple domains/sites. The rise of private browsing and ad blockers confound this solution.

  • Database
    Though it is out of the scope of this article, data that is processed by the server may not need to be saved locally at all. If you have an authenticated user, maybe just insert the data into a database table. This avoids the browser round-trip, and limitations of cookies.

When To Consider React Built-In State Management

If your application is very simple, you might be able to stop here. Congrats! But if you are using React, it is likely that the simple solutions are not the only kit you need.

React provides “hooks” to use inside functional components. These methods give you convenient access to React built-in functionality. You can use these hooks to manage state in React, or even write your own. These hooks allow you to manipulate state (among other things) in a component. The state is specific to the component that it resides in. That might lead you to believe these hooks are only useful for a limited set of solutions; maintaining state for one view/modal/component. Hooks are indeed best-suited for those scenarios.

Let’s open up some key hooks and try to understand when to use them. And then, we will talk about ways to pass around these hooks and also extend them, or use 3rd party libraries to share state with many components.

Hooks to Manage State:

  • useState — Useful for managing string/value pairs.
    • GOOD FOR
      • Displaying modals and other toggles
      • Simple form field management
      • Counters and up/down value setters/getters
      • Extremely localized API data that is not needed elsewhere in the application
    • Consists of an array of a getter and setter to store data in an instance of a component
    • This getter returns the current value of the const
    • The setter mutates the value and enqueues a rerender of the component.
  • useReducer — Useful for managing more complex data and objects and multiple states with interdependencies. If your useState logic is getting too complicated, this might be a good choice. But useReducer could be a sign that you should consider a 3rd party tool like Redux.
    • GOOD FOR
      • Performing the same action or calculation on state from multiple points of entry
      • Updating multiple pieces of state at once, such as a form
      • The state is needed in multiple parts of the application (global)
    • Consists of the current state and a dispatch. 
    • The dispatch is typically an object that allows you to identify which type of function or calculation to perform when called. A typical format of a dispatch might be be {type: “SOME_STRING_TO_SWITCH_ON”, payload: “a value”}
    • More performant than multiple useState hooks
    • useReducer + context might be considered redundant and inferior to Redux. But if you are opposed to using a  If you are already using Redux in the application, you should probably reconsider your useReducer hooks.

Hooks to Monitor State.

  • useMemo  great for performing complex calculations that you do not want to perform on every rerender
  • useCallback  great for stabilizing functions to avoid recalculation inside a component.
  • useEffect — Useful for triggering a post-render function. Add a dependency to monitor and react to state changes. Often misused – it can easily set you up for infinite recursion.
  • useRef — Bind a DOM element to React by reference, without triggering rerender. Use for uncontrolled form elements.
  • Custom hook — If you plan to reuse your hook in multiple components, it is a good idea to create a custom hook. Since it documents itself, it might help with testing and sharing your code, too.

An interesting quirk of the event queue: because state changes inherently force a rerender, you might assume that multiple useState setter calls would result in a rerender infinite loop. After all, if you update the state of one const followed by another, it seems possible that this would cause instability. However, if you update multiple const states inside the same function,  the queue will identify and complete all of them, before a rerender happens. An example would be a “reset form” button. You can set all of your form field values to an empty string at once, rather than rerendering for each field. If the queue detects a duplicate value passed through a setter, it ignores the request and stops the loop.

However, if you update multiple const states inside the same function, the queue will identify and complete all of them, before a rerender happens. An example would be a “reset form” button. You can set all of your form field values to an empty string at once, rather than rerendering for each field. If the queue detects a duplicate value passed through a setter, it ignores the request and stops the loop.

It really helps to have a buddy to help when working with these hooks. If you would like to save time and headaches, check out our post about using GitHub Copilot.

Context / Provider

Rather than passing a reference to the state of an element from child to child to child (aka prop drilling), React allows you to define a Context that can be called regardless of inheritance. It serves as a rudimentary global state. Wrap elements in a Provider HOC to provide a direct reference to Context. 

If your state doesn’t change much, this is a perfectcccccly reasonable pattern, which doesn’t require more packages to depend on. But the penalty for using this pattern in its current state is that it forces rerender of all children, when any part of state is altered. There is currently no way to “subscribe” only dependent components to specific state changes without 3rd party libraries.

3rd Party Packages to Assist with State Management

If you are retrieving data from an API or managing forms, you will likely want to leverage tools to manage those tasks. While these tools are not directly responsible for managing state, know that they should be in your toolbox.

  • Axios or just plain Fetch — Manage API calls and responses. Fetch now has mature adoption and can work for you. However, I think the convenience of Axios makes it worth adding the dependency.

    Speaking of APIs,
    check out our post about building better APIs.
  • React-query (TanStack) — Despite the name, it does not actually fetch. But it is packed with shortcuts that make data fetching less painful. TanStack has its own Provider, but you can alternately use a state management library. Also check out competitor: SWR.
  • Any number of forms management libraries. Forms with validation and complex rules and controls can be very difficult to manage by hand. There are countless forms management tools to suit your needs.

And now, we move on to state management libraries. There are far too many to list, but here are several big players, listed by type. 

Atomic State Management 

Basically, these tools allow you to manage small, “atomic” pieces of state, rather than selecting nodes of a larger state.

  • Recoil

    • Atoms key/value pairs
    • Hooks attach variables to global state
    • Selector allows you to combine atoms
    • Uses RecoilRoot in place of Context an HOC to provide store data.
  • Jotai

    • Automatically handles state interdependencies, and allows you to get/set discrete pieces of state. Comes with an indirect state management tool, based on react-query (jotai-react-query), but you still need to use fetch or a fetch equivalent. The patterns here are very similar to built-in React state management, but without the need for context to manage state globally.
    • Atom  a discrete piece of state
    • useAtom setter
    • useAtomValue  just get the current value of an atom

Direct State Management Unidirectional 

Direct state management tools allow you to access state from your React application or from outside the application. Which can be very useful for unit testing, or extending your application data to other views.  

Unidirectional state management means that the state triggers changes in the UI, but a UI change does not automatically update state. You must specify changes to state, discreetly. While this does necessitate more code, it also provides a finer grained control of state updates.

  • Zustand

    • Define your data getters and setters in a custom hook.
    • Use a selector to monitor the specific part of the store that you are interested in, i.e.
    • const thisWidget = useWidgetHook((state) => state.widget); So this variable only updates when that particular node of the state is updated. The
    • selector is the primary reason to choose a direct state management tool: Rather than maintaining one global state everywhere, you can maintain
    • discrete parts of state where it makes sense to do so. With a monolithic state, you run the risk of constantly refreshing everything.
  • Redux
    • Packages: redux, react-redux, and the optional redux-toolkit
    • Central store with slices
    • “Reducers” define functions to perform on the store
    • Create a RootState and combine the slices here
    • Use selectors to retrieve the discrete piece of store that you need
    • Provider wraps your components in an HOC
    • You can mutate data directly inside the selector, if desired. This can be helpful to restructure API data into a format that the frontend needs.
    • Rather than extending the central functions, Redux encourages you to manipulate the store data for display separately.
    • Redux devtools plugin for Chrome is fantastic.

Direct state management Bidirectional

Bidirectional state management means that the state and UI are in sync. Think of a controlled component in a form -; if you update the text in the form, the state and the text that you see in the component are always in sync.

  • MobX
    “Observable” basically allows you to subscribe to a piece of the store. Try mobx-react-lite to wrap components for context. Very minimal boilerplate code.
  • Valtio
    State is held in a “proxy” and you can use a hook called useSnapshot to access the proxy. What makes a bi-directional state manager special is having the ability to mutate state without passing a setter. A proxy is a discrete state. So selectors are not needed. Derive allows you to combine data sources. Use in conjunction with fetch. A bit lighter duty than MobX.

So, Which Libraries Should I Use for My Project?

Many redditors will tell you (whether you want them to or not) — they have very strong opinions about which of these approaches you should NEVER use. But do take that with a grain of salt. None of the above are “bad choices” if you adopt them holistically and repeat sensible patterns. Often, the very best choice for your project will require a combination of “appropriate-ness” of the solutions or tools, familiarity, supportability, testability and internal preference. 

If you are developing alone, you have an opportunity to try something new. Do always follow best practices for choosing where and how to control state. If it doesn’t need to be global, keep it local.

If you are developing with a team, then this should be a group decision. As with a solo project, do always follow best practices for choosing where and how to control state. If it doesn’t need to be global, keep it local. 

I recommend Axios and Tanstack (react-query). You will probably get down to a decision between Zustand and Redux. It is a hard sell, maybe because of the glut of outdated sample code and help posts, but I think Redux is excellent for teams. The extra boilerplate provides documentation to the team, and the Redux Chrome plugin is very helpful for debugging and testing. And the core ability to mutate API responses into stores that are structured and named specifically for frontend use is very helpful.

If you need help navigating these decisions, our team of experts at GAP can guide you. Contact Us for a free consultation.

What does the not-too-distant future have to offer?

I sincerely hope that after years of 3rd party library dominance, we get a more robust state management solution built directly into React. The hooks mentioned above were a major upgrade. Anding a subscription-based context might help ween some of our projects off extra npm packages.

  • “use”  hook is coming to React. It could possibly be considered a replacement for React-query/SWR.
  • “Selector” for React context. It could possibly solve the “rerender everything” root issue with using native Context.
  • “signals” have been around for a bit. Think of it as a message queue for state changes. But adoption of signals appears to be poor. Maybe some evangelists will change that.
  • Legend State is a newer, very fast and very small state management solution that looks very promising. It uses an HOC for context, and mutates state values locally to aid with performance.

How will you proceed?

There are a lot of choices to make. And each project will have its own decision making process. GAP is here to help you navigate these decisions. 

Our team of engineers and business experts are here to partner with your organization to find sound solutions for your business needs. Contact Us for a free consultation.