React: State of the State

Flux, Redux, shared state, centralized store

Matthew Tyson
Nov 19 · 9 min read
Complex Looking Diagram :)

React is a component-based front-end library whose basics are easy to grasp. One of the key areas of complexity is state management.

State represents the disposition of the data in a program. In a UI, that means both the business data and the condition of the interface elements. In React, there are two primary places where state will reside: in components, or in a central store.

There are a variety of techniques for dealing with state, including render props, the Context API and Redux. Most apps will use a blend of options.


Pure Components and State

It is possible to build stateless, functional components in React: so-called pure components. In this case, we could look at the components as agents of manifestation of the centralized state.

The state still exists somewhere. If not in the components, then in a centralized state object, whose information is reflected in the components.


Component State

Traditionally, every component can have its own state, which is to say, variables representing what content they hold. This is not just analogous, but exactly equivalent to state in an object, in OOP.

Component state works well in that it encapsulates the data in the component, so that when reasoning about the component behavior, you can think mostly about the component itself, without reference to external actors.

It is an easy-to-understand model of the flow of state.

Component state works great, until an application’s UI becomes complex enough that components must interact and share data.

There is a clean mechanism for passing data from a parent component to a child: you can use props. Props are a basic feature of React and serve the purpose well.

The idea of daisy-chaining props from parent to child looks something like figure 1.

Fig. 1: Daisy-chain props

In this model, what happens is that the different views (as large apps will decompose their views into different root components) is the root component end up acting as a centralized state store.

The state of the root parent then distributes state to children, in a similar fashion to the centralized state options we’ll see in a moment. So much for communicating state from parents down the hierarchy to children.


Child to Parent

The first wrinkle arises when you need to communicate from children up to parents.

The official React docs have render props as existing to:

“…share the state or behavior that one component encapsulates to other components that need that same state.”

A render prop uses a function instead of a concrete variable prop. (It acts something like a mixin, if you are familiar with that concept.)

The essence of a render prop is that you’ll pass in a function to the receiving component (the child component, that is), and then inside the component, call that function.

The key mental acrobatic here is this: you pass a function from the parent to the child and the child calls it, thereby enabling the child to pass data back to the parent by fulfilling any parameters to that function call.

A classic example of functional programming, btw.

Here’s a JS fiddle with a simple example, this example descends from the example in the NoNonsense Intro to React — if you already know React basics, you’ll understand, if not, take seven minutes to get up to speed.

So, the essential line is here:

{this.props.renderProp(‘Hello’)}

In the child component, where the function is called. Notice that we pass back an argument, hello, to the function. Right there is the capacity to communicate from child to parent.

Of course, in a real app, you could use whatever real data you want, including child component state, as the parameter to pass back up to the parent.

So — we have a solution for passing state from child to parent in the render prop pattern.

We could visualize the render prop concept as in figure 2.

Fig. 2: Daisy-chaining render props

Not only does this offer a clean mechanism for child-parent communication, it enables the creation of reusable components (and higher-order components, or components that return components).

But on the inter-component communication front, this is fairly limited. If state needs to be shared across multiple, or distant, components, it becomes untenable.

One thing we don’t want to do is start daisy-chaining a bunch of render props across distant components to achieve state-sharing. This is brittle and just plain ugly.


Context API

Another candidate for dealing with shared state is the Context API.

The Context API is explicitly designed to handle cases where some state is required by a multitude of components.

In this case, the components each get access to the context, instead of relating the components together to share state as in the props manner.

This notion looks like figure 3.

Fig. 3: Context API

So, that looks a lot more like what we want for dealing with shared state across a complex component tree.

The Context API is pretty straightforward. To create a default context:

const LanguageContext = React.createContext(‘english’);

Then, you can modify the value in a root or low-level component like so:

class Parent extends React.Component {
render() {
return (
<LanguageContext.Provider value="tibetan">
<Child />
</LanguageContext.Provider>
);
}
}

Once the low-level component introduces the context into the tree, any child components have access to it, regardless of how deeply nested they are.

You can access the value like so:

static lang = LanguageContext;
render() {
return <Button language={this.lang} />;
}

(Note: This is the newer Context API).

So, this is a pretty clean and easy way to hand simple data around the component tree. However, it doesn’t scale very well and it doesn’t work very nicely with dynamic data. It is more suited to a simple data type that is static.

You don’t want to try to manage complex data structures that are being modified by API calls and user interaction in the context.

For more options, let’s look at the much-discussed…


Redux

Redux descends from Flux. Flux is an idea.

The idea is that you create a centralized store and then control access to it in specific ways. At a high-level, this looks similar to the way the Context API works.

The core restriction on changing Redux state is that state is only modifiable by submitting an Action to the store. (This is analogous to the requirement that component state only be modified via setState()).

An action defines a discrete kind of modification that’s going to happen to the state. A dispatcher is responsible for sending the action to the store, where it is caught by a reducer, responsible for applying the action to the state.

This is essentially an event-based architecture, analogous to the Command design pattern (where actions take the role of the command).

We could look at figure 4.

Fig. 4: Redux

But… why?

One thing it does do is overcome the limitations of the Context API. The store is capable of handling complex data structures and relating them to complex views. It also, via Redux middleware, can interact with a back-end API.

But again we ask: “Why is it structured like this?”

First up, and I want to really hammer this home, there is a key reason for the development of Redux; a specific problem it solves.

Redux solves a specific problem: multiple-actor state change race conditions

The problem it solves is race conditions in state that are being modified by multiple actors.

By enforcing the submission of actions to the store, you avoid the (classic, canonical coding) problem of:

  • Component A looks at the state, gets a value, say: 100.
  • Component B also looks, also gets 100.
  • Component A modifies the value by adding 1, making it 101.
  • Component B now modifies its value by adding 1, making it 101 again.

Where the value should end up as 102.

Now, you can see that such a race condition might arise as multiple components compete for state modification, but that is actually very unlikely. More problematic is interleaving of user interaction and API responses arriving at the UI.

However, and let’s make a big point about this one again, the real repeatable problem arises as the WebSocket (or other server push) competes directly for the same state as user actions.

With Facebook, this was the unread message counter, if legend is to be believed.

Anyway, let’s make a figure just for this, because it really is central. Figure 5.

Fig. 5: Redux with WebSockets

That is the iron-clad reason to use Redux: you are dealing with race conditions.

But let’s back up for a minute. We want to avoid some serious wrong-thinking.

First of all, don’t use central global state unless you have to. You want to encapsulate your component state and leave it at that, if it satisfies your requirements.

Second in this train of thought: don’t use a complex central store to manage state unless there’s a good reason.

“The simplest solution is almost always the best.” — Occam’s Razor

However, there are a couple more benefits offered by Redux that might motivate you.

Most compelling (IMHO) after the race condition, is the idea that you are creating a more understandable and easy-to-reason about application state.

In short: all the state is concentrated in one place, and all the possible mutations to it are enumerated explicitly in the actions and accounted for in the dispatchers.

This is an interesting idea.

I don’t buy it hook, line, and sinker, but I think it could make sense for a large-scale application, with some caveats.

For one thing, you are adding complexity to the way state is managed, so we’ll have to overcome that with added understandability.

For another, the complexity of the app state (and possibly the complexity of the team working on the application, as well) should merit the adoption.

I’ll hasten to add that I think most apps, especially agile, fast-moving fledgling apps, should shy away from (read: completely avoid) the extra overhead of dealing with Redux to manage state.

Please fill in the following blank if you are using it: “We are using Redux to __________________.”

Now that’s out of the way, let’s look at a couple of other benefits offered by Redux:

  • Replayable state.
  • Undo history.

Replayable state means that you have the ability to send the complete app state for debug purposes, essentially recreating the state that was in place when an error occurred.

My feeling is that probably sounds nicer and works better as an idea than it always does in practice (unless you’ve got an army of devs to throw at it, of course).

Undo history relies on the fact that you can maintain a history and revert actions (exactly like the classic command pattern we mentioned earlier).

Finally, there is one more compelling reason to use Redux when faced with centralized state: it is well-understood and popular.

Wait, wait — I don’t mean you should use it because it’s popular. I mean, it is in its favor than many folks know about it and understand it.

So you can balance that in its favor, against the added complexity, and the curious realization that there may actually not be a simpler, cleaner, central state store solution you can throw at React.


Hooks

Now, let’s consider an alternative, using Hooks as described in this article: State Management With React Hooks.

It seems like an interesting idea, however, on balance, we are again adding complexity instead of reducing it.

I think it’s a valuable experiment the author performs. Perhaps the npm package he is working on will grow to provide me with what I’m looking for, which is…

Goldilocks

I’m still looking for my “just right” solution for state management in React.

It feels like there should be a simple mechanism that exists in between the Context API and Redux. Something that allows for a centralized state, can handle complex structures, and service API interaction, with a minimum of complexity.

I wonder if, in the final analysis, it is better, meaning, more understandable and manageable, to have a clean centralized state or a component-oriented state, or a blend?

It probably becomes a question, like most things in software, of what the specific requirements are. What are you building, how complex, how large is the team, how much API interaction, and so on.

Better Programming

Advice for programmers.

Matthew Tyson

Written by

www.darkhorse.tech

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade