One of the most challenging issues found in dealing with modern web interfaces is that of state management. In this post, we’ll be looking at at HMVC architecture inspired potential solution for using React’s internal state management functionality to give greater control and clarity in an application.
This post requires a good working knowledge of React to understand. Also, Redux and Flux are both great concepts. This simply takes their ideas and ensures you don’t need any specific implementation to get similar functionality.
Outlining the Problem
React allows for an element of ease with the render by having state as being a declarative thing, and then allowing the UI rendered to follow from whatever the state currently is. This means that the currently returned output is simply a function of the state of the application at the time.
The challenge is that a React application can be composed of dozens of modules on the page at the same time, each with its own current state. One popular solution to this is Redux, which replicates a lot of the state management from React and adds a few nice bells and whistles solves this by having the entire state held in a single place, and exposing what’s required to components that need that data. Individual functions can mutate that single state object, meaning that the state of the application is always complete in a single place.
Personally though, I have an issue with this – React already includes tooling to achieve similar functionality; it just doesn’t do it all for you. So here’s a guide to getting some interesting things from React without the need for Redux.
When it comes to creating an application, we’ll have a single component at the top which will render everything else, calling in various different components to render different parts of the UI as required. This means we have the potential for a single master parent element at the top of our application.
This single part, which we’ll call the Conductor, will hold all the *multiply-applicable* state required by the application. By this I mean all the data that is relevant to anything more than one component. We’ll also be referring to the idea of component levels, as we progress deeper into an application. As a result, the Conductor is level 0 (here-on referred to as L0).
Child components of the Conductor, or of other children then receive that data in the form of props. The reason we do this is simple – props are immutable. We must never mutate them directly. Instead, we must pass an instruction to the Conductor that we want that value to be changed, and allow it to make the change. Remember also that **functions can be props too**. That means you can pass tools to manipulate state higher in your application to child components. Also, the **main aim of a component is to allow communication** between UI elements. They *may* also render DOM output, but we don’t require that that be the case.
We now have a tree-type structure, with the base of it being the conductor and data flowing through. Importantly though, the Conductor doesn’t need to hold *all* of the application state. Let’s imagine that at L2 we have two sibling components which need to each know the current value of the other. Whilst we could put this piece of state at L0 in the Conductor, it would be more efficient to simply have the state value at L1 in their parent component, and pass it down as a prop. On the other hand, if the components aren’t siblings, then the nearest element which knows about each must be at L0. We therefore hold the state value at L0 and pass it down through L1 to the components at L2.
Obviously then any data that only resides in a single component can then be state held in that single component.
One of the nice things you get with Redux is the concept of time travel through your application’s state – having all the state existing in one global store means you can log the entire state each time it mutates and then traverse through the various iterations of it to see what happened when and where. However, again we can achieve this goal without the need for Redux.
Since the current state of the application can be obtained by traversing from L0 down to L(n) and asking each component for anything it has in state, we can quickly and easily recurse through everything in the application to obtain an encapsulated version of the state of any part of the system. We simply collect the state from La down to L(n), where L(a) is the top level we’re interested in, and L(n) is the deepest child. That state data can be represented as a JSON object and logged somewhere. Then a recreation of the entire state of the application can be found by treating the log as an audit trail. We take the last known state for every level rendered, starting with L0 and moving down.
The only other data we need to log is when a component mounts of unmounts, so we know what should be present on the screen.
For an example, let’s imagine we’re working with an simple TODO app. At L0 we have the Conductor, L1 contains the components Input and List, with List having child components at L2 of Item. Item is instantiated many times with different values. Conductor has the current values for the list as state, which it passes as a prop to List in the form of an array of objects. List iterates through that array and passes each object to an Item component. Neither List nor Item have any state. Input has its current input as state, but receives no props.
To get the entire state of this application, we’d start with the Conductor. It holds the state which gives all the values displayed by List using Items. Its last known state gives us a timestamp to start at. We’d also get the last known state of Input since that time, which will give the last piece of information required to construct the view state of the app.
Taking this a step further, let’s say we’ve got another component. It’s an autocomplete, which lists types of item to be added. It has as a prop an array of all the available options, and currentValue as state, which is what the user is typing. We can then regex currentValue against the values supplied as a prop to test which should be shown. The items will then be shown as TypeItem components which will be mounted or not based on what’s required by Type.
If we now added the Type component at L2, as a child of Input, we can still perform a similar operation, looking at the last given states of the Conductor, Input and Type to reconstruct the app. We can also see what TypeItem components should be mounted as a child of Type, as those will follow from the state of Type. We wouldn’t need to log the mount/unmount of those, although of course we could if we wished to.
Fortunately, React has the componentDidMount, componentWillUpdate and componentWillUnmount methods. Using these, we can check every time we may need to log new state (componentWillUpdate), and mounting and unmounting operations as needed. The only other pieces of data we need to tie it all together are a user session and timestamp.
With these pieces of data, we can trace forward and back through every action a user took at any point, on any screen. We simply pull the states from the last time L0 updated to the final timestamp required, and apply the last state given for every child component.
Simplicity vs DRY
There’s two obvious drawbacks to this method of React application, one when we compare it to Flux, and one when we compare it to Redux.
Compared to Flux, because we end up passing a lot of data and control functions down as props, a component can hold data that it doesn’t actually use itself. Instead, it has it to pass it to deeper components. It’d be neater if you could be more declarative as to what each component required and the actions each should take. This would lead to a more Flux-like architecture though, and comes with the downside that state necessarily becomes split over different components, leading to more complex reconciliation required between components.
The other downside is that state isn’t entirely represented as a single monolithic object like it would be with Redux. Instead, it’s held exactly has high-up in the application as it needs to be. Logging is simpler with Redux as you’re only ever dealing with a single object. However, if you want to log changes, you can end up logging vastly more data than is required (as you’re logging the entire application state, rather than the changes).
Neither of these three approaches is perfect – each has pros and cons. Instead, think about what sort of functionality you want to create, and select the approach that best fits that specific use case.