BLOG
Exploring State Management in React
State management in React was originally a monolithic beast that needed to be tamed in Classical React. The introduction of Hooks introduced a paradigm shift that ushered in the age of micro state management in React. Now we were afforded a lightweight solution for more nimbly handling state in our apps.
What is State Management in React?
One of the seminal contributors to this field of the React ecosystem has been Daishi Kato. He has produced some of the most well known and eagerly adopted libraries including Zustand, Jotai and Valtio. I was so excited to find out that Daishi had published a book outlining the ideas that inspired these libraries. I hope you’ll join me as I share what I have learned in this series of posts. Without further ado let’s dive right in with Chapter 1.
The Evolution of State Management in React
Classical React brought about monolithic state libraries which covered all the state app-wide. The main negative to this approach is that it wasn’t customized to the needs of specific facets of the app and often contained additional features that went unused. React Hooks allowed bespoke micro state management for use cases such as form state, server cache state, and navigation state.
Before we get started let’s take a quick look at a couple of definitions for state related terms that will come up often in our discussion.
- State refers to the data that backs our user interface
- Local state is state within a component or small tree of components. React provides us hooks for dealing with this state.
- Global state is state shared between several components and has no built-in solution with React. We will be exploring ways to fill in this missing functionality using both built-in hooks and custom ones.
How to Manage State Across Functions in Hooks
The hooks that will serve as our primitives to build out a micro state library are useState, useReducer, & useEffect. Let’s explore these all important units for constructing our global state. Some of this will no doubt be a review for grizzled React veterans but it’s good to make sure we’re building our knowledge on a firm understanding of these fundamental constituents of state.
useState & useReducer are the building blocks for local state and will be repurposed for building out global state.
useEffect handles logic related to side effects. Let’s take a slight detour to understand this term more fully. In the functional programming paradigm, pure functions are defined as functions that do two things:
1.) Rely solely on inputs (arguments) and are not affected by the environment outside the function in any other way. Another way of saying this is that pure functions are deterministic, which means that the same inputs will produce the same outputs.
2.) Only affect the environment by their outputs (return values) and do not cause any other changes. In other words, they do not produce side effects.
Examples of side effects include network requests, file access, logging & Web API access such as the Document Object Model (DOM). This functionality is vital for the app but must be restricted to the useEffect hook because of how the phases in React behave.
The Phases of React
React has two phases, the render phase where the changes to be made to the DOM are calculated by diffing the current render against the previous render, and the commit phase where those changes are actually made to the DOM. Whereas pure functions are deterministic, functions with side effects are decidedly not, which means running the same function multiple times may produce different output on each call.
The render phase can be called multiple times so it’s especially important that we call side effects outside of this phase of the component lifecycle and only call them inside the useEffect hook. Not doing this can lead to bugs that are very hard to track down, therefore the old adage “an ounce of prevention is worth a pound of cure” applies here.
This makes for a good segue into two rules we need to follow to make sure we avoid difficult to track down bugs:
1.) do NOT mutate existing state or ref objects which would lead to too many or too few re-renders. This relates to JavaScript’s handling of primitive values differently than objects. Two objects with identical content are treated as different values if they point to different references. Also an object reference that is mutated is not picked up as changed hence the need to treat the object immutably by using the spread operator. This ensures we make React aware of changes to the state that involves object references.
2.) make hook functions & component functions pure so that they are deterministic (behave predictably/consistently when called repeatedly on re-renders).
Flexibly Decoupling Component Logic with Hooks
Custom hooks allow us to share logic between components without the onerous requirements of a specific nesting hierarchy as was dictated by Classical React which led to the aptly named wrapper hell of days past. In a boon to developer experience we now take for advantage, custom hooks make available to us a horizontal inheritance of sorts that allows us to cleanly demarcate shared logic in functions. Custom hooks will be our primary go-to for abstracting global stateful logic to live independently of our components.
That wraps up our initial look at the introduction to micro state management. In the next post we’ll compare & contrast the useState and useReducer hooks.