Scala.js and unidirectional data flow

During the last two years the one of the most popular subjects for discussion in the JavaScript society is so-called “unidirectional data flow”.
Those happy years, when we used JavaScript for organizing of some effect on the page or for loading some widget by Ajax, gone to the past. Today JavaScript is used for building client-side web applications.
There are several frameworks for doing such things. I used React in my last projects. It’s a great library. It allows doing everything you want. Except unidirectional data flow. You have to use Flux or Redux for that purpose.
But what about Scala.js?
We have very good facade for React, and I often use it. I haven’t found any facade for Redux. You can argue that Diode offers similar approach. OK! I just propose another way…
Monix
Monix is a high-performance Scala / Scala.js library for composing asynchronous and event-based programs, exposing high-level types, such as observable sequences that are exposed as asynchronous streams, expanding on the observer pattern, strongly inspired by ReactiveX and by Scalaz, but designed from the ground up for back-pressure and made to cleanly interact with Scala’s standard library, compatible out-of-the-box with the Reactive Streams protocol.
Idea
First of all, I want to describe an idea.
- In our React application we have many stateful elements. If we want to change an element, we have to change its state.
- Aside from states of React elements we have the State of the application. This state is calculated by applying some pure function to the previous state. So we have a stream of immutable states. Every time we have a new pure function, this function produce a new state.
- Every time we want to make some changes on the page, we add some pure function to our stream. And this pure function produce new state.
- Stateful elements subscribe to the stream and can watch for changes. And if these changes are of the type which they are waiting for they change their states.
The whole library
I know that all this sounds awful, but we need only several lines of code to realize this idea:
import monix.execution.Ack
import monix.execution.Ack.Continue
import monix.reactive.{Observable, Observer}
import monix.reactive.subjects.BehaviorSubject
import scala.concurrent.Future
class Dispatcher [State](val initialState: State){
private val dispatcher: BehaviorSubject[State => State] = BehaviorSubject.apply(identity)
val stream: Observable[State] = dispatcher.scan(initialState)((s, x) => x(s))
def dispatch(f:State => State): Unit = dispatcher.onNext(f)
def observer(f:State => Unit)= new Observer[State] {
def onNext(s: State): Future[Ack] = {
f(s)
Continue
}
def onError(ex: Throwable): Unit =
ex.printStackTrace()
def onComplete(): Unit =
println("Completed")
}
}
That’s all!
To create our stream we need a Subject. And we need the scan
method.
Subject has some properties:
- It’s an observable. It’s shaped like an observable, and has all the same operators.
- It’s an observer.
- When subscribed to as an observable, will emit any value you “next” into it as an observer.
- It multicasts.
- Subjects cannot be reused after they’re unsubscribed, completed or errored.
- It passes values through itself.
So we have a class Dispatcher
. The constructor takes only one parameter: the initial state of the application. The constructor creates the field stream
of Observable type.
The class has only one method dispatch
that gives a new Observer
.
Only one field and only one method! That’s enough for building unidirectional data flow! And we need React of course.
Example
To illustrate the simplicity of this approach let’s look at the example.
Say, we have an application with a state of Integer type. We have a field that showcases this state. We have two buttons, an increment and a decrement, that can change this state.
First of all, we need to create a type for our state. The case class CounterState
has to fields: a count
– that is our state itself, and a changeType
– that characterizes the last change. It’s of Integer type in our example, but it can be String or any other type.
Then we create a new instance of the Dispatcher
class. And we pass it to the application:
case class CounterState(count: Int, changeType: Int)
val state_0 = CounterState(0, 0)
val dispatcher = new Dispatcher[CounterState](state_0)
ReactDOM.render(Counter(CounterProps(dispatcher)), dom.document.getElementById("target"))
I specified a CounterProps
that has only one field. Of course, I could easily not to do it, but it’s not a very bad idea to have a separate type for the application property.
case class CounterProps(d: Dispatcher[CounterState])
Let’s create a React element for our +/- buttons:
case class ButtonProps(increment: Boolean, step: Int, props: CounterProps)
class ButtonBackend ($: BackendScope[ButtonProps, Unit]) {
def click:Callback = {
val props = $.props.runNow()
val increment = props.increment match {
case true => 1
case _ => -1
}
Callback(props.props.d.dispatch(
s => s.copy(count = s.count + increment).copy(changeType = props.step)
))
}
def render(p: ButtonProps) =
<.button(^.onClick --> click, p.increment match {
case true => "+"
case _ => "-"
})
}
private val Button = ReactComponentB[ButtonProps]("Button")
.renderBackend[ButtonBackend]
.build
The ButtonProps
has three fields:
increment
– is it “+” or “-“step
– some characteristic of the buttonprops
– theDispatcher
placed to theCounterProps
The most interesting part of this code is the click
method. It returns a callback which calls the dispatch
method. The only parameter of this method is a pure function that takes a state and returns a new state. This function does not mutate the state but produces the new one.
A Button
element is stateless, because we don’t intend to change it. But it can change the application state.
Let’s create a stateful element, that will be watching for changes.
class Count0Backend ($: BackendScope[CounterProps, Int]) {
var end: Option[Cancelable] = None
def render(s: Int) =
<.div(s)
}
private val Count0 = ReactComponentB[CounterProps]("Count")
.initialState(0)
.renderBackend[Count0Backend]
.componentDidMount(scope =>
Callback {
val disp = scope.props.d
scope.backend.end = Option(disp.stream
.subscribe(disp.observer(x => scope.modState(_ => x.count).runNow())))
}
)
.componentWillUnmount(scope =>
Callback(scope.backend.end.map(_.cancel)))
.build
There is a mutable field end: Option[Cancelable]
in the backend.
When the component is mounted, a new subscription to our stream is made. When the component is unmounted, this subscription is canceled. We use the end
variable to cancel the subscription.
Here is the Counter
element, that contains buttons and a count:
val Counter = ReactComponentB[CounterProps]("Counter")
.render_P { prop => <.div(^.id:="counter-container",
Count0(prop),
<.div(
Button(ButtonProps(false, 1, prop)),
Button(ButtonProps(true, 1, prop))
)
)
}.build
So, that’s all. When we need our element to be changeable, we must subscribe it to the stream. When we want to change the state, we use a pure function through the dispatch method.
Now it’s time for a more complex example.
Let’s add more buttons.
The buttons of the second row are “+” and “-” buttons, but they change the changeType
to 2
(the buttons of the first row change the value of this field to 1
).
And I added a “Clear” button, that change the field count
to 0
and the changeType
to 3
.
Now we have 3 types of changes. And we have only one subscriber that reacts to any change of the application state.
Let’s add more subscribers.
My second subscriber will react to any change of type 1
and type 3
. But if the change is of type 2
, it will react only when the count
value is even.
class CountBackend($: BackendScope[CounterProps, Int]) {
var end: Option[Cancelable] = None
def render(s: Int) =
<.div(s)
}
private val Count = ReactComponentB[CounterProps]("Count")
.initialState(0)
.renderBackend[CountBackend]
.componentDidMount(scope =>
Callback {
val disp = scope.props.d
scope.backend.end = Option(disp.stream
.filter(_ match {
case x if x.changeType == 1 => true
case x if x.changeType == 3 => true
case x if x.changeType == 2 & x.count % 2 == 0 => true
case _ => false
})
.subscribe(disp.observer(
x => scope.modState(_ => x.count).runNow())
))
}
)
.componentWillUnmount(scope =>
Callback(scope.backend.end.map(_.cancel)))
.build
My third subscriber will ignore any change of type 1
:
class Count1Backend($: BackendScope[CounterProps, Int]) {
var end: Option[Cancelable] = None
def render(s: Int) =
<.div(s)
}
private val Count1 = ReactComponentB[CounterProps]("Count")
.initialState(0)
.renderBackend[Count1Backend]
.componentDidMount(scope =>
Callback {
val disp = scope.props.d
scope.backend.end = Option(disp.stream
.filter(_ match {
case x if x.changeType == 2 => true
case x if x.changeType == 3 => true
case _ => false
})
.subscribe(disp.observer(
x => scope.modState(_ => x.count).runNow())
))
}
)
.componentWillUnmount(scope =>
Callback(scope.backend.end.map(_.cancel)))
.build
We have 3 independent types of changes, and 3 independent subscribers. Every subscriber is watching for changes of the application state, and it reacts only if it must by changing its own state.
See the demo.
Here is the full code:
package example
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom._
import japgolly.scalajs.react.ReactComponentB
import monix.execution.Cancelable
import monix.execution.Scheduler.Implicits.global
case class CounterProps(d: Dispatcher[CounterState])
object Counter {
case class ButtonProps(increment: Boolean, step: Int, props: CounterProps)
class ButtonBackend($: BackendScope[ButtonProps, Unit]) {
def click: Callback = {
val props = $.props.runNow()
val increment = props.increment match { case true => 1
case _ => -1
}
Callback(props.props.d.dispatch(
s => s.copy(count = s.count + increment)
.copy(changeType = props.step)
))
}
def render(p: ButtonProps) =
<.button(^.onClick --> click, p.increment match {
case true => "+"
case _ => "-"
})
}
private val Button = ReactComponentB[ButtonProps]("Button")
.renderBackend[ButtonBackend]
.build
class ClearBackend($: BackendScope[CounterProps, Unit]) {
def click: Callback = {
Callback($.props.runNow().d.dispatch(
s => s.copy(count = 0).copy(changeType = 3)
)
)
}
def render =
<.button(^.onClick --> click, "clear")
}
private val Clear = ReactComponentB[CounterProps]("Clear")
.renderBackend[ClearBackend]
.build
class Count0Backend($: BackendScope[CounterProps, Int]) {
var end: Option[Cancelable] = None
def render(s: Int) =
<.div(s)
}
private val Count0 = ReactComponentB[CounterProps]("Count")
.initialState(0)
.renderBackend[Count0Backend]
.componentDidMount(scope =>
Callback {
val disp = scope.props.d
scope.backend.end = Option(disp.stream
.subscribe(disp.observer(x => scope.modState(_ => x.count).runNow())))
}
)
.componentWillUnmount(scope =>
Callback(scope.backend.end.map(_.cancel)))
.build
class CountBackend($: BackendScope[CounterProps, Int]) {
var end: Option[Cancelable] = None
def render(s: Int) =
<.div(s)
}
private val Count = ReactComponentB[CounterProps]("Count")
.initialState(0)
.renderBackend[CountBackend]
.componentDidMount(scope =>
Callback {
val disp = scope.props.d
scope.backend.end = Option(disp.stream
.filter(_ match {
case x if x.changeType == 1 => true
case x if x.changeType == 3 => true
case x if x.changeType == 2 & x.count % 2 == 0 => true
case _ => false
})
.subscribe(disp.observer(
x => scope.modState(_ => x.count).runNow())
))
}
)
.componentWillUnmount(scope =>
Callback(scope.backend.end.map(_.cancel)))
.build
class Count1Backend($: BackendScope[CounterProps, Int]) {
var end: Option[Cancelable] = None
def render(s: Int) =
<.div(s)
}
private val Count1 = ReactComponentB[CounterProps]("Count")
.initialState(0)
.renderBackend[Count1Backend]
.componentDidMount(scope =>
Callback {
val disp = scope.props.d
scope.backend.end = Option(disp.stream
.filter(_ match {
case x if x.changeType == 2 => true
case x if x.changeType == 3 => true
case _ => false
})
.subscribe(disp.observer(
x => scope.modState(_ => x.count).runNow())
))
}
)
.componentWillUnmount(scope =>
Callback(scope.backend.end.map(_.cancel)))
.build
private val Counter = ReactComponentB[CounterProps]("Counter")
.render_P { prop =>
<.div(^.id := "counter-container",
Count0(prop),
Count(prop),
Count1(prop),
<.div(
Button(ButtonProps(false, 1, prop)),
Button(ButtonProps(true, 1, prop))
),
<.div(
Button(ButtonProps(false, 2, prop)),
Button(ButtonProps(true, 2, prop))
),
<.div(Clear(prop))
)
}.build
def apply(props: CounterProps) = Counter(props)
}
You can find the full code on GitHub.
What we achieved
- Unidirectional data flow
- Granularity of every React element
- Immutable application state
- Concise and clear code
And we did it with Scala.js!