A Simple Frontend Architecture That Works
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:
- All data is gathered in one place.
- The data flow is predictable and unidirectional.
- UI components have all their data passed to them.
- UI components are independent of domain and context.
- Actions are communicated from the UI components via data.
- The moving parts are centralized at the top level in a main function.
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.
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.
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?
- A data flow that is easy to follow.
- Reusable components that implement the design.
- A reproducible user interface due to a single data source.
- Freedom from the perpetual framework race.