Skip to content

Commit

Permalink
Merge pull request #649 from gemini-hlsw/use-effect-result-when-deps-…
Browse files Browse the repository at this point in the history
…ready

Implement WhenDepsReady for useEffectResult
  • Loading branch information
cquiroz authored Sep 10, 2024
2 parents c24fb39 + 9d173db commit 916fa83
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 13 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ Note that all versions either have dependencies or are executed `onMount`. It do

Also note that when dependencies change, the hook value will revert to `Pending` until the new effect completes. If this is undesireable, there are `useEffectKeepResult*` variants which will instead keep the hook value as `Ready(oldValue)` until the new effect completes.

There are also `WhenDepsReady` versions, which will only execute the effect when dependencies are ready. If they change or transition to `Pending` or `Error`, then the result will revert to `Pending`. However, if the `KeepResult` version is used, it will retain the last value.


``` scala
useEffectResultWithDeps[D: Reusability, A](deps: => D)(effect: D => IO[A]): Pot[A]
Expand All @@ -266,6 +268,12 @@ Also note that when dependencies change, the hook value will revert to `Pending`

useEffectResultOnMount[A](effect: IO[A]): Pot[A]
useEffectResultOnMountBy[A](effect: Ctx => IO[A]): Pot[A]

useEffectResultWhenDepsReady[D: Reusability, A](deps: => D)(effect: D => IO[A]): Pot[A]
useEffectResultWhenDepsReadyBy[D: Reusability, A](deps: Ctx => D)(effect: Ctx => D => IO[A]): Pot[A]

useEffectKeepResultWhenDepsReady[D: Reusability, A](deps: => D)(effect: D => IO[A]): Pot[A]
useEffectKeepResultWhenDepsReadyBy[D: Reusability, A](deps: Ctx => D)(effect: Ctx => D => IO[A]): Pot[A]
```

#### Example:
Expand Down
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.5.0")
ThisBuild / tlBaseVersion := "0.43"
ThisBuild / tlBaseVersion := "0.44"

ThisBuild / tlCiReleaseBranches := Seq("master")

Expand Down
112 changes: 100 additions & 12 deletions modules/core/js/src/main/scala/crystal/react/hooks/UseEffectResult.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,31 @@

package crystal.react.hooks

import cats.syntax.all.*
import crystal.*
import crystal.react.*
import japgolly.scalajs.react.*
import japgolly.scalajs.react.hooks.CustomHook
import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA

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

private def hook[D: Reusability, A] =
CustomHook[WithDeps[D, Input[A]]]
CustomHook[Input[D, A]]
.useState(Pot.pending[A])
.useMemoBy((props, _) => props.deps): (props, _) =>
deps => props.fromDeps(deps)
.useEffectWithDepsBy((_, _, input) => input): (_, state, _) =>
input => state.setState(Pot.pending).unless(input.keep).void
.useAsyncEffectWithDepsBy((_, _, input) => input): (_, state, _) =>
input =>
(for {
a <- input.effect
.useMemoBy((props, _) => props.depsOpt.void): (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)))
yield ()).handleErrorWith: t =>
state.setStateAsync(Pot.error(t))
.buildReturning((_, state, _) => state.value)

object HooksApiExt {
Expand All @@ -41,6 +44,19 @@ object UseEffectResult {
): step.Next[Pot[A]] =
useEffectResultWithDepsBy(_ => deps)(_ => effect)

/**
* Runs an async effect when `Pot` dependencies transition into a `Ready` state 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 def useEffectResultWhenDepsReady[D: Reusability, A](
deps: => Pot[D]
)(effect: D => DefaultA[A])(using
step: Step
): step.Next[Pot[A]] =
useEffectResultWhenDepsReadyBy(_ => deps)(_ => effect)

/**
* 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.
Expand All @@ -52,6 +68,19 @@ object UseEffectResult {
): step.Next[Pot[A]] =
useEffectKeepResultWithDepsBy(_ => deps)(_ => effect)

/**
* Runs an async effect when `Pot` dependencies transition into a `Ready` state 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 def useEffectKeepResultWhenDepsReady[D: Reusability, A](
deps: => Pot[D]
)(effect: D => DefaultA[A])(using
step: Step
): step.Next[Pot[A]] =
useEffectKeepResultWhenDepsReadyBy(_ => deps)(_ => effect)

/**
* Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`.
*/
Expand All @@ -64,10 +93,17 @@ object UseEffectResult {
deps: Ctx => D
)(effect: Ctx => D => DefaultA[A], keep: Boolean)(using
step: Step
): step.Next[Pot[A]] =
useEffectResultInternalWhenDepsReadyBy(deps.andThen(_.ready))(effect, keep)

private def useEffectResultInternalWhenDepsReadyBy[D: Reusability, A](
deps: Ctx => Pot[D]
)(effect: Ctx => D => DefaultA[A], keep: Boolean)(using
step: Step
): step.Next[Pot[A]] =
api.customBy { ctx =>
val hookInstance = hook[D, A]
hookInstance(WithDeps(deps(ctx), effect(ctx).andThen(Input(_, keep))))
hookInstance(Input(WithPotDeps(deps(ctx), effect(ctx)), keep))
}

/**
Expand All @@ -81,6 +117,19 @@ object UseEffectResult {
): step.Next[Pot[A]] =
useEffectResultInternalWithDepsBy(deps)(effect, keep = false)

/**
* Runs an async effect when `Pot` dependencies transition into a `Ready` state 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 def useEffectResultWhenDepsReadyBy[D: Reusability, A](
deps: Ctx => Pot[D]
)(effect: Ctx => D => DefaultA[A])(using
step: Step
): step.Next[Pot[A]] =
useEffectResultInternalWhenDepsReadyBy(deps)(effect, keep = false)

/**
* 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.
Expand All @@ -92,6 +141,19 @@ object UseEffectResult {
): step.Next[Pot[A]] =
useEffectResultInternalWithDepsBy(deps)(effect, keep = true)

/**
* Runs an async effect when `Pot` dependencies transition into a `Ready` state 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 def useEffectKeepResultWhenDepsReadyBy[D: Reusability, A](
deps: Ctx => Pot[D]
)(effect: Ctx => D => DefaultA[A])(using
step: Step
): step.Next[Pot[A]] =
useEffectResultInternalWhenDepsReadyBy(deps)(effect, keep = true)

/**
* Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`.
*/
Expand All @@ -116,6 +178,19 @@ object UseEffectResult {
): step.Next[Pot[A]] =
useEffectResultWithDepsBy(step.squash(deps)(_))(step.squash(effect)(_))

/**
* Runs an async effect when `Pot` dependencies transition into a `Ready` state 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`.
*/
def useEffectResultWhenDepsReadyBy[D: Reusability, A](
deps: CtxFn[Pot[D]]
)(effect: CtxFn[D => DefaultA[A]])(using
step: Step
): step.Next[Pot[A]] =
useEffectResultWhenDepsReadyBy(step.squash(deps)(_))(step.squash(effect)(_))

/**
* 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.
Expand All @@ -127,6 +202,19 @@ object UseEffectResult {
): step.Next[Pot[A]] =
useEffectKeepResultWithDepsBy(step.squash(deps)(_))(step.squash(effect)(_))

/**
* Runs an async effect when `Pot` dependencies transition into a `Ready` state 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`.
*/
def useEffectKeepResultWhenDepsReadysBy[D: Reusability, A](
deps: CtxFn[Pot[D]]
)(effect: CtxFn[D => DefaultA[A]])(using
step: Step
): step.Next[Pot[A]] =
useEffectKeepResultWhenDepsReadyBy(step.squash(deps)(_))(step.squash(effect)(_))

/**
* Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`.
*/
Expand Down

0 comments on commit 916fa83

Please sign in to comment.