Web app architecture based on Redux
My latest side project is a full-blown web app made with some of the latest front-end technologies. The purpose of this article is to describe the modular architecture solution that I’ve used in that web app.
This architecture is dependent on some libraries and their concepts, which will be somewhat explained throughout the article:
- RxJS which is a “library for composing asynchronous and event-based programs by using observable sequences”;
- “Epics” from Redux-Observable which is a side-effects manager for Redux, allowing for composing of asynchronous actions in reaction to other actions using observables (there are other similar solutions, e.g. @ngrx/effects);
- Apollo GraphQL client. GraphQL is “a query language for APIs” and a beautiful evolution from REST. On the architecture depicted here the GraphQL is not a mandatory option. Other data providers can be used on this architecture.
Towards the end of the article, I also provide a Case Study with a general example for each module. This case study is not a full working application with source code but only a real life example with what suffices to better understand the abstract concepts. The sparse code included was originally in Typescript and I left it that way for clarity of types.
At the final of this article I present some features of the web app and how you can easily see the Redux and the time-travel working in real-time!
This architecture inherits the Redux advantages (and disadvantages, of course). A great summary about choosing Redux is the article “You Might Not Need Redux” by Dan Abramov. I think the two more interesting advantages are:
- the complexity restraint by using immutables and state changes without side-effects as discussed in the Redux information.
- application state recording, allowing for time-travel and replay. Also allows for a client browser, when encountering a severe error, to send the last states to a report server for later analysis. The debugger’s dream!
Another advantage is the total separation from view logic and view. The view logic can be totally developed and tested without a single HTML element being written. The point of contact is solely the Redux store using data models that are common throughout the application. This separation allows for better modularity in development teams but also to easily apply the same view logic to different UI.
Follows a diagram with the architecture modules where the good old 3-tier architecture is still in good shape, where the main difference from Redux to an MVC architecture (that also uses that 3-tier architecture) is in restrictions. Let’s analyze each component right away.
The “Models” module declares the data models and associated static micro/helper functions. Every other part of the application can access, or use, the models, so the diagram does not show that dependency for clarity.
Only serializable models can be stored on Redux and passed to/from the server using JSON. To make this rule always present it is advisable to mark the types that are indeed seriable. I’ve chosen to append the serializable types with the “Ser” suffix. This way I always remember not to have non-static functions in those classes.
A reference version of each model is also declared, which provides the data to store on entities that reference that model. Normally it is only an “id” field, but if there are some “lightweight” fields that are commonly accessed, they can also be included. The entity-relationship manager in the data layer should know that these are the same entities. Which is the case with Apollo.
This architecture also implements a metadata abstraction, for passing along data fetching status, called AsyncDataSer:
- Provides a placeholder for accessing asynchronously obtainable data. It provides the data (possibly) and status about it, like “loading” or “error”.
- Normally it is generated on the Data Layer and using it across the application every consumer gets to know what’s the data fetching state and can react accordingly. This includes the HTML templates that can switch between different presentations of the data (or lack of).
- Also provides a cursor property for helping paginating data.
The Data Service concentrates all the specific queries/mutations to the database required by the Business Layer. Also needed is a client-side entity-relationship manager which interfaces with the remote database, providing cached results and entity-relationship navigation. I have used the Apollo GraphQL client.
The result/cached data is stored on the Redux Store thus making part of the application state, which can be directly consumed by the Presentation Layer. For this, the ER manager must provide its own reducers and actions, explaining the double arrow between “ER Manager” and “Redux Store” in the diagram above.
The Data Layer also generates AsyncDataSer objects in every query, as discussed in the “Model” section above.
The business logic implements all the processing of the received and sent data and how it interacts with the Presentation Layer by means of the Redux state. That logic is expressed solely on Redux reducer functions and epics which are totally decoupled from the UI and extremely easy to test.
One “epic” is a function that, in response to an action being processed by the reducers, produces a chain of other actions throughout time. A simple example is to emit an action for resetting search filters when an action of entering a page is dispatched. A more useful role for epics is to emit a call to an API endpoint in response to the action produced by a “Save” button, for example. Then it can wait asynchronously for the response and chain an action with that response. Epics use RxJS which implements this timely chain beautifully. Epics only interact with Models, Data Service and Redux Store, where reducers are much more restrict.
Reducers produce a new state from solely two inputs: the current state and a dispatched action. Every reducer must be a pure function, implementing a deterministic algorithm given those two inputs. Besides the home site for Redux, there are lots of tutorials like the article by Alex Bachuk for more information on all these matters.
Pages are “container components” (as defined below) that contain other containers and presentational components (also defined below) in a layout and do not do much else. They are separated from containers because they are top level and are only what the application router sees (the application router is what manages the navigation in the application).
The separation of components into “Container” and “Presentational” is presented on the memorable article by Dan Abramov. You can read more there, but to get going I need only to say that containers are components that coordinate the interaction between presentational child components and the Redux store. They have only the necessary HTML/CSS for laying out the child components. Also, they validate and receive events from the child components and trigger the appropriate Redux actions. Since containers have a direct dependency on the Redux state and actions, which in turn are totally related to the business logic, they tend to be hardly reusable. Finally they can have local inputs: mainly for configuration since they can read the Redux state on their own.
A “presentational component” is a final UI web component, fully reusable and producing all the necessary HTML/CSS.
Completely self-contained, these components have no access to Redux nor database service, making the them independent of application state and business logic. They are configured with immutable inputs and produce output events upon user interaction.
For the case study, I declare an “Entry”, “EntryRef” and “UserRef” types. Entry data will be available as an AsyncDataSer for Entry.
In the code below the “fields” static member is only relevant since I’m using GraphQL to talk to the backend API. The defined string select the fields I’ll be querying the server for.
The EntryService provides a function fetchEntry_query(dataId, entryId, userId) for retrieving an Entry’s data. This query can be used by different client code possibly in different parts of the applicaion, so the “dataId” is a global identifier provided by the client code which will be later used to access the query’s result on the presentation layer.
This case study is for the initial loading of the detail page for editing an “Entry”. The page can be accessed by an URL like “/editentry/ebbdd170–6dea-11e7–80d7–475d81aec50b” and, entering the page, that entry is loaded for displaying and editing.
The Redux state data needed:
- Selected entry Id
- Editing state (READY, CHANGED, LOADING, SAVING, ERROR)
- Entry data
- logged user
There are two epics to accomplish the loading of the data in the page.
Epic for setting the current selected entry id:
Epic for loading an entry’s data:
There is a top level container which is the entry editing page itself. Other containers or components exist on this page, like an header, footer and menu. Also there is a EditEntry container to handle the main functionality of the page. This container is reused on other page for adding a new entry (which is not depicted in this case study) where the different behavior (add or edit) is triggered by the presence of an entry id.
The container component EditEntry container uses two presentational components: EntryForm and EntryPreview. It fetches state (editing state, entry data) and receives events (for form changed) for and from the components. Also includes a “save/update” button, which is enabled only when editing state is “CHANGED”. It triggers the appropriate Redux action for saving or updating the edited entry data collected from the EntryForm component.
The EntryForm component has an input for entry data which presents on input fields. When an input changes, it emits an event with a new entry data object copy with the change applied.
My side project: www.brickurator.com
The “brickurator” web app is a tagging and ranking system for external images. In this case for tagging LEGO® constructions shown on Flickr® (will be expanded to others in future). It was implemented following the architecture depicted on this article and from a web app development point of view, the following features are of mention:
- Redux implementation with full time-travel;
- fine-grained loading/error data status;
- optimistic UI: data changes are implemented client side ahead of server response;
- asynchronous operation: no UI blocking, no “save” button (with the exception of an entity creation);
- page interface tour system with automation;
- user authorization and authentication using auth0.com;
- liquid UI: from mobile to desktop using Angular and Ionic;
- GraphQL server API using apollo-server and node.js;
- GraphQL apollo-client custom wrapped for time-travel compatibility;
- Flickr® API integration;
- BigHugeLabs API integration for English thesaurus.
If you are not acquainted with Redux you can see it in action by installing the Redux DevTools Extension in Chrome and see the actions and state develop as you interact with the application. You can also use the slider to make some nice time-traveling in the application, like in the video above!
PS: This article is also available in my blog.