Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/oleg-py/shironeko
Frontend state management library for cats-effect
https://github.com/oleg-py/shironeko
Last synced: 2 months ago
JSON representation
Frontend state management library for cats-effect
- Host: GitHub
- URL: https://github.com/oleg-py/shironeko
- Owner: oleg-py
- Created: 2018-09-18T17:13:40.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2021-11-21T09:31:53.000Z (about 3 years ago)
- Last Synced: 2024-10-11T08:44:28.258Z (3 months ago)
- Language: Scala
- Homepage:
- Size: 920 KB
- Stars: 27
- Watchers: 5
- Forks: 3
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Shironeko
![Maven central](https://img.shields.io/maven-central/v/com.olegpy/shironeko-core_2.12.svg?style=flat-square)*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-friendlyCurrently only supports Scala 2.12
# Dependencies
```scala
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:```scala
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.
```scala
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:```scala
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`:```scala
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:```scala
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:```scala
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 `object`s 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:```scala
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:```scala
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.```scala
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