Categories
scala.js series

Part 2: The SMAkkaR.js Stack- Using monocle and akka to facilitate model and component reusability in a react scala.js application

In our our previous blog we used akka to manage the state of our application. Here is a list of benefits we get so far:

  • Leveraging akka as the delegated entity to modify the state of a single page application.
  • Unlike redux, we can use type pattern matching to identify a message type, making the application far more robust.
  • We can also leverage the akka messaging pattern for all sorts of other cool applications such as authentication, tracking events, or state history.

In this blog, we will use the capabilities of monocle. Through monocle and akka, we will be able to easily design react components that can update themselves without the react component having to know where in the application model the data resides.

Let me explain with the example I am about to show you. If our application, has many counters in different hierarchy levels and in many different types of react components, we can update any of these counters using the same code! When a react component has a counter, neither component nor the update method need to know where the counter came from for the application to increment it. I hope the message sort of makes sense at this point. So let’s start!

We will start with the code from our previous blog or we can also use the repo for this blog and track the changes through commit tags.

Our first step is to run the application from previous blog repo or use this blog’s repo and checkout tag “initial-with-no-monocle”:

~/projects/my-app % sbt dev 

As discussed in previous blog, we should see two counters, and each of them should increment it’s value by clicking on it.

Setting up monocle

Let’s add monocle to our build.sbt file first:

libraryDependencies ++= Seq(
  "com.github.julien-truffaut" %%%  "monocle-core"  % "2.0.4",
  "com.github.julien-truffaut" %%%  "monocle-macro" % "2.0.4",
  "com.github.julien-truffaut" %%%  "monocle-law"   % "2.0.4" % "test"
)

Reload build.sbt file and make sure it compiles and runs the same way.

Adding our first lenses

We start by replacing the older case class update with update through lenses. For now, we will add the lenses into file hello.world.messaging.MessageHandler.scala

// File MessageHandler.scala
package hello.world.messaging

...
...

// we use these two lens objects to update each counter
  val counter1Lens   : Lens[Application, Counter] = GenLens[Application](_.counter1)
  val counter2Lens   : Lens[Application, Counter] = GenLens[Application](_.counter2)

  val system = ActorSystem("ApplicationStoreActorSystem")
...
...
    private def messageUpdate: PartialFunction[Any, Application] = {
      case IncrementCounter1 =>
        log.info(s"Increment  counter1")
        //models.topModel.app.copy(counter1 = Counter(models.topModel.app.counter1.value + 1))
        counter1Lens.modify(c => c.copy(c.value+1))(models.topModel.app)

      case IncrementCounter2 =>
        log.info(s"Increment counter2")
        //models.topModel.app.copy(counter2 = Counter(models.topModel.app.counter2.value + 1))
        counter2Lens.modify(c => c.copy(c.value+1))(models.topModel.app)
    }
  }

}

Moving our lenses to a react component as a methodology

Application should still run exactly the same way as before. But at this point, we will perform a series of steps so we can be more DRY, these two lenses look awfully similar don’t they?

In our quest for a more DRY solution, we will start by taking the lenses out of file MessageHandler.scala and move it to react component App.scala.

// File App.scala
package hello.world
import hello.world.messaging.ApplicationMessageListContainer.IncrementCounter
import hello.world.messaging.MessageHandler
import hello.world.models.{Application, Counter}
import slinky.core._
import slinky.core.annotations.react
import slinky.web.html._

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import monocle.{Lens, Optional}
import monocle.macros.GenLens
@JSImport("resources/App.css", JSImport.Default)
@js.native
object AppCSS extends js.Object

@JSImport("resources/logo.svg", JSImport.Default)
@js.native
object ReactLogo extends js.Object



object ApplicationProxy {
  var update:() => Unit = () => ()
}

@react class App extends StatelessComponent {
  type Props = Unit
  private val css = AppCSS

// We move our lenses to here
  val counter1Lens = GenLens[Application](_.counter1)
  val counter2Lens = GenLens[Application](_.counter2)


  override def componentWillMount() = {
    ApplicationProxy.update = () => {
      this.forceUpdate()
    }
  }
  def render() = {

    div(className := "App")(
      header(className := "App-header")(
        img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"),
        h1(className := "App-title")("Welcome to React (with Scala.js!)")
      ),
      p(className := "App-intro")(
        "To get started, edit ", code("App.scala"), " and save to reload."
      ),
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}",
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter1Lens)
        })
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter2Lens)
        })
      )
    )
  }
}

Don’t try running anything as you will see compile errors at this point. Now we update our message types so they can contain a lens by updating file ApplicationMessageListContainer.scala:

// File ApplicationMessageListContainer.scala
package hello.world.messaging

import hello.world.models.{Application, Counter}
import monocle.Optional

trait ApplicationFrontEndMessage

object ApplicationMessageListContainer {
    case class IncrementCounter1(lens: Lens[Application, Counter]) extends ApplicationFrontEndMessage
    case class IncrementCounter2(lens: Lens[Application, Counter]) extends ApplicationFrontEndMessage

}

And finally, we remove the lenses from MessageHandler.scala and modify the message handling:

// File MessageHandler.scala
...
...
// remove lenses from here!
//  val counter1Lens   : Lens[Application, Counter] = GenLens[Application](_.counter1)
//  val counter2Lens   : Lens[Application, Counter] = GenLens[Application](_.counter2)
...
..
    private def messageUpdate: PartialFunction[Any, Application] = {
      case m:IncrementCounter1 =>
        log.info(s"Increment  counter1")
        // now the lens is in the message!
        m.lens.modify(c => c.copy(c.value+1))(models.topModel.app)

      case m:IncrementCounter2 =>
        log.info(s"Increment counter2")
        // now the lens is in the message!
        m.lens.modify(c => c.copy(c.value+1))(models.topModel.app)
    }
  }

}

Now stuff should work exactly as before. Make sure you have all the imports available.

Generalizing our message handler so we can be fully DRY

It looks a bit cleaner, but we are not DRY yet. We notice Increment* messages are identical at this point. Let’s make a single IncrementCounter message!

We start by modifying our message type in file ApplicationMessageListContainer.scala:

// File ApplicationMessageListContainer.scala
package hello.world.messaging

import hello.world.models.{Application, Counter}
import monocle.Optional

trait ApplicationFrontEndMessage

object ApplicationMessageListContainer {
  //object IncrementCounter1 extends ApplicationFrontEndMessage
  //object IncrementCounter2 extends ApplicationFrontEndMessage

  case class IncrementCounter(lens:Optional[Application, Counter]) extends ApplicationFrontEndMessage
}

Notice that we are no longer using Lens[_,_] types, we are using Optional which is a more flexible lens that allows us to deal with Map, List, Vector etc.

In our App.scala we perform the following changes:

// File App.scala
...
import monocle.{Lens, Optional}
import monocle.macros.GenLens
...
// Notice we are now using Optional
  val counter1Lens   : Optional[Application, Counter] = GenLens[Application](_.counter1).asOptional
  val counter2Lens   : Optional[Application, Counter] = GenLens[Application](_.counter2).asOptional
  val counterGroupLens   : Optional[Application, Vector[Counter]] = GenLens[Application](_.counterList).asOptional
..
..
  def render() = {
...
...
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}",
// One message used IncrementCounter
        onClick := (_ => {
// One message used IncrementCounter
          MessageHandler.actor ! IncrementCounter(counter1Lens)
        })
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
// One message used IncrementCounter
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter2Lens)
        })
      ),
    )
  }
}

At this point the app should be up and running, and again with exactly the same behaviour as usual.

At this point, we were able to generalize the update counter model. However, there is no evidence how reusable this approach is through different react components in different hierarchies. This next session addresses this very matter.

Passing monocle lenses through react properties and composing them to enable component and model reusability

We will add a new react component to this application, a group of counters in it’s own visual react component. These counters will use the same message handler to update the models. We will compose lenses to achieve this.

Let’s create the new component first.

// New file CounterListComponent
package hello.world

import hello.world.messaging.MessageHandler
import hello.world.messaging.ApplicationMessageListContainer._
import hello.world.models.{Application, Counter}
import slinky.core._
import slinky.core.annotations.react
import slinky.web.html._
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import monocle.{Iso, Lens, Optiona}
import monocle.function.At
import monocle.macros.GenLens
import monocle.function.all._
import monocle.function.At.at
import monocle.std.list._



@react class CounterListComponent extends StatelessComponent {
  type Props = Optional[Application, Vector[Counter]]
  private val css = AppCSS


  def render() = {

    val counterListLens = props  // for doc purposes

    // our lens gives two way access to Vector[Counter], get and update
    val counterListOption = counterListLens.getOption(models.topModel.app)

    val counterDomElementList = counterListOption match {
      case Some(list) => list.zipWithIndex.map(c => {
// We compose a lens per counter
        val counterLens: Optional[Application, Counter] = props composeOptional index(c._2)
        div(
          style := js.Dynamic.literal(
            padding = "10px",
          ),
          button(
            s"click here to increment: ${
              c._1.value
            }",
            onClick := (_ => {
              MessageHandler.actor ! IncrementCounter(counterLens)
            })
          )
        )
      }
      )
    }


    div(
      className := "CounterList",
      style := js.Dynamic.literal(
        padding = "0px 150px 0px 150px",
      )
    )(
      div(
        style := js.Dynamic.literal(
          background = "deeppink",
          padding = "10px",
        ),
        counterDomElementList
      )
    )
  }
}

The code above is creating a lens for each counter inside the map. In the onClick we send the lens to the actor. We expect this message to end in the same place in MessageHandler.scala as the other messages passed in App.scala.

Now we include component CounterListComponent in App.scala:

// File App.scala
package hello.world

...
...
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter(counter2Lens)
        })
      ),
      br(),br(),
// new component added
      CounterListComponent(counterGroupLens)
    )
  }
}

At this point the application should look like the following. And you should be able to increment any of the counter buttons independently:

Cool isn’t it? Now lets summarize on what we achieved in this blog:

  • Passing lenses as react properties
    • Allows for the react component to be oblivious of where the data is coming from. This is not a big deal if the component is read only, but it’s a great deal if the react component needs to modify itself.
  • Composing lenses as they get passed through component hierarchies enables separation of concern
    • Stateful react components don’t need to care where the data is placed in the data model. The same goes for it’s child components.

And here is how we do it

  • Lenses are created within react components
  • Pass lenses as react properties, not so much data
  • Compose lenses as you pass them as properties to your child components
  • Generalize model updates through akka messages and pattern matching

Categories
scala.js series

Part 1: Using akka and react to organize your single page scala.js application

This blog should resonate specially to those users who are looking for something like Redux to organize a single page React+Scala.js application.

We will try to manage a single page application from a single nested case class. All updates to this model will be delegated to an AKKA actor. Later in this blog, we will be using Monocle to simplify the update of the application model.

The way we are going to do this is by having two separate buttons. Each button will show an integer that will be incremented with a click.

We will start very simple, and as we progress, we will enhance our code.

Project setup

We will start simple from the react slinky template.

% sbt new shadaj/create-react-scala-app.g8

For the sake of consistency, please keep the default values. After running command dev under sbt you should see the following browser content if everything goes well:

~/projects/my-app % sbt dev 

Now we add the akka dependencies to your build.sbt:

// build.sbt
libraryDependencies += "org.akka-js" %%% "akkajsactor" % "2.2.6.5"
libraryDependencies += "org.akka-js" %%% "akkajsactortyped" % "2.2.6.5"

At this point, we are ready for coding!

We then setup the application model

We will be creating a package called “models” under our default package “hello.world”. We then create file Application.scala with the following code:

// File Application.scala
package hello.world.models

case class Counter(value: Integer = 1)

case class ApplicationContainer(var app: Application = Application())

case class Application
(
  counter1: Counter = Counter(),
  counter2: Counter = Counter()
)

That is it for our application model. We keep it simple.

Next we create a package file under “models” . We use this package to retrieve and initialize our application model:

//package.scala
package hello.world

package object models {
  val topModel = ApplicationContainer()
}

We then render our buttons showing the counter values

Now we add our buttons. At this point, they won’t do anything at all. We edit file App.scala and add 2 html buttons inside method render():

// File App.scala
....
..
def render() = {

    div(className := "App")(
      header(className := "App-header")(
        img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"),
        h1(className := "App-title")("Welcome to React (with Scala.js!)")
      ),
      p(className := "App-intro")(
        "To get started, edit ", code("App.scala"), " and save to reload."
      ),
// add the following: start
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}"
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}"
      )
// end of additions
    )
  }

At this point, you should see two additional, non functional buttons, available in the browser:

One ugly hack is required to make this work, just one

In order for the counters to increment, we need to tell the react application to update. For this to work, we need to access react method forceUpdate() externally from an akka actor. This is done by modifying the following in App.scala:

// File App.scala
....
// Added ApplicationProxy for external access of forceUpdate()
object ApplicationProxy {
  var update:() => Unit = () => ()
}

@react class App extends StatelessComponent {
  type Props = Unit
  private val css = AppCSS

// Added componentWillMount() to "steal" forceUpdate() out of react
  override def componentWillMount() = {
    ApplicationProxy.update = () => {
      this.forceUpdate()
    }
  }

  def render() = {
.....

Enter the awesomeness of akka

Akka deals with messages. This means we first need to build our message types before we do anything. Since we only increment two counters, we are going to have two message names, IncrementCounter1 and IncrementCounter2. We create package “messaging” under root package “hello.world” and then create file ApplicationMessageListContainer.scala:

// File ApplicationMessageListContainer.scala
package hello.world.messaging

trait ApplicationFrontEndMessage

object ApplicationMessageListContainer {
  object IncrementCounter1 extends ApplicationFrontEndMessage
  object IncrementCounter2 extends ApplicationFrontEndMessage
}

Now we are ready to create the messaging code. Under same package “hello.world.messaging” we create file MessageHandler.scala:

// File MessageHandler.scala
package hello.world.messaging

import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
import hello.world.messaging.ApplicationMessageListContainer.{IncrementCounter1, IncrementCounter2}
import hello.world.{ApplicationProxy, models}
import hello.world.models.{Application, Counter}

object MessageHandler {

  val system = ActorSystem("ApplicationStoreActorSystem")

  val actor = system.actorOf(Props[ApplicationActor], name = "ApplicationInfoActor")

  class ApplicationActor extends Actor with ActorLogging {
    def receive = {
      case m: ApplicationFrontEndMessage => {
        models.topModel.app = messageUpdate(m)
        ApplicationProxy.update()
      }
    }

    private def messageUpdate: PartialFunction[Any, Application] = {
      case IncrementCounter1 =>
        log.info(s"Increment  counter1")
//monocle will improve this ugly update
        models.topModel.app.copy(counter1 = Counter(models.topModel.app.counter1.value + 1))


      case IncrementCounter2 =>
        log.info(s"Increment counter2")
//monocle will improve this ugly update
        models.topModel.app.copy(counter2 = Counter(models.topModel.app.counter2.value + 1))
    }
  }

}

The messaging system is basically copy-modifying the current model. With a 2 level case class, it already looks pretty ugly. We will be using monocle to address this ugliness in my next blog.

Of course, we need to add a listener to our buttons (onClick) in file App.scala for the buttons to trigger changes:

// File App.scala
package hello.world

import hello.world.messaging.MessageHandler
import hello.world.messaging.ApplicationMessageListContainer._
import slinky.core._
import slinky.core.annotations.react
import slinky.web.html._
.......
....
  def render() = {
    div(className := "App")(
      header(className := "App-header")(
        img(src := ReactLogo.asInstanceOf[String], className := "App-logo", alt := "logo"),
        h1(className := "App-title")("Welcome to React (with Scala.js!)")
      ),
      p(className := "App-intro")(
        "To get started, edit ", code("App.scala"), " and save to reload."
      ),
      button(
        s"Click to increment counter1 ${models.topModel.app.counter1.value}",
//onClick added
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter1
        })
      ),
      br(),br(),
      button(
        s"Click to increment counter2 ${models.topModel.app.counter2.value}",
//onClick added
        onClick := (_ => {
          MessageHandler.actor ! IncrementCounter2
        })
      )
    )
  }

Now take the app for a ride and click on those buttons, they should increment as you click on them:

Not too bad isn’t it? Now you have a single page app with a model organized by akka messaging. In my next blog, we will add monocle to manage our application model.

Code for this blog can be accessed through the following repo:

https://github.com/scala-blog/akka-react-no-monocle/tree/master