Early this year, I published a blog article on Push-based Architectures for Angular applications. In that earlier post, I provided code solutions for Angular developers. And I recorded a YouTube video of a panel discussion and presentation of those concepts in Push-based Architectures with RxJS.
Now with React v16.8 and Hooks, we can build equally elegant, performant push-based solutions with React. In this post, I provide similar mind-blowing ideas and code solutions for React developers!
Note: this is not a tutorial on React, React Hooks, RxJS, nor state management. This is an advanced blog on how to use those concepts to quickly build amazing architectures in React using TypeScript.
With RxJS and Observable streams, developers can implement architectures that PUSH data changes to all interested subscribers.
Views simply subscribe to the desired data streams. When the ‘remote’ data changes — for any reason — that data will be auto-pushed through the stream to any interested view components. View components will re-render ONLY when specific data changes are pushed.
This approach is a fundamental, HUGE mental paradigm-shift from traditional pull-based or polling architectures.
What are the benefits of designing and using Push-based Architectures? With push-based services, developers can create applications using Passive View components. Passive view components have the following characteristics:
- Auto-render when data arrives via a push-stream.
- Delegate user interactions to a business layers.
- Focus on rendering, layouts, animations, and interactions.
- Have no knowledge of data sources, loading, processes, or logic.
- Do not contain business logic.
- Require no unit testing… only UX testing.
Consider the following React application:
Let’s dive into code and explore three (3) approaches to building this React application.
(1): React.Component classes with Data-Services
(2): React.Component classes with Akita Redux + Data-Services
(3): React Functional components with Custom Hooks + Akita Redux
Note: As each approach/solution is presented, you may notice that the view components will progressively evolve to a more simple and clean implementation!
Solution #1: Component Classes with Services
Here is the classic React architecture pattern where developers often mix together view logic and business logic. For testing, maintenance, and stability reasons, this approach is not ideal.
Here are the important code parts:
Our UsersService class manages the complexity of data loads, searching, and user selection. With the
usersService singleton instance, the UsersPage view component uses the component lifecycle method
componentDidMount to load the user list.
Note how the event handlers
updateUser employ the UsersService business logic and then use
this.setState() to trigger component rendering with the latest data values.
The imported singleton
vmis a view-model (aka facade). Facades hide the complexities of the business and data-access layers. Facades — when used exclusively at the view layer [to provide specific properties and methods ] — are often called View Models.
This is the typical developer approach. It is also Pull-based Approach because the view pulls the data from the service. Pull-based solutions can create “stale data” and have serious flaws!
- What if another component changes the active user? How does the current
UsersPagesview component get notification regarding
userchanges and render with the new, active user?
- What about the subscription to
users$. When the component is unmounted, we will have a memory leak.
- Our business logic is mixed inside the view component. View component logic must first call usersService methods and before calling
Solution #2: Component with State Management
This version uses Redux + RxJS to manage state with RxJS observable streams: Akita State Management.
In this code below, Observables are used to stream the current
active to the view component; and
this.setState() is used to trigger rendering with the latest data.
Under the hood, the
UsersService is using a
EntityState<User> instance to manage a collection of User records and a
QueryEntity<UsersState, User> instance to quickly, easily build subscribable RxJS streams to internal data (aka state).
For Angular NgRx developers the semantics are very, very similar to
Whenever the state changes, the current state values are pushed to subscribing view handlers and triggers view updates.
This is a great Push-based Approach!
This solution still manifests, however, a mixture of business logic (state management tooling) within the React.Component view code. We can improve upon this solution using the latest React techniques: React Hooks.
Solution #3: Functional Components with Hooks
Let’s use React Hooks, custom hooks, and facade (aka view-model) patterns to hide all the complexity from the
UsersPage view layer.
Below, the custom hook
useUsersFacade() will internally manage all issues regarding RxJS, Facades, and Akita. Note how the view uses the custom hook how the hook internally uses the standard React Hooks
This is an elegant, push-based solution that cleanly partitions the business logic from the view layers.
The custom hook also manage RxJS subscriptions to auto-unsubscribe when the component is discarded (aka unmounted).
For our React application, you can see [below] the evolution of the
UsersPages view component through each of the labs/solutions. As we re-architect each lab, the
UsersPages view component becomes more clean, terse, and most maintainable.
Solution #3 is the obvious winner!
Using React Custom Hooks + RxJS Facades + State Management is an amazing approach to React applications. With this approach, your business code has now been moved into the business layer(s) and is easily tested.
- Use Jest to test your business layers, facades, data services, and more
- Use Cypress to test your UX workflows and UI stylings + layouts.
I cannot encourage developers strongly enough to use push-based architectures for their React or Angular applications.