GithubHelp home page GithubHelp logo

rpiaggio / shironeko Goto Github PK

View Code? Open in Web Editor NEW

This project forked from oleg-py/shironeko

0.0 1.0 0.0 900 KB

Frontend state management library for cats-effect

Scala 87.81% HTML 3.46% JavaScript 8.74%

shironeko's Introduction

Shironeko

Maven central

A cat that can manage state

Shironeko is a state management library for Scala.js with the following goals:

  • Make simple things trivial and hard things possible without boilerplate
  • Support writing logic in pure FP, with cats-effect and final tagless style
  • Be developer and IDE-friendly

Currently only supports Scala 2.12

Dependencies

libraryDependencies += "com.olegpy" %%% "shironeko-core" % "0.1.0-M1"
libraryDependencies += "com.olegpy" %%% "shironeko-slinky" % "0.1.0-M1"

Core abstractions

shironeko is largely relying on cats-effect and fs2. Every action that happens is represented by F[Unit] (for cats-effect compatible effect type F). All data that is rendered comes in fs2.Stream. For state cells that can be changed manually, SignallingRef[F, A] is used, as it provides a stream of changed values via .discrete method.

  • Store is a class containing the data your application is showing
  • Container is a (react) component which is able to show data from the Store and has FFI compatibilities for react interop
  • Connector is an object which links Containers to an instance of a Store

Example

Basics

Let's say we'll be using tagless final style. We want to create a simple counter which you can increment or decrement, so we keep that state in a store:

class Store[F[_]](val counter: SignallingRef[F, Int])
object Store {
  def make[F[_]: Concurrent] = SignallingRef[F, Int](0).map(new Store[F](_))
}

To get any updates, we need to first create a Connector for our application.

object Connector extends SlinkyConnector[Store]

Connectors define a number of base classes to be extended by other singleton objects. Here, let's use a simple container without any props:

object CounterDisplay extends Connector.ContainerNoProps {
  override type State = Int
  
  override def subscribe[F[_]: Subscribe] = getAlgebra.counter.discrete
  override def render[F[_]: Render](state: State) = {
    div(
      button(onClick := toCallback { getAlgebra.counter.modify(_ - 1) })("-"),
      s"Current value is $state",
      button(onClick := toCallback { getAlgebra.counter.modify(_ + 1) })("+"),
    )
  }
}

Extending any container class gives access to the:

  • Store instance (getAlgebra) for F effect type in subscribe and render
  • Concurrent instance (getConcurrent) for F effect type in subscribe and render
  • FFI type Exec (getExec) for F in render only, for tagless style, or in both render and subscribe when using concrete effect type.

Exec allows you to use exec(fa) to schedule fa for later execution, and also toCallback utility, converting fa to impure callback (() => Unit)

With this, we have enough to build our app. I will be using cats.effect.IO as effect type, and the easiest way to get all needed typeclass instances is by extending IOApp:

object Main extends IOApp {
  override def run(args:  List[String])  = {
    Store.make[IO].flatMap(store => IO.suspend { 
      val root = dom.document.getElementById("root")
      ReactDOM.render(root, Connector(store)(CounterDisplay()))
      IO.never.widen[ExitCode]
    })
  }
  
  @JSExportTopLevel("main")
  def main(): Unit = super.main(Array())
}

More complex states

It's quite rare that you can get away with just one SignallingRef. For this example, let's save the number of times counter has been altered in a separate SignallingRef:

class Store[F[_]](
  val counter: SignallingRef[F, Int],
  val changes: SignallingRef[F, Int],
)

object Store {
  def make[F[_]: Concurrent] =
    (SignallingRef[F, Int](0), SignallingRef[F, Int](0)).mapN(new Store[F](_, _))
}

Store DSL

Given how unwieldy these constructors can grow, shironeko has a DSL that you can use to create the store more declaratively:

class Store[F[_]](dsl: StoreDSL[F]) {
  import dsl._
  val counter = cell(0)
  val changes = cell(0)
}

object Store {
  def make[F[_]: Concurrent] =
    StoreDSL[F].use(new Store[F](_).pure[F])
}

StoreDSL is a Resource that cannot be used after the constructor has been executed. Its methods bypass referential transparency to create signalling refs immediately. Because of this, you must use val, not lazy val or def and also you cannot store the dsl somewhere for other state allocation (it'll crash).

Also, if you use DSL methods in objects defined inside your store, beware that objects are initialized lazily, when first demanded.

Writing actions

You don't have to put every state update inline into the rendered component. When logic grows reasonably complex, you can write them anywhere - just remember that store and Concurrent instance are given for you in the implicit scope in the body of render. For example, you can write:

object CounterActions {
  def increment[F[_]: Monad](implicit S: Store[F]): F[Unit] =
    S.counter.modify(_ + 1) >> S.changes.modify(_ + 1)
    

  def decrement[F[_]: Monad](implicit S: Store[F]): F[Unit] =
    S.counter.modify(_ - 1) >> S.changes.modify(_ + 1)
}

And, since it's just plain effect datatypes, there's zero reason why we can't just factor out repeating parts:

object CounterActions {
  private[this] def change[F[_]: Monad](by: Int)(implicit S: Store): F[Unit] =
    S.counter.modify(_ + by) >> S.changes.modify(_ + 1)
    
  def increment[F[_]: Monad](implicit S: Store[F]): F[Unit] =
    change[F](1)

  def decrement[F[_]: Monad](implicit S: Store[F]): F[Unit] =
    change[F](-1)
}

You may delete increment/decrement and use change directly. Your call.

Combining multiple cells

Let's revisit our display component. We need to show multiple values at the same time. We can't flatMap two calls to discrete - that gives us an endless stream of pairs of all values ever came through our app. What is needed is parallel combination - take the latest value that has arrived in each SignallingRef, and emit these pairs of latest values.

Shironeko provides a blackbox macro util.combine that allows you to construct a stream of case class instances out of several streams, one per each field. It also works for tuples, if you don't like nicely named fields.

The construct is combine[A].from(stream1, stream2, ...). A concrete type A needs to always be specified, as it guides macro inference.

object CounterDisplay extends Connector.ContainerNoProps {
  case class State(value: Int, changed: Int)
  
  override def subscribe[F[_]: Subscribe]: Stream[F, State] = {
    val S = getAlgebra
    combine[State].from(
      S.counter.discrete,
      S.changes.discrete
    )
  }

  override def render[F[_]: Render](state: State) = {
    div(
      button(onClick := toCallback { CounterActions.decrement[F] })("-"),
      s"Current value is ${state.value}, changed ${state.changed} times",
      button(onClick := toCallback { CounterActions.increment[F] })("+"),
    )
  }
}

Using event-based model

TODO

shironeko's People

Contributors

oleg-py avatar cquiroz avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.