The Same Good Architecture Applies Everywhere
Although there are significant differences between React, Angular and Vue, they share architectural fundamentals. We could look at this unity like so:
This high-level applies equally to the big three, React, Angular and Vue (and probably emerging things like lit-html).
A core improvement that is shared by all three is the DOM Binding concept seen above, and described more fully here.
A key area of complexity in applications using these frameworks is the Application State. I ask the question, what is the best way to handle state in React here. I’ve arrived at what I think is the ideal design principle for this question, across all three frameworks.
In short, the principle I’m proposing is this:
For business data, favor centralized state, for UI state, favor component-state.
Let me emphasis the favor part of the idea. Lean-towards this idea, don’t implement it like its ethically wrong to put business state in the components.
I definitely haven’t worked on enough applications across all three frameworks to say with a certainty, so jump in there and tell me your experience.
The experience I’ve had in all three tells me this is a good guiding principle that can handle even very large and complex apps.
Two Kinds of State
If you tell me the position of an onscreen element, you are telling me about the UI state.
If you tell me how much money is in the account total widget, you are talking about business data.
This is not a hard-and-fast distinction because you might persist to the backend the location of widgets, thereby turning the UI state into a kind of business data.
Moreover, business data always drives the content of the UI.
A more perfect way to look at business data is more as remote persistent data.
That is to say, data which comes from and returns via remote API. For the purpose of this discussion, I’m saying business data is almost synonymous with persistent API data.
So far, the idea looks like this:
The main thing we are trying to AVOID is this:
Again, not because it’s wrong or doomed to colossal failure.
We are avoid having persistent data go through multiple actors simply because it violates a fundamental tenant of application design that has made complexity manageable for decades: isolate data concerns to layers.
I think most seasoned software developers would look at that and know instinctively that sprinkling back-end API calls throughout the components will lead to difficult-to-understand data flows.
Concentrating Back-End Calls to a Layer
So we’d like to really call from a multitude of components to a centralized and consistent central data store, representing the data layer.
There are all kinds of ways you could achieve that. In Vue, React and Angular, there is a standard and similar way:
- React: Redux
- Angular: ngrx/store
- Vue: Vuex
Each of these creates a central store that is interacted with in a formal way, and includes the ability to handle backend API interaction.
In each case, it goes something like this:
That looks like a pretty fair abstraction of the store concept as manifested across all three frameworks.
So the Flux-like Store implementations give a standardized and rigorous way to enforce a data access layer.
They also give more than that, in the formalization of state changes via the command observer. In short, using a formal store gets you more complexity than you’d need for a straight centralized store option.
Do you really need all that extra stuff?
Here’s my take on this: no you probably don’t need all that, but you also probably aren’t going to find a simpler or quicker central setup than just going ahead and using the central store, except in Angular.
In Angular, the central store is a bit clunky, if I may say, and the Inversion of Control and Observable nature of its RxJS framed architecture makes it easy to create your own central state management.
Simple Examples of Central Stores
Here’s very simple examples of each framework using their official central store.
Notice that Vuex and Redux are simple one page JSFiddles, while ngrx/store is a multi-file app. Not good, Angular, not good at all.
Root Component as Global Scope and API Layer
In the absence of a global state, the root component of a view tends to serve the role.
By view here I mean that as an app grows you will split it into multiple separately loaded pages, which each has a root component. The default situation will be that the root component acts as the global variable space.
In this case the root component will be in charge of interacting with the children in the tree to keep them in sync with state via props and events coming up from the children back to the parents.
Something like so:
This is a fair compromise, I think: it keeps the API interactions concentrated in the root components. Once again, we’re working to keep the API calls from being scattered throughout the code base.
This will look quite different according to which framework you are using. The most awkward will be React (see here) in that you’ll be using “render props” to deal with communication upwards.
In Angular and Vue, there is strong eventing support that can lessen the difficulty of managing child-to-parent communications. These are cleaner than render props, but you still have to watch out for exploding complexity as events trace across your components.
An interesting approach is stateless components. In this case, you take the extreme end of the spectrum and eliminate state from components altogether. Components simply become behavior manifestors.
The idea here is to make components very easy to think about. My feeling is this probably actually pushes the complexity around, rather than eliminate it, but I don’t have enough in-the-trenches experience to say with a certainty.
These are also called “Functional Components” to highlight the fact they are not modeled as objects. (Also called “Pure” components, which is a really useless and confusing name, IMO).
These kinds of components are possible:
(In Angular, they are called “Presentational Components” which I feel is an OK descriptor.)
My biggest drawback in pushing for centralized state/API access is that in beginning a new app, one might be tempted to enforce this as a hard-and-fast rule, thereby slowing down development as people deal with the store, instead of delivering features.
The essential rule of Software Survival then applies: make the app work so it survives, then refactor if you have to.
Better to work on an app with warts, than let the app die.
The ideal always is out there a bit ahead of you in software — that’s one of the great fascinating things about it.
Besides, as soon as you get things dialed in on your architecture, some client or PM will walk in and screw everything up.
Anyway, the key points I want to make here are:
- The overall design options are analogous across Vue, React and Angular
- Try to keep API calls concentrated into a layer
- Try to only share UI state into the component tree
- Use root component as API layer and central state if not using store
- Use store if you/your team/your timeline can support the complexity