A Counter

Let's implement the "Hello, World!" of UIs: A counter that can be incremented or decremented by clicking a button.

State

In Scala, the natural choice for an immutable state container is a case class:

case class State(counter: Int = 0)

Actions

State can only be updated through actions dispatched to the store. We typically encode the set of actions as an ADT:

enum Action:
  case Inc(amount: Int)
  case Reset

Store

With the State and Action types in hand, we can set up our store, which is a centralized place for all state updating logic.

import cats.effect.*

object Store:

  // When all actions are pure state updates (no effects), we can use the `pure` constructor.
  def apply[F[_]](implicit
      F: Concurrent[F]
  ): Resource[F, ff4s.Store[F, State, Action]] =
    ff4s.Store.pure[F, State, Action](State()):
      case (Action.Inc(amount), state) =>
        state.copy(counter = state.counter + amount)
      case (Action.Reset, state) => state.copy(counter = 0)

The fact that store is a Resource will turn out to be extremely useful later (think WebSockets, subscribing to state changes, etc.). Be sure to check out the other examples to see more elaborate store logic.

View

Finally, we describe how our page should be rendered using the built-in DSL for HTML markup.

trait View:
  self: ff4s.Dsl[State, Action] =>

  val view =

    import html.*

    useState: state =>
      div(
        cls := "counter-example", // cls b/c class is a reserved keyword in scala
        div(s"value: ${state.counter}"),
        button(
          cls := "counter-button",
          "Increment",
          onClick := (_ => Some(Action.Inc(1)))
        ),
        button(
          cls := "counter-button",
          "Decrement",
          onClick := (_ => Some(Action.Inc(-1)))
        ),
        button(
          cls := "counter-button",
          "Reset",
          onClick := (_ => Some(Action.Reset))
        )
      )

To turn this into an app, we need a small amount of boilerplate.

// App.scala
class App[F[_]](using F: Concurrent[F])
    extends ff4s.App[F, State, Action]
    with View:
  override val store = Store[F]

// Main.scala
// this defines an appropriate `main` method for us
object Main extends ff4s.IOEntryPoint(new App) // uses cats.effect.IO for F