Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to monadic hooks #685

Merged
merged 1 commit into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Global / onChangedBuildSource := ReloadOnSourceChanges

ThisBuild / crossScalaVersions := List("3.6.2")
ThisBuild / tlBaseVersion := "0.46"
ThisBuild / tlBaseVersion := "0.47"

ThisBuild / tlCiReleaseBranches := Seq("master")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,43 @@ import japgolly.scalajs.react.hooks.CustomHook
import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA

object UseAsyncEffect {
def hook[G, D: Reusability](using EffectWithCleanup[G, DefaultA]) =
CustomHook[WithDeps[D, G]].useSingleEffect
.useEffectWithDepsBy((props, _) => props.deps): (props, dispatcher) =>
deps => dispatcher.submit(props.fromDeps(deps).normalize)
.build

/**
* Run async effect and cancel previously running instances, thus avoiding race conditions. Allows
* returning a cleanup effect.
*/
final def useAsyncEffectWithDeps[G, D: Reusability](deps: => D)(effect: D => G)(using
G: EffectWithCleanup[G, DefaultA]
): HookResult[Unit] =
// hookBuilder(WithDeps(deps, effect))
useSingleEffect.flatMap: dispatcher =>
useEffectWithDeps(deps): deps =>
dispatcher.submit(effect(deps).normalize)

/**
* Run async effect and cancel previously running instances, thus avoiding race conditions. Allows
* returning a cleanup effect.
*/
final inline def useAsyncEffect[G](effect: => G)(using
G: EffectWithCleanup[G, DefaultA]
): HookResult[Unit] =
useAsyncEffectWithDeps(NeverReuse)((_: Reuse[Unit]) => effect)

/**
* Run async effect and cancel previously running instances, thus avoiding race conditions. Allows
* returning a cleanup effect.
*/
final inline def useAsyncEffectOnMount[G](effect: => G)(using
G: EffectWithCleanup[G, DefaultA]
): HookResult[Unit] = // () has Reusability = always.
useAsyncEffectWithDeps(())((_: Unit) => effect)

// *** The rest is to support builder-style hooks *** //

private def hook[G, D: Reusability](using
EffectWithCleanup[G, DefaultA]
): CustomHook[WithDeps[D, G], Unit] =
CustomHook.fromHookResult(input => useAsyncEffectWithDeps(input.deps)(input.fromDeps))

object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,105 @@ import japgolly.scalajs.react.*
import japgolly.scalajs.react.hooks.CustomHook
import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA

object UseEffectResult {
object UseEffectResult:
private case class Input[D, A, R: Reusability](
effect: WithPotDeps[D, DefaultA[A], R],
keep: Boolean
):
val depsOpt: Option[D] = effect.deps.toOption

private def hook[D, A, R: Reusability] =
CustomHook[Input[D, A, R]]
.useState(Pot.pending[A])
.useMemoBy((props, _) => props.effect.reuseValue): (props, _) => // Memo Option[effect]
_ => props.depsOpt.map(props.effect.fromDeps)
.useEffectWithDepsBy((_, _, effectOpt) => effectOpt): (props, state, _) => // Set to Pending
_ => state.setState(Pot.pending).unless(props.keep).void
.useAsyncEffectWithDepsBy((_, _, effectOpt) => effectOpt): (_, state, _) => // Run effect
_.value.foldMap: effect =>
(for
a <- effect
_ <- state.setStateAsync(a.ready)
yield ()).handleErrorWith: t =>
state.setStateAsync(Pot.error(t))
.buildReturning((_, state, _) => state.value)

// Provides functionality for all the flavors
private def hookBuilder[D, A, R: Reusability](
deps: Pot[D]
)(effect: D => DefaultA[A], keep: Boolean, reuseBy: Option[R]): HookResult[Pot[A]] =
for
state <- useState(Pot.pending[A])
effectOpt <- useMemo(reuseBy): _ => // Memo Option[effect]
deps.toOption.map(effect)
_ <- useEffectWithDeps(effectOpt): _ => // Set to Pending
state.setState(Pot.pending).unless(keep).void
_ <- useAsyncEffectWithDeps(effectOpt): // Run effect
_.value.foldMap: effect =>
(for
a <- effect
_ <- state.setStateAsync(a.ready)
yield ()).handleErrorWith: t =>
state.setStateAsync(Pot.error(t))
yield state.value

/**
* Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`. When
* dependencies change, reverts to `Pending` while executing the new effect.
*/
final inline def useEffectResultWithDeps[D: Reusability, A](
deps: => D
)(effect: D => DefaultA[A]): HookResult[Pot[A]] =
hookBuilder(deps.ready)(effect, keep = false, deps.some)

/**
* Runs an async effect whenever `Pot` dependencies transition into a `Ready` state (but not when
* they change once `Ready`) and stores the result in a state, which is provided as a `Pot[A]`.
* When dependencies change, reverts to `Pending` while executing the new effect or while waiting
* for them to become `Ready` again. For multiple dependencies, use `(par1, par2, ...).tupled`.
*/
final inline def useEffectResultWhenDepsReady[D, A](
deps: => Pot[D]
)(effect: D => DefaultA[A]): HookResult[Pot[A]] =
hookBuilder(deps)(effect, keep = false, deps.toOption.void)

/**
* Runs an async effect when `Pot` dependencies transition into a `Ready` state or change once
* `Ready` and stores the result in a state, which is provided as a `Pot[A]`. When dependencies
* change, reverts to `Pending` while executing the new effect or while waiting for them to become
* `Ready` again. For multiple dependencies, use `(par1, par2, ...).tupled`.
*/
final inline def useEffectResultWhenDepsReadyOrChange[D: Reusability, A](
deps: => Pot[D]
)(effect: D => DefaultA[A]): HookResult[Pot[A]] =
hookBuilder(deps)(effect, keep = false, deps.toOption)

/**
* Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`. When
* dependencies change, keeps the old value while executing the new effect.
*/
final inline def useEffectKeepResultWithDeps[D: Reusability, A](
deps: => D
)(effect: D => DefaultA[A]): HookResult[Pot[A]] =
hookBuilder(deps.ready)(effect, keep = true, deps.some)

/**
* Runs an async effect whenever `Pot` dependencies transition into a `Ready` state (but not when
* they change once `Ready`) and stores the result in a state, which is provided as a `Pot[A]`.
* When dependencies change, keeps the old value while executing the new effect or while waiting
* for them to become `Ready` again. For multiple dependencies, use `(par1, par2, ...).tupled`.
*/
final inline def useEffectKeepResultWhenDepsReady[D, A](
deps: => Pot[D]
)(effect: D => DefaultA[A]): HookResult[Pot[A]] =
hookBuilder(deps)(effect, keep = true, deps.toOption.void)

/**
* Runs an async effect whenever `Pot` dependencies transition into a `Ready` state or change once
* `Ready` and stores the result in a state, which is provided as a `Pot[A]`. When dependencies
* change, keeps the old value while executing the new effect or while waiting for them to become
* `Ready` again. For multiple dependencies, use `(par1, par2, ...).tupled`.
*/
final inline def useEffectKeepResultWhenDepsReadyOrChange[D: Reusability, A](
deps: => Pot[D]
)(effect: D => DefaultA[A]): HookResult[Pot[A]] =
hookBuilder(deps)(effect, keep = true, deps.toOption)

/**
* Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`.
*/
final inline def useEffectResultOnMount[A](effect: => DefaultA[A]): HookResult[Pot[A]] =
useEffectResultWithDeps(())(_ => effect) // () has Reusability = always.

// *** The rest is to support builder-style hooks *** //

private def hook[D, A, R: Reusability]: CustomHook[Input[D, A, R], Pot[A]] =
CustomHook.fromHookResult: input =>
hookBuilder(input.effect.deps)(input.effect.fromDeps, input.keep, input.effect.reuseValue)
object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {

Expand Down Expand Up @@ -313,4 +389,3 @@ object UseEffectResult {
}

object syntax extends HooksApiExt
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,98 @@ import japgolly.scalajs.react.*
import japgolly.scalajs.react.hooks.CustomHook
import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA

object UseEffectStreamResource {

protected def hook[D: Reusability] =
CustomHook[WithDeps[D, StreamResource[Unit]]]
.useAsyncEffectWithDepsBy(props => props.deps): props =>
deps =>
for
latch <- Deferred[DefaultA, Unit] // Latch for stream termination.
(_, close) <- props
.fromDeps(deps)
.flatMap: stream =>
(stream.compile.drain >> latch.complete(())).background.void
.allocated
supervisor <- (latch.get >> close).start // Close the resource if the stream terminates.
yield
// Cleanup closes resource and cancels the supervisor, unless resource is already closed.
(supervisor.cancel >> close).when:
latch.tryGet.map(_.isEmpty)
.build
object UseEffectStreamResource:
/**
* Open a `Resource[Async, fs.Stream[Async, Unit]]` on mount or when dependencies change, and
* drain the stream by creating a fiber. The fiber will be cancelled and the resource closed on
* unmount or deps change.
*/
final def useEffectStreamResourceWithDeps[D: Reusability](deps: => D)(
effectStreamResource: D => StreamResource[Unit]
): HookResult[Unit] =
useAsyncEffectWithDeps(deps): depsValue =>
for
latch <- Deferred[DefaultA, Unit] // Latch for stream termination.
(_, close) <- effectStreamResource(deps)
.flatMap: stream =>
(stream.compile.drain >> latch.complete(())).background.void
.allocated
supervisor <- (latch.get >> close).start // Close the resource if the stream terminates.
yield
// Cleanup closes resource and cancels the supervisor, unless resource is already closed.
(supervisor.cancel >> close).when:
latch.tryGet.map(_.isEmpty)

/**
* Open a `Resource[Async, fs.Stream[Async, Unit]]` on each render, and drain the stream by
* creating a fiber. If there was another fiber executing from the previous render, it will be
* cancelled and its resource closed.
*/
final inline def useEffectStreamResource(
effectStreamResource: => StreamResource[Unit]
): HookResult[Unit] =
useEffectStreamResourceWithDeps(NeverReuse)(_ => effectStreamResource)

/**
* Open a `Resource[Async, fs.Stream[Async, Unit]]` on mount, and drain the stream by creating a
* fiber. The fiber will be cancelled and the resource closed on unmount.
*/
final inline def useEffectStreamResourceOnMount(
effectStreamResource: => StreamResource[Unit]
): HookResult[Unit] = // () has Reusability = always.
useEffectStreamResourceWithDeps(())(_ => effectStreamResource)

/**
* Open a `Resource[Async, fs.Stream[Async, Unit]]` when a `Pot` dependency becomes `Ready`, and
* drain the stream by creating a fiber. The fiber will be cancelled and the resource closed on
* unmount or if the dependency transitions to `Pending` or `Error`.
*/
final def useEffectStreamResourceWhenDepsReady[D](
deps: => Pot[D]
)(effectStreamResource: D => StreamResource[Unit]): HookResult[Unit] =
useEffectStreamResourceWithDeps(deps.toOption.void)(_ =>
deps.toOption.map(effectStreamResource).orEmpty
)

/**
* Drain a `fs2.Stream[Async, Unit]` by creating a fiber on mount or when deps change.The fiber
* will be cancelled on unmount or deps change.
*/
final inline def useEffectStreamWithDeps[D: Reusability](deps: => D)(
effectStream: D => fs2.Stream[DefaultA, Unit]
): HookResult[Unit] =
useEffectStreamResourceWithDeps(deps)(deps => Resource.pure(effectStream(deps)))

/**
* Drain a `fs2.Stream[Async, Unit]` by creating a fiber on each render. If there was another
* fiber executing from the previous render, it will be cancelled.
*/
final inline def useEffectStream(effectStream: fs2.Stream[DefaultA, Unit]): HookResult[Unit] =
useEffectStreamWithDeps(NeverReuse)(_ => effectStream)

/**
* Drain a `fs2.Stream[Async, Unit]` by creating a fiber when a `Pot` dependency becomes `Ready`.
* The fiber will be cancelled on unmount or if the dependency transitions to `Pending` or
* `Error`.
*/
final inline def useEffectStreamWhenDepsReady[D](
deps: => Pot[D]
)(effectStream: D => fs2.Stream[DefaultA, Unit]): HookResult[Unit] =
useEffectStreamWithDeps(deps.toOption.void)(_ => deps.toOption.map(effectStream).orEmpty)

/**
* Drain a `fs2.Stream[Async, Unit]` by creating a fiber on mount. The fiber will be cancelled on
* unmount.
*/
final inline def useEffectStreamOnMount(
effectStream: => fs2.Stream[DefaultA, Unit]
): HookResult[Unit] =
useEffectStreamResourceOnMount(Resource.pure(effectStream))

// *** The rest is to support builder-style hooks *** //

private def hook[D: Reusability]: CustomHook[WithDeps[D, StreamResource[Unit]], Unit] =
CustomHook.fromHookResult(input => useEffectStreamResourceWithDeps(input.deps)(input.fromDeps))

object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {
Expand Down Expand Up @@ -312,4 +385,3 @@ object UseEffectStreamResource {
}

object syntax extends HooksApiExt
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,73 @@ import japgolly.scalajs.react.hooks.Hooks.UseEffectArg
import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA

object UseEffectWhenDepsReady:
// Provides functionality for all the flavors
private def hookBuilder[D, A: UseEffectArg: Monoid, R: Reusability](deps: => Pot[D])(
effect: D => A,
reuseBy: Option[R]
): HookResult[Unit] =
useEffectWithDeps(reuseBy): _ =>
deps.map(effect).toOption.orEmpty

def hook[D, A: UseEffectArg: Monoid, R: Reusability] =
CustomHook[WithPotDeps[D, A, R]]
.useEffectWithDepsBy(props => props.reuseValue): props =>
_ => props.deps.toOption.map(props.fromDeps).orEmpty
.build

def asyncHook[G, D, R: Reusability](using EffectWithCleanup[G, DefaultA]) =
CustomHook[WithPotDeps[D, G, R]]
.useAsyncEffectWithDepsBy(props => props.reuseValue): props =>
_ => props.deps.toOption.map(props.fromDeps(_).normalize).orEmpty
.build
/**
* Effect that runs whenever `Pot` dependencies transition into a `Ready` state (but not when they
* change once `Ready`). For multiple dependencies, use `(par1, par2, ...).tupled`. Dependencies
* are passed unpacked to the effect bulding function.
*/
final inline def useEffectWhenDepsReady[D, A: UseEffectArg: Monoid](
deps: => Pot[D]
)(effect: D => A): HookResult[Unit] =
hookBuilder(deps)(effect, deps.toOption.void)

/**
* Effect that runs when `Pot` dependencies transition into a `Ready` state or change once
* `Ready`. For multiple dependencies, use `(par1, par2, ...).tupled`. Dependencies are passed
* unpacked to the effect bulding function.
*/
final inline def useEffectWhenDepsReadyOrChange[D: Reusability, A: UseEffectArg: Monoid](
deps: => Pot[D]
)(effect: D => A): HookResult[Unit] =
hookBuilder(deps)(effect, deps.toOption)

// Provides functionality for all the flavors
private def asyncHookBuilder[D, G, R: Reusability](deps: => Pot[D])(
effect: D => G,
reuseBy: Option[R]
)(using EffectWithCleanup[G, DefaultA]): HookResult[Unit] =
useAsyncEffectWithDeps(reuseBy): _ =>
deps.map(effect(_).normalize).toOption.orEmpty

/**
* Effect that runs whenever `Pot` dependencies transition into a `Ready` state (but not when they
* change once `Ready`). For multiple dependencies, use `(par1, par2, ...).tupled`. Dependencies
* are passed unpacked to the effect bulding function.
*/
final inline def useAsyncEffectWhenDepsReady[D, G](
deps: => Pot[D]
)(effect: D => G)(using EffectWithCleanup[G, DefaultA]): HookResult[Unit] =
asyncHookBuilder(deps)(effect, deps.toOption.void)

/**
* Effect that runs when `Pot` dependencies transition into a `Ready` state or change once
* `Ready`. For multiple dependencies, use `(par1, par2, ...).tupled`. Dependencies are passed
* unpacked to the effect bulding function.
*/
final inline def useAsyncEffectWhenDepsReadyOrChange[D: Reusability, G](
deps: => Pot[D]
)(effect: D => G)(using EffectWithCleanup[G, DefaultA]): HookResult[Unit] =
asyncHookBuilder(deps)(effect, deps.toOption)

// *** The rest is to support builder-style hooks *** //

private def hook[D, A: UseEffectArg: Monoid, R: Reusability]
: CustomHook[WithPotDeps[D, A, R], Unit] =
CustomHook.fromHookResult(input => hookBuilder(input.deps)(input.fromDeps, input.reuseValue))

private def asyncHook[G, D, R: Reusability](using
EffectWithCleanup[G, DefaultA]
): CustomHook[WithPotDeps[D, G, R], Unit] =
CustomHook.fromHookResult: input =>
asyncHookBuilder(input.deps)(input.fromDeps, input.reuseValue)

object HooksApiExt {
sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) {
Expand Down
Loading
Loading