Crystal
crystal
is a toolbelt to help build reactive UI apps in Scala by providing:
- A structure for managing delayed values (
Pot
,PotOption
). - Wrappers for values derived from state with a callback function to modify them (
View
,ViewOpt
,ViewList
). - A way to delegate reusability to another type (
Reuse
). Useful for types for which universal reusability cannot be defined (like functions orVdomNode
). - Tight integration between
scalajs-react
andcats-effect
+fs2.Stream
s via hooks.
crystal
assumes you use a scalajs-react
's core-bundle-cb_io
, where the default sync effect is CallbackTo
and the default async effect is IO
. However, the library should compile with another bundle.
Core
Pot[A]
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
.Ready(<A>)
.Error(<Throwable>)
.
The crystal.implicits.*
import will provide:
- Instances for
cats
MonadError
,Traverse
,Align
andEq
(as long as there's anEq[A]
in scope). - Convenience extension 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).- Extesion methods:
renderPending(f: => VdomNode): VdomNode
renderError(f: Throwable => VdomNode): VdomNode
renderReady(f: A => VdomNode): VdomNode
PotOption[A]
Similar to Pot[A]
but provides one additinal state. Its values can be:
Pending
.ReadyNone
.ReadySome(<A>)
.Error(<Throwable>)
.
It is useful in some situations (see Hooks
below).
scalajs-react
View[A]
A View[A]
wraps a value of type A
and a callback to modify it effectfully: (A => A) => Callback
.
It is useful for passing state down the component hierarchy, allowing descendants to modify it.
It provides several zoom
functions for drilling down its properties. It also allows effects to be chained whenever it's modified (withOnMod
).
ViewOpt[A]
and ViewList[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
.
Reuse[A]
A Reuse[A]
wraps a value of type A
and a hidden value of another type B
such that there is a implicit Reusability[B]
.
The instance of Reuse[A]
will be reused as long as the associated value B
can be reused.
This is useful to define Reusability
for types where universal reusability can't be defined. For example, we could define reusability for a function (S, T) => U
can be turned into a Reuse[T => U]
by specifying a curried value of S
(and assuming there's a Reusability[S]
in scope).
Hooks
useSingleEffect
Provides a context in which to run a single effect at a time.
When a new effect is submitted, the previous one is canceled. Also cancels the effect on unmount.
A submitted effect can be explicitly canceled too.
If a debounce
is passed, the hooks guarantees that effect invocations are spaced at least by the specified duration.
useSingleEffect(): Reusable[UseSingleEffect]
useSingleEffect(debounce: FiniteDuration): Reusable[UseSingleEffect]
useSingleEffectBy(debounce: Ctx => FiniteDuration): Reusable[UseSingleEffect]
where
trait UseSingleEffect:
def submit(effect: IO[Unit]): IO[Unit]
val cancel: IO[Unit]
Example:
ScalaFnComponent
.withHooks[Props]
...
.useSingleEffect(1.second)
.useEffectWithDepsBy( ... => deps)( (..., singleEffect) => deps => singleEffect.submit(longRunningEffect) )
// Previous `longRunningEffect` is cancelled immediately and new one is ran after 1 second
useStateCallback
Class components allow us to specify a callback when we modify state. The callback is executed when state is modified and is passed the new state value.
This is not available in functional components. This hook seeks to emulate such functionality.
Given a state created with .useState
, the hook allows us to register a callback that will be ran once, the next time the state changes. The callback will be passed the new state value.
useStateCallbackBy[A](state: Ctx => Hooks.UseState[A]): (A => Callback) => Callback
Example:
ScalaFnComponent
.withHooks[Props]
...
.useState(SomeValue)
.useStateCallbackBy( (..., state) => state)
.useEffectBy( (..., state, stateCallback) =>
state.modState(...) >> stateCallback(value => effect(value))
) // `effect` is run with new `value`, after `modState` completes.
useStateView
Provides state as a View
.
Functionally equivalent to useState
but the View
is more practical to pass around to child components, zoom into members and define callbacks upon state change.
useStateView[A](initialValue: => A): View[A]
useStateViewBy[A](initialValue: Ctx => A): View[A]
useStateViewWithReuse
Similar to useStateView
but returns a Reuse[View[A]]
. The resulting View
is reused by its value and thus requires an implicit Reusability[A]
, as well as a ClassTag[A]
.
useStateViewWithReuse[A: ClassTag: Reusability](initialValue: => A): Reuse[View[A]]
useStateViewWithReuseBy[A: ClassTag: Reusability](initialValue: Ctx => A): Reuse[View[A]]
useSerialState
Creates component state that is reused as long as it's not updated.
useSerialState[A](initialValue: => A): UseSerialState[A]
useSerialStateBy[A](initialValue: Ctx => A): UseSerialState[A]
where
trait UseSerialState[A]:
val value: Reusable[A]
val setState: Reusable[A => Callback]
val modState: Reusable[(A => A) => Callback]
Reusability of UseSerialState[A]
and its members depends on an internal counter which is updated every time the wrapped value changes. This is useful to provide stable reusability to types where we don't have or can't define Reusability
instances.
Usage is the same as for .useState
/.useStateBy
.
useSerialStateView
Version of useSerialState
that returns a Reuse[View[A]]
.
useSerialStateView[A](initialValue: => A): Reuse[View[A]]
useSerialStateViewBy[A](initialValue: Ctx => A): Reuse[View[A]]
useAsyncEffect
Version of useEffect
that allows defining an async effect with an (also async) cleanup effect.
useEffect
allows defining a cleanup effect only when used with the default sync effect (usually CallbackTo
).
This hook should only be used when a cleanup effect is needed. To use a regular async effect, just use regular useEffect
.
useAsyncEffect(effect: IO[IO[Unit]])
useAsyncEffectBy(effect: Ctx => IO[IO[Unit]])
useAsyncEffectWithDeps[D: Reusability](deps: => D)(effect: D => IO[IO[Unit]])
useAsyncEffectWithDepsBy[D: Reusability](deps: Ctx => D)(effect: Ctx => D => IO[IO[Unit]])
useAsyncEffectOnMount(effect: IO[IO[Unit]])
useAsyncEffectOnMountBy(effect: Ctx => IO[IO[Unit]])
useEffectResult
Stores the result A
of an effect in state. The state is provided as Pot[A]
, with value Pending
until the effect completes (and Error
if it fails).
useEffectResult[A](effect: IO[A]): Pot[A]
useEffectResultBy[A](effect: Ctx => IO[A]): Pot[A]
useEffectResultWithDeps[D: Reusability, A](deps: => D)(effect: D => IO[A]): Pot[A]
useEffectResultWithDepsBy[D: Reusability, A](deps: Ctx => D)(effect: Ctx => D => IO[A]): Pot[A]
useEffectResultOnMount[A](effect: IO[A]): Pot[A]
useEffectResultOnMountBy[A](effect: Ctx => IO[A]): Pot[A]
Example:
ScalaFnComponent
.withHooks[Props]
...
.useEffectResultOnMount(UUIDGen.randomUUID)
.render( (..., uuidPot) =>
uuidPot.fold(
"Pending...",
t => s"Error! ${e.getMessage}",
uuid => s"Your fresh UUID: $uuid"
)
)
useResource
Opens a Resource[IO, A]
upon mount or dependency change, and provides its value as a Pot[A]
.
The resource is gracefully closed upon unmount or dependency change.
Note that there is no version without deps or onMount
since it doesn't make sense to open a resource in each render, especially taking into account that once the resource is acquired it will force a rerender.
useResource[D: Reusability, A](deps: => D)(resource: D => Resource[IO, A]): Pot[A]
useResourceBy[D: Reusability, A](deps: Ctx => D)(resource: Ctx => D => Resource[IO, A]): Pot[A]
useResourceOnMount[A](resource: Resource[IO, A]): Pot[A]
useResourceOnMountBy[A](resource: Ctx => Resource[IO, A]): Pot[A]
useStream
Executes and drains a fs2.Stream[IO, A]
upon mount or dependency change, and provides the latest value from the stream as a PotOption[A]
.
The fiber evaluating the stream is canceled upon unmount or dependency change.
Note that there is no version without deps or onMount
since it doesn't make sense to open a resource in each render, especially taking into account that starting the draining fiber will force a rerender, as well as every new value produced.
useStream[D: Reusability, A](deps: => D)(stream: D => fs2.Stream[IO, A]): PotOption[A]
useStreamBy[D: Reusability, A](deps: Ctx => D)(stream: Ctx => D => fs2.Stream[IO, A]): PotOption[A]
useStreamOnMount[A](stream: fs2.Stream[IO, A]): PotOption[A]
useStreamOnMountBy[A](stream: Ctx => fs2.Stream[IO, A]): PotOption[A]
The resulting PotOption[A]
takes one of these values:
Pending
: Fiber hasn't started yetReadyNone
: Fiber has started but no value has been produced by the stream yet.ReadySome(a)
:a
is the last value produced by the stream.Error(t)
: Fiber raised an exceptiont
.
useStreamView
Like useStream
but returns a PotOption[View[A]]
, allowing local modifications to the state once it's Ready
.
In other words, the state will be modified on every new value produced by the stream, and also on every invocation to set
or mod
on the View
.
useStreamView[D: Reusability, A](deps: => D)(stream: D => fs2.Stream[IO, A]): PotOption[View[A]]
useStreamViewBy[D: Reusability, A](deps: Ctx => D)(stream: Ctx => D => fs2.Stream[IO, A]): PotOption[View[A]]
useStreamViewOnMount[A](stream: fs2.Stream[IO, A]): PotOption[View[A]]
useStreamViewOnMountBy[A](stream: Ctx => fs2.Stream[IO, A]): PotOption[View[A]]
useStreamResource
Given a Resource[IO, fs2.Stream[IO, A]]
, combines useResource
and useStream
on it.
In other words, when mounting or depdency change, the resource is allocated and the resulting stream starts being evaluated.
Upon unmount or dependency change, the evaluating fiber is cancelled and the resource closed.
useStreamResource[D: Reusability, A](deps: => D)(streamResource: D => Resource[IO, fs2.Stream[IO, A]]): PotOption[A]
useStreamResourceBy[D: Reusability, A](deps: Ctx => D)(streamResource: Ctx => D => Resource[IO, fs2.Stream[IO, A]]): PotOption[A]
useStreamResourceOnMount[A](streamResource: Resource[IO, fs2.Stream[IO, A]]): PotOption[A]
useStreamResourceOnMountBy[A](streamResource: Ctx => Resource[IO, fs2.Stream[IO, A]]): PotOption[A]
useStreamResourceView
Given a Resource[IO, fs2.Stream[IO, A]]
, combines useResource
and useStreamView
on it.
Like useStreamResource
but returns a PotOption[View[A]]
, allowing local modifications to the state once it's Ready
.
useStreamResourceView[D: Reusability, A](deps: => D)(streamResource: D => Resource[IO, fs2.Stream[IO, A]]): PotOption[View[A]]
useStreamResourceViewBy[D: Reusability, A](deps: Ctx => D)(streamResource: Ctx => D => Resource[IO, fs2.Stream[IO, A]]): PotOption[View[A]]
useStreamResourceViewOnMount[A](streamResource: Resource[IO, fs2.Stream[IO, A]]): PotOption[View[A]]
useStreamResourceViewOnMountBy[A](streamResource: Ctx => Resource[IO, fs2.Stream[IO, A]]): PotOption[View[A]]
scalajs-react
<-> cats-effect
interop
The crystal.react.implicits.*
import will provide the following methods:
Effect conversion
<CallbackTo[A]>.to[F]: F[A]
- converts aCallbackTo
to the effectF
.<Callback>.to[F]
returnsF[Unit]
. (Requires implicitSync[F]
).<F[A]>.runAsync(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 implicitDispatcher[F]
).<F[A]>.runAsyncAndThen(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 implicitDispatcher[F]
).<F[A]>.runAsyncAndForget: 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 implicitDispatcher[F]
).<F[Unit]>.runAsyncAndThen(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 implicitDispatcher[F]
andLogger[F]
).<F[Unit]>.runAsync(errorMsg: String?): Callback
- When the resultingCallback
is run,F[Unit]
will be run asynchronously. If it fails,errorMsg
will be logged. (Requires implicitDispatcher[F]
andLogger[F]
).
Please note that in all cases the the Callback
returned by .runAsync*
will complete immediately.
BackendScope
Extensions to <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]
).