Using the observer pattern on Redux and Mobx
The observer pattern has probably been known since the advent of the OOP itself. It’s simpler to imagine that there is an object that stores the list of listeners and has a “add”, “delete” and “notify” method, and the external code is either subscribed or notifies subscribers
class Observable {
listeners = new Set();
subscribe(listener){
this.listeners.add(listener)
}
unsubscribe(listener){
this.listeners.delete(listener)
}
notify(){
for(const listener of this.listeners){
listener();
}
}
}
The redux-e this pattern is used without any changes - package "react-redux" provides a function connect
that wraps the component, and the call componentDidMount cause subscribe()
method have Observable
, when you call the componentWillUnmount()
cause unsubscribе()
and dispatch()
simply call a method trigger()
that is in the cycle will cause all listeners where each, in turn, cause mapStateToProps()
and then depending on whether the value has changed - will causesetState()
on the component itself. Everything is very simple, but the payment for such simplicity of implementation is the need to work with the state immutably and normalize the data, and when a single object or even one property changes, notify absolutely all component subscribers, even if they do not depend on that changed part of the state and at the same time in the component - the subscriber must explicitly indicate which parts of the store it depends onmapStateToProps()
Mobx is very similar to redux in that it uses this pattern, the observer only develops it even further - that if we don’t write, we mapStateToProps()
make the components depend on the data that they "render" independently, separately. Instead of collecting subscribers on one state object of the entire application, subscribers will subscribe to each individual field in the state. It is as if for a user who has fields firstName
and lastName
we would create a whole redux-stor separately for firstName
and separately for lastName
.
Thus, if we find an easy way to create such “stores” and subscribe to them, we mapStateToProps()
won’t need it, because this dependence on different parts of the state is already expressed in the existence of different stores.
So at each field we will be on a separate "mini-story" - the object where the observer apart subscribe()
, unsubscribe()
and trigger()
added another field value
as well as methods get()
and set()
and when calling set()
subscribers will cause only if the value itself has changed.
class Observable {
listeners = new Set();
constructor(value){
this.value = value
}
get(){
return this.value;
}
set(newValue){
if(newValue !== this.value){
this.notify();
}
}
subscribe(listener){
this.listeners.add(listener)
}
unsubscribe(listener){
this.listeners.delete(listener)
}
notify(){
for(const listener of this.listeners){
listener();
}
}
}
const user = {
fistName: new Observable("x"),
lastName: new Observable("y"),
age: new Observable(0)
}
const listener = ()=>console.log("new firstName");
user.firstName.subscribe(listener)
user.firstName.get()
user.firstName.set("new name");
user.firstName.unsubscribe(listener);
At the same time, the requirement for the immunity of the store needs to be interpreted a little differently - if we keep only primitive values in each individual store, then from the point of view of redux there is nothing wrong with calling user.firstName.set("NewName")
- since the line is an immutable value - then just setting up a new one immutable value of the store, just like in redux. In cases where we need to save an object or complex structures in the “mini-store”, we can simply put them in separate “mini-stores”. For example instead
const user = {
profile: new Observable({email: "...", address: "..."})
}
it’s better to write so that the components can individually depend "email"
on "address"
and from so that there are no unnecessary “re-renders”
const user = {
profile: {
email: new Observable("..."),
address: new Observable("..."}
}
}
The second point - you can notice that with this approach we will be forced to call a method on every access to the property get()
, which adds inconvenience.
const App = ({user})=>(
{user.firstName.get()} {user.lastName.get()}
)
But this problem is solved through javascript getters and setters
class User {
_firstName = new Observable("");
get firstName(){ return this._firstName }
set firstName(val){ this._firstName = val }
}
And if you don’t have a negative attitude towards decorators, then this example can be further simplified.
class User {
@observable firstName = "";
}
In general, we can summarize for now and say that 1) there is no magic at this moment - decorators are just getters and setters 2) getters and setters just read and set the root-state in the “mini-store” a la redux
We go further - in order to connect all this to the reaction it will be necessary in the component to subscribe to the fields that are displayed in it and then unsubscribe to componentWillUnmount
this.listener = ()=>this.setState({})
componentDidMount(){
someState.field1.subscribe(this.listener)
....
someState.field10.subscribe(this.listener)
}
componentWillUnmount(){
someState.field1.unsubscribe(this.listener)
....
someState.field10.unsubscribe(this.listener)
}
Yes, with the growth of the fields that are displayed in the component, the number of bolerplate will increase many times, but with one small movement the manual subscription can be completely removed if you add a few lines of code - since the method will be called in the templates one way or another .get()
to render the value, we can use this to make an automatic subscription - if before calling render()
component, we write in the global variable current array in the method .get()
, we simply add this
to the array, and then in the end of the call to the method render()
we have obtained them an array of all the "mini-story" to which the current component is signed. This simple mechanism even solves situations where the sides to which the component is subscribed dynamically change during rendering - for example, when the component renders
{user.firstName.get().length < 5 ? user.firstName.get() : user.lastName.get()}
(if the length of the name is less than 5, the component will not respond (that is, it will not be signed) to a change in the name and the subscription will automatically happen when the length of the name is greater than 5)let CurrentObservables = null;
class Observable {
listeners = new Set();
constructor(value){
this.value = value
}
get(){
if(CurrentObservables) CurrentObservables.add(this);
return this.value;
}
set(newValue){
if(newValue !== this.value){
this.notify();
}
}
subscribe(listener){
this.listeners.add(listener)
}
unsubscribe(listener){
this.listeners.delete(listener)
}
notify(){
for(const listener of this.listeners){
listener();
}
}
}
function connect(target){
return class extends (React.Component.isPrototypeOf(target) ? target : React.Component) {
stores = new Set();
listener = ()=> this.setState({})
render(){
this.stores.forEach(store=>store.unsubscribe(this.listener));
this.stores.clear();
const prevObservables = CurrentObservables;
CurrentObservables = this.stores;
cosnt rendered = React.Component.isPrototypeOf(target) ? super.render() : target(this.props);
this.stores = CurrentObservables;
CurrentObservables = prevObservables;
this.stores.forEach(store=>store.subscribe(this.listener));
return rendered;
}
componentWillUnmount(){
this.stores.forEach(store=>store.unsubscribe(this.listener));
}
}
}
Here, the function connect
wraps the component or stateless-component (function) of the reaction and returns the component which, thanks to this auto-subscription mechanism, subscribes to the necessary "mini-cards".
As a result, we got such an automatic subscription mechanism only for the necessary data and notifications only when this data has changed. The component will be updated only when only those "mini-stores" to which it is subscribed have changed. Considering that in a real application, where there may be thousands of these “mini-stores”, with this multiple store mechanism, when changing one field, only those components that are in the array of subscribers to this field will be updated, but with the redux approach when we sign all these thousands of components for one single side, with each change you need to notify all these thousands of components in a cycle (and at the same time force the programmer to manually describe what parts of the state the components inside depend on mapStateToProps
)
Moreover, this auto-subscription mechanism is able to improve not only redux but also such a pattern as function memoization and replace the reselect library - instead of explicitly specifying what function our function depends on in createSelector (), the dependencies will be determined automatically exactly the same way as with the function render ()
Conclusion
Mobx is a logical development of the observer pattern to solve the problem of "point" component updates and memoization of functions. If you refactor a little bit and make the code in the above example of the component Observable
and instead of calling .get()
and .set()
put getters and setters, we almost get observable
and computed
mobx-and decorators. Almost - because mobx has a more complex subscriber call algorithm instead of a simple call in the loop in order to eliminate unnecessary calls computed
for diamond-shaped dependencies, but more on that in the next article.
upd: Continued article