Component UI State Is Never Enough

I’ve been following a certain React ‘Guru’ for some time now who proposes that because of the new additions to React (post version 16) and the implementation of better context and hooks we only need to use React for state and nothing else.

Depending on your perspective; this could be right.

However, some perspectives are not healthy ones. Especially ones that propose a UI framework can solve UI architectural issues.

Let’s discuss.

What Is State

In our UI Architecture Academy we teach a very simple approach to understanding state. We define state in any JavaScript UI app as;

  • Assignment – the use of the assignment operator will put a runtime value into memory regardless of tooling
  • Change Tracking – the callback mechanism that allows the update and broadcast of state in our UI app

Whether you use React state or something like Redux; state is always the same; but has different implementations (such as using the reducer pattern with observables in Redux).

Now that we have a simple understanding of state, the next question is… Where does state live?

It Depends

My favourite way to explain how state is misunderstood is to use a simple login token example;

// what our api data looks like
const dataFromServer = {token:'1234567', userId:'[email protected]'}

// step 1. either pass it or load it inside the component
// variation 1 :
<SpecificComponent props={dataFromServer} />
// variation 2 :
useEffect(() => {this.state = api.loadData()})

// step 2. show the log out button when the user has a token (is authenticated)
{this.propsOrState.token && <span onClick={this.deleteTokenFromSpecificComponentState()}>Log Out</span>}

This example maps our backend data directly into the React state or props. When we need to manipulate it, we know that the React context, hooks API or prop drilling can track the changes to it.

But this code is weak because;

  • It has directly coupled our API data into our UI framework components, meaning that if our API data changes it will break them
  • If we want to broadcast any changes to the token to other parts of our app, we now must couple unrelated components to this component

 We can solve code like this with one of two approaches.

Containers

Firstly, we could use some sort of state container (like Redux) to hold a copy of this state which we can then pass through our app. This solution helps us reduce coupling because now we simply bind to a centralised container, not to our specific components.

But we know from experience that these containers become pretty unwieldy over time because it becomes hard to rationalise the relationships between related concepts that we store in state.

React Itself

The aforementioned Guru (and many other people to be fair) now say we can get rid of our containers and go all the way and simply use React.

The proposed solution is to visualise React as a tree (which makes sense). Then it is to declare any state you need in the component of where it’s needed (like we already did).

But then we take a magic step when we want to share state between two components that need the same state but are unrelated. We do this by declaring the state in the component but then ‘lifting it up’ the tree.

What this means is that we just keep moving the state assignment and change tracking into a higher and higher component like this…

function AuthTokenDisplayer({token}) {
    return <span>{token}</span>
}
function LogoutButton({count, setToken}) {
    return <span onClick={this.props.setToken(null)}>Log Out</span>
}
function App() {
    const [token, setToken] = React.useState(null)
    api.loadData().then((dataFromServer) => {setToken(dataFromServer.token)})
        return (
            <div>
                <AuthTokenDisplayer token={token} />
                <LogoutButton count={count} onIncrementClick={increment} />
            </div>
        )
}

We can see now that we have a supposedly super elegant solution. But not so fast…

Whilst moving things up like this may seem like a good idea, we now have an ‘inheritance’ problem. When you make all parts of your app inherit like this, you force potentially unstable design downwards to parts of your architecture that may not need it. 

We can prove this claim with one simple trope; tell don’t ask.

Tell Don’t Ask

I am known for telling my students one thing. That I am NOT a Guru. The difference between me and a GURU is that I openly admit I get easily overwhelmed by the complexity of UI frameworks. What I like to be able to do is implement patterns and approaches which mean that we (the engineers) can be in control and not the UI framework.

In the examples above we are letting the UI framework dictate the architecture of our app because we are making all state become located ‘inside’ the framework files (like components).

We will run into problems with state like this because at each layer of our app the assignment of our state actually needs to change context. Why? Well if you think about the ‘token’ example and how we have been using it thus far we have accidently hidden two states in one because we have decided to show some markup dependent on whether we have a token available. Notice our markup;

{this.propsOrState.token && <span onClick={this.deleteTokenFromSpecificComponentState()}>Log Out</span>}

Is actually asking our state ‘should I show this’ by using a Boolean flag.

Code like this does not scale well, because when we ask questions like this in our markup, we actually skip over what the code is really doing. We obscure the intent. This will make our view overly complicated and with complex component trees, we WILL get lost.

What we really need to do is proxy this ‘question’ and explicitly set our state to be completely declarative about what the control should show.

In other words, we want to ‘transform’ the state of token into something more meaningful to be consumed later on.

What we really want; is to hide the token from our tree (which apart from being more secure will lead to less ambiguity later down the line as it’s more encapsulated). And then simply map it to something which tells our component what to do.

In essence we need a couple of steps during our state assignment and change tracking. Consider the following example;

// what our api data looks like
const dataFromServer = {token:'1234567', userId:'[email protected]'}

// step 1. centralise our 'domain' model
someAuthObject.uiToken = dataFromServer.token
someAuthObject.userLoggedIn = !!dataFromServer.token

// step 2. expose only what we need to our 'view' model
someViewModelObject.showLogout = someAuthObject.userLoggedIn

// step 3. either pass it or load it inside the component
// variation 1 :
<Component viewModel={someViewModelObject} />
// variation 2 :
useEffect(() => {this.state = presenter.loadViewModel()})

// step 4. markup is now being told what to do and can remain dumb
{this.propsOrState.showLogout && <span onClick={this.updateDomain(null)}>Log Out</span>}

What we have done here is take some steps (1,2) to actually do this transformation.

Now the objects someAuthObject and someViewModelObject are in fact both types of state. But the latter is an interpretation of the former. This means that we actually need to implement the viewModel state as a ‘shadow’ state of the domain state and have it loaded with variables that explicitly tell our React component what they should be doing, instead of letting them ask!

Critically we are not implementing this state inside the component using its change tracking, we do it outside the component and use our own change tracking (between step 1 and 4).

SO In our academy we teach students how to do this simply using a Reactive Architecture which is independent from React because…

Separation of Concerns

When we begin to pay more attention to the subtleties of actually having good state architecture by realising simple design tropes (tell don’t ask), we will begin naturally creating better code structure and the knock-on effect will be to … separate concerns.

Thus, we don’t want to create state globally inside either a container or React (or any other UI framework for that matter).

What we want is to logically lay our state out in our UI architecture to show and hide information and to contextually transform information based on the requirement of each component.

By hiding and transforming data like this we will create a more predictable and scalable UI architecture because we reduce coupling and complexity by just using what React is designed for; generating views!

Conclusion

This article explores how using containers is not a great idea for managing state in a React app because they become God objects. It then goes into a proposed solution using React Hooks and prop drilling to manage state.

The problem with both approaches is that they leave little room for transformation and re-contextualisation of what you want from your state.

Often you will simply want a viewModel to shadow your state and transform deeper domain concepts into things which ‘tell’ the view what to do so that your markup remains simple.

For this reason, we really need to separate concerns of state management and keep them isolated and allow this transformation to happen outside either a container or React (or any UI framework).

By doing this our code is more likely to scale; predominantly because our view will be much simpler, and this is really what UI frameworks like React should be doing instead of managing complex state arrangements!


Author: Pete Heard

Pete is the founder of Logic Room. He designs the companies core material for JavaScript architecture, test automation and software team leadership.

LinkedIn

Want to learn 5 critical lessons for framework-agnostic JavaScript UI app design and structure?

Download our 28-page book exploring big picture ideas to help you create better architecture with any UI framework (inc. React, Angular or Vue).

Download
Close

50% Complete

Book Trial

Please fill out your details if you would like to trial our system.