How To Upgrade Your React UI Architecture

architecture react Feb 23, 2021

The React documentation tells us that React is a simple library to build encapsulated components that manage their own state. Then compose them to make complex UIs. It's unique use of closures and scopes to manage it's native templating language (JSX), automatically pushes the engineer to move towards a declarative approach to building component markup. Either in the 'render' or 'return' section of class or functional components.

We are going to explore why you should guard yourself against Reacts 'default architecture' because it can introduce architectural brittleness which will stop you from scaling your code.

Religion

As an engineer you should be sceptical of frameworks. This is because all frameworks start with the same basic premise (to solve the problem of the framework before them) and give you the same basic argument (that their way is the best).

The React team wanted to improve on earlier versions of web frameworks by introducing the concept of closures and scopes directly into the component with the use of JSX. This was purported to give us a cleaner API of each component; making views fully 'reactive' which could respond to changes to their internal data model (the virtual DOM). Instead of using a data model 'binding' approach as came in older frameworks such as AngularJS and Backbone.

The resulting benefit React gives is to allow it to 'react' to real-time changes in JavaScript, this enables component design where the engineer can follow a declarative instead of an imperative programming model.

Declarative programming can often be more effective and predictable when building complex structures because we can 'inline' every assumption. This makes it easier to predict what will happen (it becomes more predictable) - for example Sql is often much more predictable than NoSql for the same reason.

You can see one of the original talks about why React was created here.

By making components declarative React should give us separation of concerns (SOC) where views and logic are independant because now behaviour and data are separate. Instead what we ended up with was a move towards both the data and the behaviour (buried in markup) becoming tightly coupled...

Render or Return

Whether we use Reacts class component based approach or the functional component based approach they both come down to the same basic use; returning markup (JSX). They both take one input, props.

Props are the single point of entry into a component. They work by allowing the declarative JSX to 'pump in' whatever it needs. It builds a tree of execution which flows downwards from highest render or return function to lowest. At each step React reads the props then decides how to render it's children.

This article will use functional components as an example but the same argument also applies to class components.

Consider a component that needs to add two values by adding together two inputs. We would firstly declare this component as follows (using pseudo-javascript-code):

const totalComponent = (props) => {return (<div>{props.left + props.right}</div>)}

Then, in order to instantiate the above component (in JSX) we simply put something like this into markup of the component.

<TotalComponent left={1} right={2} />

But...the last code sample I have given you has one glaring problem. Can you see what it is?

No?

Let me ask you another question, how many arguments should a function have for good architecture?

How many arguments should we have if we want the least amount of coupling in an app?

The answer is... none.

Niladic

The moment we begin putting inputs into a function as we have done in both the declaration and instantiation steps above is the moment we begin creating more coupling. This is because anything passed into a component is simply a proxy for passing arguments into a function (the underlying implementation).

We should try and build programs that limit the amount of inputs to functions if we want to create loosely coupled software. Ideally our functions would have no inputs (they would be niladic). By implementing the React architecture (as React encourages) it breaks this fundamental rule of good design.

In the declaration of the component we have only one input, but in the instantiation we have many. Added to this we have implicitly tied the declaration of our view (showing the total) to our procedural logic (calculating the total). After all, we cannot create a total output without injecting many inputs

Over time this coupling problem will become magnified in your architecture; these inputs and outputs begin to dictate the relationships of our business logic and affect the design of our view (the markup). This is never a good idea since we will end up with 'spaghetti' components.

To see a fully working example and demonstration of this problem in React (which uses fully implemented user input and internal component state) go to the CodeSandbox here

Externalisation

So, if we want to solve this problem and de-couple our views from our business logic by avoiding using component inputs what can we do?

Well we don't throw the baby out with the bathwater and change frameworks (again). Us JavaScript engineers waste so much time learning new frameworks to simply eschew the old ones.

Instead we should 'help' React by forcing it to do what it does well (render views) without overloading it with responsibilities (inputs). We do this by actually separating our business logic into external modules and have this separated logic do the complex work.

At Logic Room we refer to this as taking a framework agnostic approach to designing our UI app because we are no longer letting the framework dictate the rules of engagement!

This automatically means that we need to use an externally located dependency which will manage the inputs in order to calculate the output. We can either do this with object oriented programming (OO) or functional programming (FP).

Here I have decided to take a blended approach:

class External {
  constructor() {
    this.left = 0;
    this.right = 0;
    this.finalCall = null;
  }

  registerFinalCall = (finalCall) => {
    this.finalCall = finalCall;
  };

  doFinalCall = () => {
    this.finalCall(Number(this.left) + Number(this.right));
  };
}

export const external = new External();

This very basic construct uses a simple callback mechanism (lines 8-10) to allow the React component to wire into it and receive updates accordingly, during a final call phase (line 12-14). For those of you who like patterns, you will probably recognise what I have done here since I have implemented a very basic and dumbed down version of the observer pattern.

If we decide to take the load away from React like this we will be able to build a fully composable UI which has no dependency on inputs, only outputs (as JSX markup) which is then returned through Reacts render tree. Before I present you with the full working solution (as a CodeSandbox) and in-case you aren't convinced; I want to show you the before and after so you can see how clean our markup becomes:

Instead of this:

return (
    <div className="App">
      <h1>Totals</h1>
      <Left state={state} setState={setState} />
      <Right state={state} setState={setState} />
      total is {state.left + state.right}
    </div>
  );

We end up with this:

return (
 <div className="App">
      <h1>Totals</h1>
      <Left />
      <Right />
      total is {total}
    </div>
  );

In the second one we avoid the tightly coupled props inputs from the first one. However now we will need to wire up our component to this new external interface. We do this by using React hooks (or in classes the use of shouldComponentUpdate).

There are two clear advantages to externalising code like this. 

Firstly it will make our markup simpler to understand. Markup should be dumb and should only be responsible for the most basic presentation concerns. Secondly it will mean that testing our React apps is much easier. Now we don't need to test the component to make sure our inputs are added together; we simply test the external module in isolation! 

Here is a diagram which shows the high-level before and after of our architecture.

You can see full code for this in the CodeSandbox here

State

My argument comes to moving the generation of inputs away from markup into external locations.

However, this does now mean you may need to allow this external interfaces to carry state when your app grows, and because of this you must be able to broadcast updates to this state (the total output in our example) back to React. 

One option is to use React-Redux. Redux is very opinionated so you will have to learn it's fundamental patterns of actions and reducers. In my experience; over time codebases using Redux will become difficult to maintain because engineers now are wrestling not only with Reacts default architecture but also Redux's.

A potentially simpler solution we teach in our 12-Week UI Architecture Academy uses Mobx. This will provide a way to get updates to state flowing effectively from external modules to and from your framework of choice (React, Angular or Vue). Mobx's fundamental design just uses basic FP and OO.

Conclusion

React is a simple library to generate HTML views which implies an architectural standard based on ‘orthodox’ functions. These demand inputs be buried into it's markup. It does this because it has introduced scope and closures into its templating language (JSX).

Where no better opinion exists this will elicit the engineer to tightly couple business concerns into the view markup.

This causes an over-reliance on component arguments (which are a proxy for function arguments) taking us away from our ideal niladic function proposition which causes coupling (the first true enemy of design). 

But, a good React architecture would not force bad design like this. A good React architecture would open good options and close bad options. For this reason, we should use Reacts' component tree to focus on generating HTML instead of managing data. This means we must leave React as soon as possible and build an external architecture which serves our business logic concerns independently.

This does mean we need to provide a communication mechanism between our React tree and our external design. This can be done by leveraging something like the observable pattern.

When we do this we not only create less coupling but we can leverage the power of Object-Oriented programming and or Functional programming in our external library and let the React architecture control only what it should be controlling – markup!


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.