📜 ⬆️ ⬇️

Applying the observer pattern in Redux and Mobx


The "observer" pattern is probably known since the appearance of the aop itself. It is easy to imagine that there is an object that stores a list of listeners and has a method of "add", "delete" and "notify", and the external code either subscribes 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(); } } } 

In redux, this pattern is applied without any changes - the "react-redux" package provides a connect function that wraps the component and when the componentDidMount is called, it calls the subscribe() method, when it calls the componentWillUnmount() calls unsubscrib() and the dispatch() will simply call the trigger() method which in the loop will call all listeners where everyone in turn will call mapStateToProps() and then, depending on whether the value has changed, will call setState() on the component itself. Everything is very simple, but the price for such simplicity of implementation is the need to work with the state immiabably and normalize the data and, when a single object or even one property changes, absolutely all subscriber components should be notified even if they do not depend on the changed part of the state and at the same time -subscriber needs to explicitly specify from which parts of the store it depends inside mapStateToProps()


Mobx is very similar to redux in that using this observer pattern only develops it even further - that if we don’t write mapStateToProps() ’ll make the components dependent on the data they “render” independently, separately. Instead of collecting subscribers on one state of the entire application, subscribers will subscribe to each individual field in the state. It is as if for a user who has the fields firstName and lastName we would create a whole redux-store separately for firstName and separately for lastName .


Thus, if we find an easy way to create such "stor" and subscribe to them, then mapStateToProps() will not be needed, because this dependence on different parts of the state is already expressed in the existence of different stor.


So for each field we will have a separate “mini-store” - an observer object where, besides subscribe() , unsubscribe() and trigger() another value field will be added as well as get() and set() methods when calling set() subscribers will only be called 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 a store needs to be interpreted a little differently - if we store only primitive values ​​in each separate store, then from redux's point of view, there is nothing wrong with calling user.firstName.set("NewName") - since the string This is an immutable value — here it is just the installation of a new immutable value for the store, just like in redux. In cases when we need to save an object or complex structures in the “mini-store”, then we can simply put them into separate “mini-stores”. For example instead


 const user = { profile: new Observable({email: "...", address: "..."}) } 

it is better to write so that the components can individually depend on the "email" then on the "address" and that there are no extra "re-tenants"


 const user = { profile: { email: new Observable("..."), address: new Observable("..."} } } 

The second point - you can see that with this approach, we will have to call the get() method for each access to the property, which adds inconvenience.


 const App = ({user})=>( <div>{user.firstName.get()} {user.lastName.get()}</div> ) 

But this problem is solved through javascript setters and setters.


 class User { _firstName = new Observable(""); get firstName(){ return this._firstName } set firstName(val){ this._firstName = val } } 

And if you do not take a negative attitude to decorators, then this example can be further simplified.


 class User { @observable firstName = ""; } 

In general, you can still sum up and say that 1) there is no magic at this moment - decorators are just getters and setters 2) getters and setters just read and install root-state in the “mini-store” a la redux


Let's go further - in order to connect all this to the reactant, 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 the bolt plate will increase many times, but with one small movement you can remove the manual subscription completely if you add a few lines of code - because the .get() method will be called one way or another to render the value, we can use this is to make an automatic subscription - if we call the current array in the global variable before calling the component's render() method, then in the .get() method we simply add this to this array and then to the end of the call of the render() method, we get an array of all the “mini-stores” to which the current component is signed. This simple mechanism even solves situations when the strings to which a component is subscribed dynamically change during rendering - for example, when a component renders <div>{user.firstName.get().length < 5 ? user.firstName.get() : user.lastName.get()}<div> <div>{user.firstName.get().length < 5 ? user.firstName.get() : user.lastName.get()}<div> (if the name length is less than 5, the component will not respond (that is, it will not be signed) to change the name and the subscription will automatically occur when the name length is greater than or equal to five)


 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 connect function wraps the component or stateless-component (function) of the reactor and returns a component that, thanks to this auto-subscription mechanism, subscribes to the necessary "mini-stores".


As a result, we have got such a mechanism of auto-subscriptions only for the necessary data and alerts only when this data has changed. The component will be updated only when changed only those "mini-stores" for which it is signed. Considering that in a real application where there can be thousands of these “mini-stores”, with the mechanism of multiple stor when changing one field only those components that are in the array of subscribers to this field will be updated, but with redux approach when we sign all these Thousands of components on a single stop, with each change, you need to notify all these thousands of components in the loop (and at the same time forcing the programmer to manually describe from which parts of the state the components inside mapStateToProps )


Moreover, this auto-subscription mechanism can improve not only redux but also such a pattern as function memosization, and replace the reselect library — instead of explicitly specifying createSelector () in what data our function depends on, dependencies will be determined automatically just as above. render ()


Conclusion


Mobx is a logical development of the observer pattern for solving the problem of “point-like” component updates and function memosation. If we refactor a bit and take out the code in the example above from the component in Observable and put getters and setters instead of calling .get() and .set() , then we almost get observable and computed decorators. Almost - because mobx, instead of a simple call, has a more complex subscriber call algorithm in the loop in order to eliminate computed extra calls for diamond-shaped dependencies, but this will be computed in the next article.


upd: There was a continuation of the article


')

Source: https://habr.com/ru/post/348960/


All Articles