magnars

A Simple Frontend Architecture That Works

På norsk på Kodemaker-bloggen.

There are many grand frameworks to choose from for your frontend architecture, but do you need all the moving parts? In this blog post, I will talk about a simple architecture that has served me well.

Here are the key points:

In Short

The app is kicked off by a main method, which creates a place to gather data. This data is fetched and sent to a prepare function that transforms domain data into UI data. The UI data is then rendered using generic components.

Main Domain- data prepare generic components UI-data DOM

Data Flow

Your data reaches the client in some way. I won’t go into details in this blog post, other than to say that the components themselves should not be fetching it. Maybe you’re fetching data with GraphQL, or WebSockets, or some GET requests - as long as it’s done centrally, that’s fine.

Once you have the data, it is kept in a top-level location defined by the main function. This could be in a database, in an atom, or if necessary, in a JavaScript object.

Either way, you need to know when the data changes so that an update of the UI can be initiated.

When this happens, a prepare function is called with all the data, transforming domain data into UI data. This UI data is sent to a top-level component, which renders the UI using generic components.

That’s the entire data flow. When data changes, all of this happens again. The virtual DOM trick (popularized by React) allows us to do this without significant performance issues.*

* Out of the box for ClojureScript, but large JavaScript projects might need to resort to immutable.js

Generic Components

These are our building blocks. They implement our design but are not aware of the domain. They do not know the context in which they are used. They are unaware of the actions performed when buttons are pressed.

This makes the components eminently reusable. When we move from a RegistrationButton to a PrimaryButton, it can be used in many places. You get a separate language for the design, detached from the domain language.

But what about actions? Shouldn’t a RegistrationButton do something different than a SignInButton? Yes, but the actions to be performed are also passed to the components as data.

Simply put:

PrimaryButton({action: ["register-user"]})

All PrimaryButton knows is that when it is pressed, the action should be put on an event bus. This is monitored by the main function, which then carries out the action.

Main Event bus Domain- data prepare generic components UI-data DOM Actions as data watches & executes

Observe how all the arrows flow out from main, and one way. The event bus is the mechanism that inverts the dependency so we can communicate actions without introducing circular dependencies.

PS! Ove asked a timely question when this blog post was published. In response, I have written a bit more about the interplay between generic UI components.

From Domain Data to Generic Components

Since the components do not speak the domain language, we need an interpreter. That’s the prepare function. It takes the domain data from the central data source and converts it into custom data for precisely the UI we are rendering now.

The data from prepare should, as far as possible, reflect the UI. It builds a tree structure that can be sent directly down to the components. Note how the UI-data and DOM trees in the illustration above have the same structure.

This means that the component code itself can be virtually free of logic. UI code is notoriously difficult to test. With this approach we can test the UI data instead of the components, giving us testability of the important parts, while freeing us from the minutia of the component implementation.

In Conclusion

This is an architecture I have happily used on small and large projects for many years, but what do we actually get?