Pull to refresh

React Global State Management: A Brief History and a Quick Guide

Level of difficultyEasy
Reading time9 min
Views1.2K

If you’re a React developer, you know how important state management is. State is the data that powers your UI, making it interactive and dynamic. But managing state in React can be tricky, especially when you have to share it across multiple components or deal with complex and asynchronous logic.

That’s why over the years, React developers have come up with various solutions for state management, each with its own advantages and disadvantages. In this article, we’ll take a look at some of the most popular ones and how they evolved. We’ll also review some of the current state-management libraries and how to choose the best one for your app.

PROBLEM

React is based on the idea of data flow: data flows from the top-level component (or data model) to the lower-level components (or UI) through props. The UI is a function of the data, meaning that any change in the data should trigger a re-render of the UI.

But not all data is local to a single component. Some data needs to be shared across multiple components, such as user information, theme preferences, authentication status, etc. This data is called global state, and it poses some challenges for React developers.

Local vs Global State

Local state is the state that belongs to a specific component and does not affect other components. For example, a form input value, a toggle switch state, or a counter value are local states. You can use the built-in useState hook to manage local state in functional components, or this.state and this.setState in class components.

Global state is the state that is shared across multiple components and affects their behavior or appearance. For example, a user profile, a shopping cart, or a theme mode are global states. You cannot use local state hooks or methods to manage global state, because they are scoped to the component where they are defined.

To illustrate the difference between local and global state, consider a simple example of a dark mode toggle. The toggle switch itself has a local state: whether it is on or off. But the theme mode that it controls is a global state: it affects the appearance of the entire app.

Why do we need global state management?

Managing global state in React can be tricky for several reasons:

  • Prop drilling: If you want to pass global state from a top-level component to a lower-level component, you have to pass it through every intermediate component as props, even if they don’t use it. This creates a lot of boilerplate code and makes your component tree harder to maintain and refactor.

  • Context loss: If you use different rendering methods or libraries in your app, such as portals, modals, popovers, or suspense, you may lose access to the global state that is provided by context or other solutions. This can cause unexpected bugs or inconsistencies in your UI.

  • Performance issues: If you use context or other solutions that rely on React’s built-in re-render mechanism, you may encounter performance issues due to unnecessary re-renders. For example, if you have a large list of items that depend on a global state value, every change in that value will trigger a re-render of the entire list, even if only one item is affected.

  • Complexity: As your app grows in size and features, your global state may become more complex and harder to manage. You may need to implement logic for updating, deriving, or validating your state, as well as handling side effects such as API calls or localStorage operations. You may also need to debug or test your state changes and track their history.

To overcome these challenges, React developers have come up with various solutions for state management over time. Let’s take a look at some of them and how they evolved.

THE EARLY DAYS: PROP DRILLING AND REACT CONTEXT

The simplest way to share global state across components is to use props. You can define your global state in a top-level component (such as App) and pass it down to lower-level components as props. You can also pass callback functions as props to allow lower-level components to update the global state.

This approach works fine for small apps with simple data structures and shallow component trees. But it has some drawbacks:

  • once again - prop drilling: As mentioned before, prop drilling creates a lot of boilerplate code and makes your component tree harder to maintain and refactor. It also makes your components less reusable and more coupled to each other.

  • Single source of truth: If you have multiple top-level components that need to share the same global state, you have to synchronize them manually or use a higher-order component (HOC) to wrap them. This can introduce bugs or inconsistencies if you forget to update one of them or if they have different lifecycles.

To avoid prop drilling, React introduced the Context API in version 16.3. Context allows you to create a global state object and provide it to any component that needs it, without passing it through props. You can also use the useContext hook to access the context value in functional components, or the contextType property or the Context.Consumer component in class components.

Using context solves the prop drilling problem but also has some drawbacks:

  • No debugging tools: Complex state management is a difficult thing to debug. Some tools to see and trace mutations are mandatory. In the case of React Context you have to rely on good old console.log to oversee changes in the state and check the current state.

  • Performance issues: Context relies on React’s built-in re-render mechanism, which means that any change in the context value will trigger a re-render of all components that consume it, regardless of whether they actually use the changed value or not. This can cause performance issues if you have a large number of components or a complex state object.

  • Context loss: As mentioned before, context may not work well with different rendering methods or libraries, such as portals, modals, popovers, or suspense. You may lose access to the context value or get stale values in some cases.

  • Complexity: Context does not provide any built-in logic for updating, deriving, or validating your state, nor for handling side effects such as API calls or localStorage operations. You have to implement these yourself or use custom hooks or HOCs to wrap your context providers and consumers. You also have to debug or test your state changes and track their history manually.

THE RISE OF REDUX

Redux is a popular state management library inspired by Flux, an architecture for managing data flow in React apps. Flux is based on the idea of unidirectional data flow: data flows from actions to stores to views.

Actions are plain objects that describe what happened in the app. Stores are objects that hold the app’s state and logic. Views are React components that render the UI based on the state from the stores.

Redux is a canonical implementation of Flux that simplifies and enhances its concepts. Redux is based on three core principles:

  • Single source of truth: The global state of your app is stored in an object called the store, which can only be modified by dispatching actions.

  • State is read-only: The only way to change the state is to emit an action, which is a plain object that describes what happened.

  • Changes are made with pure functions: To specify how the state changes in response to an action, you write pure functions called reducers, which take the previous state and an action and return the next state.

  • Predictability: Since the state is immutable and changes are made with pure functions, you can easily predict how your app will behave and debug or test your state changes.

  • Performance: Redux uses shallow equality checks to optimize re-renders based on state changes. You can also use 3rd party libraries to optimize state data selectors to derive and memorize computed values from the state.

  • DevTools: Redux has a powerful set of dev tools that allow you to inspect, track, and manipulate your state and actions. You can also use time-travel debugging to rewind and replay your app’s history.

  • Ecosystem: Redux has a large and active community that provides many libraries and tools for enhancing its functionality and integrating it with other technologies.

However, Redux also has some drawbacks, such as:

  • Boilerplate: Redux requires a lot of boilerplate code to set up and use. You have to define actions, action creators, reducers, selectors, store, middleware, etc. You also have to use the connect HOC (Higher Order Component) or the useSelector and useDispatch hooks to connect your components to the store.

  • Learning curve: Redux has a steep learning curve and requires familiarity with some advanced concepts such as immutability, pure functions, middleware, etc. It also has its own terminology and conventions that may be confusing for beginners.

  • Overkill: Redux may be overkill for small apps or simple data structures that do not need its features and complexity. It may also introduce unnecessary re-renders if you do not optimize your selectors or use memorization.

STATE-MANAGEMENT LIBRARIES

Redux is not the only solution for global state management in React. There are many other libraries that offer different approaches and features. Here are some of them:

MobX

MobX is a state-management library that uses observable data structures and reactive programming principles to make state management simple and scalable. MobX allows you to write declarative and reactive code without worrying about state management details. You can use any JavaScript data structure or class as your state and MobX will make it observable and reactive for you.

With MobX, you can create observable values that MobX can track for changes. You can make any React component an observer that reacts to observable values by re-rendering. You can also mark functions as selectors or actions to manipulate other atoms.

Jotai

Jotai is a state-management library that uses atoms to manage both global and local state. Atoms are pieces of state that can be read and written from any component. Atoms can also depend on other atoms or async functions, creating a data-flow graph. Jotai allows you to create and access atoms with simple hooks.

With Jotai, you can create atoms that store any value, such as primitives, objects, arrays, etc. You can also create derived atoms that compute values based on other atoms or async functions. You can use the useAtom hook to read and write atom values in any component. You can also use other hooks such as useUpdateAtom, useAtomValue, useAtomCallback, etc. to customize your atom interactions.

Recoil

Recoil is a state-management library that uses atoms and selectors to manage global and derived state. Atoms are units of state that can be shared and updated by any component. Selectors are pure functions that compute derived state based on atoms or other selectors. Recoil allows you to create and access atoms and selectors with simple hooks.

With Recoil, you can create atoms that store any value, such as primitives, objects, arrays, etc. You can also create selectors that compute values based on atoms or other selectors or async functions. You can use the useRecoilState hook to read and write atom values in any component. You can also use other hooks such as useRecoilValue, useSetRecoilState, useRecoilCallback, etc. to customize your atom and selector interactions.

Easy-peasy

Easy-peasy is a state-management library that uses Redux under the hood but simplifies its API and usage. Easy- peasy allows you to create a global store with an initial state object and a set of actions and reducers. You can also use thunks, selectors, listeners, persist, etc. to enhance your store functionality.

With Easy-peasy, you can create a store model that defines your state shape and logic. You can use actions to update your state synchronously or asynchronously. You can use selectors to derive values from your state or other selectors. You can use the useStoreState and useStoreActions hooks to access your state and actions in any component. You can also use other hooks such as useStoreDispatch, useStoreRehydrated, useStore, etc. to customize your store interactions.

Zustand

Zustand is a small, fast and scalable state-management solution using simplified flux principles. It has a comfy API based on hooks, isn’t boilerplatey or opinionated. Zustand core can be imported and used without the React dependency. Zustand allows you to create a store with an initial state and some actions to mutate the state. You can then use the store as a custom hook in your components and select the state slices you need. Zustand will re-render your components only when the selected state changes. Zustand is similar to Redux in terms of data flow, but it has less boilerplate and does not require any middleware or context providers. You can use hooks such as useStore, useStoreState, useStoreActions, useStoreDispatch, useStoreRehydrated, etc. to access and manipulate your store.

Xstate

Xstate is a state management library for creating, interpreting, and executing finite state machines and statecharts. Xstate provides a declarative way to model the logic and behavior of your application using state machines, which can handle complex scenarios with ease. Xstate allows you to define states, transitions, actions, guards, and effects for your state machine using a JSON-like syntax. You can then use the Xstate React hooks to connect your state machine to your components and access the current state, context, events, and service. Xstate helps you manage state complexity by enforcing separation of concerns, modularity, and testability. You can use hooks such as useMachine, useService, useActor, useSelector, useInterpret, etc. to interact with your state machine.

Storeon

Storeon is a tiny (173 bytes) state management library that uses events to update and access global state. Storeon allows you to create a store with an initial state object and a set of modules that handle events. You can also use hooks, middleware, devtools, etc. to enhance your store functionality.

With Storeon, you can create modules that define your state shape and logic. You can use events to update your state synchronously or asynchronously. You can use the useStoreon hook to access your state and dispatch events in any component. You can also use other hooks such as useSubscription, useEvent, useLogger, etc. to customize your store interactions.

CONCLUSION

State management is an essential part of React development. It has changed a lot over time, from prop drilling to Redux to Hooks to new solutions. Each way has its pros and cons, and there is no one-size-fits-all solution.

The best way to choose a state-management solution for your app depends on many factors, such as:

  • Your app’s size and data model.

  • The availability and versatility of the state management debugging tool set

  • Your app’s business logic complexity

  • The type and frequency of your data updates

  • Your preferred learning curve and developer experience

For small applications or isolated components, local component state or Context API may be sufficient.

Remember that state management is a means to an end, not an end in itself, whatever solution you choose. The ultimate goal is to create amazing web applications that delight your users and solve their problems.

Happy coding!

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+3
Comments0

Articles