crystal
is a toolbelt to help build reactive UI apps in Scala by providing:
- A structure for managing server roundtrips (
Pot
). - Wrappers for values derived from state with a callback function to modify them (
ViewF
,ViewOptF
,ViewListF
).
Additionally, for scalajs-react
apps it provides:
- Components to hold global state and context (
StateProvider
,ContextProvider
). - A component that dynamically renders values from
fs2.Stream
s (StreamRenderer
). - A component that dynamically renders values from
fs2.Stream
s, allowing children to modify the value too (StreamRendererMod
). - Conversions between
Callback
s andcats-effect
effects (implicits._
). Reusability
and other utilities forPot
andView*F
(implicits._
)
The library takes a tagless approach based on cats-effect
. Logging is performed via log4cats
.
A Pot[A]
represents a value of type A
that has been requested somewhere and may or not be available yet, or the request may have failed.
It is a sum type consisting of:
Pending(<Long>)
, where theLong
is meant to store the instant of creation, in millis since epoch. It is initialized toSystem.currentTimeMillis()
by default.Ready(<A>)
.Error(<Throwable>)
.
Pot[A]
implements the following methods:
map[B](f: A => B): Pot[B]
fold[B](fp: Long => B, fe: Throwable => B, fr: A => B): B
flatten[B]: Pot[B]
(ifA <: Pot[B]
)flatMap[B](f: A => Pot[B]): Pot[B]
toOption: Option[A]
toTryOption: Option[Try[A]]
The crystal.implicits._
import will provide:
- Instances for
cats
MonadError
,Traverse
,Align
andEq
(as long as there's anEq[A]
in scope). - Convenience methods:
<Any>.ready
,<Option[A]>.toPot
,<Try[A]>.toPot
and<Option[Try[A]]>.toPot
.
The crystal.react.implicits._
import will provide:
Reusability[Pot[A]]
(as long as there's aReusability[A]
in scope).- Convenience methods:
renderPending(f: Long => VdomNode): VdomNode
renderError(f: Throwable => VdomNode): VdomNode
renderReady(f: A => VdomNode): VdomNode
A ViewF[F, A]
wraps a value of type A
and a callback to modify it effectfully: (A => A) => F[Unit]
.
It is useful for passing state down the component hierarchy, allowing desdendents to modify it.
Provides the following methods:
get: A
- returns the wrappedA
.mod(f: A => A): F[Unit]
- returns the effect for modifying the state usingf
.set(a: A): F[Unit]
=mod(_ => a)
.modAndGet(f: A => A): F[A]
- same asmod(f)
but returns the modifiedA
.withOnMod(f: A => F[Unit])
- creates a newViewF
that chains the passed effect whenevermod
(orset
) is called.zoom
methods - creates a newViewF
focused on a part ofA
. This method can take either raw getter and setter functions or amonocle
Lens
,Optional
,Prism
orTraversal
.as(iso: Iso[A, B])
- creates a newViewF[F, B]
.asOpt
- creates a newViewOptF[F, A]
.asList
- creates a newViewListF[F, A]
.
ViewOptF[F, A]
and ViewListF[F, A]
are variants that hold a value known to be an Option[A]
or List[A]
respectively. They are returned when zoom
ing using Optional
, Prism
or Traversal
.
Requires the following implicits in scope:
Async[F]
ContextShift[F]
The crystal.react.implicits._
import will provide:
Reusability[ViewF[F, A]]
,Reusability[ViewOptF[F, A]]
andReusability[ViewListF[F, A]]
, based solely on the wrapped valueA
(and as long as there's aReusability[A]
in scope).ViewF.fromState[F]($: BackendScope[_, S])
: create aViewF[F, S]
fromscalajs-react
'sBackendScope
.
StreamRenderer.build(fs2.Stream[F, A])
will create a component that takes a rendering function Pot[A] => VdomNode
as properties.
The component will keep a Pot[A]
as state, and will use the rendering function to render such state.
State initialized to Pending
upon mounting; and then set to Ready(<A>)
with each element received in the Stream
, or Error(<Throwable>)
if the Stream
fails.
You should store the component (in a Backend
or in State
for example) and reuse it. Do not build
it on each render
.
Requires the following implicits in scope:
ConcurrentEffect[F]
Logger[F]
Reusability[A]
- (Optionally)
Reusability[Pot[A] => VdomNode]
. If not provided, the render function will never be reused.
Same as StreamRenderer
but allows the rendering function to modify the state. Therefore, the rendering function will be a Pot[ViewF[F, A]] => VdomNode
instead.
This is useful, for example, when subscribing to a stream from a server and children can invoke mutations in the server. Children can chain the ViewF
's mod/set
effect to the invocation of the mutation in order to display the change immediately instead of having to wait for the roundtrip to the server.
When the value is modified by children in such a way, values from the stream will not be propagated during a period of time. Otherwise, this would caulse flicker in the UI if the children modify the value repeatedly in a short period of time, as the intermediate values are received in the stream. By default, the hold period is 2 seconds
, but it can be modified by passing a 2nd parameter holdAfterMod
to StreamRendererMod.build
.
The crystal.react.implicits._
import will provide the following methods:
<CallbackTo[A]>.to[F]: F[A] - converts a CallbackTo
to the effectF
.<Callback>.to[F]
returnsF[Unit]
. (Requires implicitSync[F]
).<BackendScope[P, S]>.propsIn[F]: F[P]
- (Requires implicitSync[F]
).<BackendScope[P, S]>.stateIn[F]: F[S]
- (Requires implicitSync[F]
),<BackendScope[P, S]>.setStateIn[F](s: S): F[Unit]
- will complete once the state has been set. Therefore, use this instead of<BackendScope[P, S]>.setState.to[F]
, which would complete immediately. (Requires implicitAsync[F]
).<BackendScope[P, S]>.modStateIn[F](f: S => S): F[Unit]
- same as above. (Requires implicitAsync[F]
).<BackendScope[P, S]>.modStateWithPropsIn[F](f: (S, P) => S): F[Unit]
- (Requires implicitAsync[F]
).<SyncIO[A]>.toCB: CallbackTo[A]
- converts aSyncIO
toCallbackTo
.<F[A]>.runAsyncCB(cb: Either[Throwable, A] => F[Unit]): Callback
- When the resultingCallback
is run,F[A]
will be run asynchronously and its result will be handled bycb
. (Requires implicitEffect[F]
).<F[A]>.runAsyncAndThenCB(cb: Either[Throwable, A] => Callback): Callback
- When the resultingCallback
is run,F[A]
will be run asynchronously and its result will be handled bycb
. The difference withrunAsyncCB
is that the result handler returns aCallback
instead ofF[A]
. (Requires implicitEffect[F]
).<F[A]>.runAsyncAndForgetCB: Callback
- When the resultingCallback
is run,F[A]
will be run asynchronously and its result will be ignored, as well as any errors it may raise. (Requires implicitEffect[F]
).<F[Unit]>.runAsyncAndThenCB(cb: Callback, errorMsg: String?): Callback
- When the resultingCallback
is run,F[Unit]
will be run asynchronously. If it succeeds, thencb
will be run. If it fails,errorMsg
will be logged. (Requires implicitEffect[F]
andLogger[F]
).<F[Unit]>.runAsyncCB(errorMsg: String?): Callback
- When the resultingCallback
is run,F[Unit]
will be run asynchronously. If it fails,errorMsg
will be logged. (Requires implicitEffect[F]
andLogger[F]
).
Please note that in all cases the the Callback
returned by .runAsync*
will complete immediately.