Scala.js and unidirectional data flow

Scala.js and unidirectional data flow

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.

  1. In our React application we have many stateful elements. If we want to change an element, we have to change its state.
  2. 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.
  3. 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.
  4. 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:

  1. It’s an observable. It’s shaped like an observable, and has all the same operators.
  2. It’s an observer.
  3. When subscribed to as an observable, will emit any value you “next” into it as an observer.
  4. It multicasts.
  5. Subjects cannot be reused after they’re unsubscribed, completed or errored.
  6. 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:

  1. increment – is it “+” or “-“
  2. step – some characteristic of the button
  3. props – the Dispatcher placed to the CounterProps

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

  1. Unidirectional data flow
  2. Granularity of every React element
  3. Immutable application state
  4. Concise and clear code

And we did it with Scala.js!

 

 

 

 

Please follow and like us:

About Alexandre Kremlianski

Scala / Scala.js / JavaScript programmer

Leave a Reply

Your email address will not be published. Required fields are marked *