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