📜 ⬆️ ⬇️

Development of javascript applications based on Rx.js and React.js (RxReact)

rxreactlogo

React.js allows you to work with DOM very efficiently and quickly, it is actively developing and is gaining more and more popularity every day. Recently discovered the concept of reactive programming, in particular, the equally popular Rx.js library. This library brings a new level of work with events and asynchronous code, which in UI logic, javascript applications abound. The idea came to combine the power of these libraries into one whole and see what comes of it. In this article, you'll learn how to make friends Rx.js and React.js.


Is RxReact a new library?


Maybe someone will be disappointed - but no. One of the positive aspects of this approach is that you do not need to install any new libraries. Therefore, I did not bother much and called this approach RxReact .
For the impatient - repos with test examples .

What for?


Initially, when I first became acquainted with React, I was not at all embarrassed to stuff business components with logic, ajax requests, etc. But as practice has shown, it is an extremely bad idea to interfere with all this inside React components by subscribing to various hooks, while maintaining the intermediate mutable state. It becomes difficult to make changes and to understand such components - monsters. React in my view is ideal only for drawing a specific state (snapshot) of an application at a certain point in time, but the logic of how and when this state changes is not at all his business and should be in another layer of abstraction. The less the presentation layer knows about it, the calmer we sleep. I wanted to bring React components as close as possible to the pure functions without a mutable, stored state, side effects, etc. At the same time, I wanted to improve the work with events, it is desirable to put in a separate layer of logic a declarative description of how the application should interact with the user, respond to various events and change its state. In addition, I wanted to be able to link chains of sequences of actions from synchronous and asynchronous operations.
')

No that's not exactly flux


An inquisitive reader who has read up to this point has already thought several times: “So there is Flux - take it and use it.” I recently looked at him and, to my surprise, I found a lot of similarities with the concept I want to tell you about. At the moment, I have already seen several implementations of Flux . RxReact is not an exception, but in turn has a slightly different approach. It so happened that he himself involuntarily came to almost the same architectural components as: dispatcher , storage , actions . They are very similar to those described in the Flux architecture.

Main components


I hope that you managed to intrigue you with something and you have read this far, because here the most delicious begins. For a more visual example, a test application will be considered:
demo
Demo site - demo1 .
The source is here .

The application itself does not do anything useful, just the click counter on the button.

View

The presentation layer is a React component, the main purpose of which is to draw the current state and signal events in the UI.

So, what should be able to view?

Below is the view code from the example ( view.coffee ):
React = require 'react' {div, button} = React.DOM HelloView = React.createClass getDefaultProps: -> clicksCount: 0 incrementClickCount: -> @props.eventStream.onNext action: "increment_click_count" render: -> div null, div null, "You clicked #{@props.clicksCount} times" button onClick: @incrementClickCount "Click" module.exports = HelloView 

javascript version of view.coffee file
 var React = require('react'); var div = React.DOM.div; var button = React.DOM.button; HelloView = React.createClass({ getDefaultProps: function() { return { clicksCount: 0 }; }, incrementClickCount: function() { return this.props.eventStream.onNext({ action: "increment_click_count" }); }, render: function() { return div(null, div(null, "You clicked " + this.props.clicksCount + " times"), button({onClick: this.incrementClickCount}, "Click")); }}); module.exports = HelloView; 



As you can see, all the data about clicks comes to us "from above" through the props object. When you click on the button, we send the action through the eventStream channel. View signals us about button clicks using eventStream.onNext , where eventStream is an instance of Rx.Subject . Rx.Subject is a channel to which you can both send messages and create subscribers from it. Further it will be described in more detail how to work with Rx.Subject .

Once we have clearly defined the functions of the view and the channel of messages, they can be distinguished in the structural diagram:
view_layer
As you can see, the view is a React component, receives the current state of the application (app state) as input, sends event messages via the event stream (actions). In this scheme, the Event Stream is the communication channel between the view and the rest of the application (shown by a cloud). Gradually, we will determine the specific functions of the components and make them out of the common js application block.

Storage (Model)

The next component is Storage . Initially, I called it Model, but I always thought that model was not an appropriate name. Since the model in my view is a certain specific entity (User, Product), and here we have a set of various data (many models, flags) with which our application works. In the implementations of Flux, which we had to see, storage was implemented as a singleton module. In my implementation there is no such need. This gives the theoretical possibility of the smooth existence of several application instances on one page.

What can storage?


In my example, storage is implemented through the coffee class with some properties ( storage.coffee ):
 class HelloStorage constructor: -> @clicksCount = 0 getClicksCount: -> @clicksCount incrementClicksCount: -> @clicksCount += 1 module.exports = HelloStorage 

javascript storage.coffee version
 var HelloStorage; HelloStorage = (function() { function HelloStorage() { this.clicksCount = 0; } HelloStorage.prototype.getClicksCount = function() { return this.clicksCount; }; HelloStorage.prototype.incrementClicksCount = function() { return this.clicksCount += 1; }; return HelloStorage; })(); module.exports = HelloStorage; 



By itself, storage has no idea about the UI, that there is some kind of Rx and React. The repository does what it should do by definition - store data (application state).

On the structural scheme we can distinguish storage:
storage layer

Dispatcher

So, we have a view - the application draws at a certain point in time, storage - in which the current state is stored. There is a lack of a binding component that will listen for events from the view, if necessary, change the state and give the command to update the view. Such a component is just a dispatcher .

What should a dispatcher be able to do?


From the point of view of Rx.js, we can view the view as an endless source of certain events to which we can create subscribers. In the demo example, we have only one subscriber in dispatcher, a subscriber for clicks on the increase values ​​button.

Here’s what a subscription to clicks on a button in dispatcher’s code will look like:
 incrementClickStream = eventStream.filter(({action}) -> action is "increment_click_count") 

javascript version
 var incrementClickStream = eventStream.filter(function(arg) { return arg.action === "increment_click_count"; }); 


For a more complete understanding of the code above, you can visually portray as:
image
On the image we see 2 channels of messages. The first is the eventStream (base channel) and the second, obtained from the base channel is incrementClickStream. The circles represent the sequence of events in the channel, in each event an argument is passed by which we can filter (dispatch).
Let me remind you that messages to the channel are sent by the view using the call:
 eventStream.onNext({action: "increment_click_count"}) 


The resulting incrementClickStream is an Observable instance and we can work with it the same way as with the eventStream, which we basically will do. And then we must indicate that for each click on the button we want to increase the value in storage (change the state of the application).

 incrementClickStream = eventStream.filter(({action}) -> action is "increment_click_count") .do(-> store.incrementClicksCount()) 

javascript version
 var incrementClickStream = eventStream.filter(function(arg) { return arg.action === "increment_click_count"; }).do(function() { return store.incrementClicksCount(); }); 


Schematically looks like this:

streamdo

This time we get the source of the values, which should update the view, as the state of the application changes (the number of clicks increases). In order for this to happen, you need to subscribe to the source incrementClickStream and call setProps on the react component that renders the view.

 incrementClickStream.subscribe(-> view.setProps {clicksCount: store.getClicksCount()}) 

javascript version
 incrementClickStream.subscribe(function() { return view.setProps({ clicksCount: store.getClicksCount() }); }); 



Thus, we close the chain and our view will be updated each time we click on the button. There may be many such sources updating the view, so it is advisable to combine them into one event source using Rx.Observable.merge .

 Rx.Observable.merge( incrementClickCountStream decrementClickCountStream anotherStream # etc) .subscribe( -> view.setProps getViewState(store) -> # error handling ) 

javascript version
 Rx.Observable.merge( incrementClickCountStream, decrementClickCountStream, anotherStream) .subscribe( function() { return view.setProps(getViewState(store)); }, function() {}); // error handling 



In this code, the getViewState function appears. This function only takes out the data necessary for view from storage and returns them. In the demo example, it looks like this:

 getViewState = (store) -> clicksCount: store.getClicksCount() 

javascript version
 var getViewState = function(store) { return { clicksCount: store.getClicksCount() }; }; 



Why not transfer storage directly to view? Then, in order to avoid the temptation to write something directly from the view, call unnecessary methods, etc. View gets the data prepared specifically for display in the visual part of the application, no more, no less.

Schematically, Merzh sources look like this:

stream_merge

It turns out, in addition to the fact that we do not need to call all sorts of "onUpdate" events from the model to update the view, we also have the ability to handle errors in one place. The second argument to the subscribe is the function for error handling. It works on the same principle as in Promise. Rx.Observable has much in common with promises, but is a more advanced mechanism, since it considers not the only promised value, but an infinite sequence of return values ​​in time.

The full dispatcher code looks like this:

 Rx = require 'rx' getViewState = (store) -> clicksCount: store.getClicksCount() dispatchActions = (view, eventStream, storage) -> incrementClickStream = eventStream #    .filter(({action}) -> action is "increment_click_count") .do(-> storage.incrementClicksCount()) Rx.Observable.merge( incrementClickStream #      view... ).subscribe( -> view.setProps getViewState(storage) (err) -> console.error? err) module.exports = dispatchActions 

javascript version
 var Rx = require('rx'); var getViewState = function(store) { return { clicksCount: store.getClicksCount() }; }; var dispatchActions = function(view, eventStream, storage) { var incrementClickStream = eventStream.filter(function(arg) { return arg.action === "increment_click_count";}) .do(function() { return storage.incrementClicksCount(); }); return Rx.Observable.merge(incrementClickCountStream) .subscribe(function() { return view.setProps(getViewState(storage)); }, function(err) { return typeof console.error === "function" ? console.error(err) : void 0; }); }; module.exports = dispatchActions; 



The full file code is dispatcher.coffee.

All dispatching logic is placed in the dispatchActions function, which takes as input:



Having placed the dispatcher on the scheme, we have a complete structural diagram of the application architecture:

image

Component initialization

Next we need to somehow initialize: view, storage and dispatcher. Let's do this in a separate file - app.coffe :
 Rx = require 'rx' React = require 'react' HelloView = React.createFactory(require './view') HelloStorage = require './storage' dispatchActions = require './dispatcher' initApp = (mountNode) -> eventStream = new Rx.Subject() #    store = new HelloStorage() # c  #    view view = React.render HelloView({eventStream}), mountNode #    dispatcher dispatchActions(view, eventStream, store) module.exports = initApp 

javascript version
 var Rx = require('rx'); var React = require('react'); var HelloView = React.createFactory(require('./view')); var HelloStorage = require('./storage'); var dispatchActions = require('./dispatcher'); initApp = function(mountNode) { var eventStream = new Rx.Subject(); var store = new HelloStorage(); var view = React.render(HelloView({eventStream: eventStream}), mountNode); dispatchActions(view, eventStream, store); }; module.exports = initApp; 



The initApp function accepts mountNode as input. Mount Node, in this context, is a DOM element to which the root React component will be drawn.

The generator of the basic structure of the module RxRact (Yeoman)

To quickly create the above components in a new application, you can use Yeoman .
Generator - generator-rxreact

The example is more complicated


The example of one event source well demonstrates the principle of interaction of components, but does not at all demonstrate the advantage of using Rx in conjunction with React. For example, let's imagine that on demand we need to improve the 1st example from demo like this:



In the end, should get the following result:
demo2

Demo site - demo2 .
The source code for demo2 is here .

I will not describe the changes in all components, I will show the most interesting thing - the changes in dispatcher and I will try to comment on what is happening in the file as thoroughly as possible:

 Rx = require 'rx' {saveToDb} = require './transport' #    (    ) getViewState = (store) -> clicksCount: store.getClicksCount() showSavedMessage: store.getShowSavedMessage() #  view state      #     dispatchActions = (view, eventStream, store) -> #  "+1"  incrementClickSource = eventStream .filter(({action}) -> action is "increment_click_count") .do(-> store.incrementClicksCount()) .share() #  "-1"  decrementClickSource = eventStream .filter(({action}) -> action is "decrement_click_count") .do(-> store.decrementClickscount()) .share() #       countClicks = Rx.Observable .merge(incrementClickSource, decrementClickSource) #   (-1, +1) showSavedMessageSource = countClicks .throttle(1000) #   1  .distinct(-> store.getClicksCount()) #       .flatMap(-> saveToDb store.getClicksCount()) #     .do(-> store.enableSavedMessage()) #      #  ,        2  hideSavedMessage = showSavedMessageSource.delay(2000) .do(-> store.disableSavedMessage()) #     ,    view Rx.Observable.merge( countClicks showSavedMessageSource hideSavedMessage ).subscribe( -> view.setProps getViewState(store) (err) -> console.error? err) module.exports = dispatchActions 

I hope that you, just like me, are impressed by the possibility of declaratively describing the operations performed in our application, while creating composable chains of calculations consisting of synchronous and asynchronous actions.
On this I will finish the story. Hopefully, I managed to convey the main essence of using the concept of reactive programming and React to build user applications.

Several links from the article



PS All demos from the article use server side prerendering for React.js, for this purpose I created a special gulp plugin - gulp-react-render .

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


All Articles