diff --git a/README.md b/README.md index 5cd5da3b..04464afd 100644 --- a/README.md +++ b/README.md @@ -242,11 +242,16 @@ Stores the result `A` of an effect in state. The state is provided as `Pot[A]`, Note that all versions either have dependencies or are executed `onMount`. It doesn't make sense to execute the effect on each render since its completion will alter state and force a rerender, which would provoke a render loop. The naming keeps the `WithDeps`, even though it's redundant, for consistency with the `useEffect` family of hooks. +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. + ``` scala 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] + useEffectKeepResultWithDeps[D: Reusability, A](deps: => D)(effect: D => IO[A]): Pot[A] + useEffectKeepResultWithDepsBy[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] ``` diff --git a/build.sbt b/build.sbt index 69e4048c..f10de7a5 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,7 @@ Global / onChangedBuildSource := ReloadOnSourceChanges ThisBuild / crossScalaVersions := List("3.4.2") -ThisBuild / tlBaseVersion := "0.41" +ThisBuild / tlBaseVersion := "0.42" ThisBuild / tlCiReleaseBranches := Seq("master") diff --git a/modules/core/js/src/main/scala/crystal/react/hooks/UseEffectResult.scala b/modules/core/js/src/main/scala/crystal/react/hooks/UseEffectResult.scala index 6a8f1816..4d6249b8 100644 --- a/modules/core/js/src/main/scala/crystal/react/hooks/UseEffectResult.scala +++ b/modules/core/js/src/main/scala/crystal/react/hooks/UseEffectResult.scala @@ -10,22 +10,29 @@ import japgolly.scalajs.react.hooks.CustomHook import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA object UseEffectResult { - def hook[D: Reusability, A] = CustomHook[WithDeps[D, DefaultA[A]]] - .useState(Pot.pending[A]) - .useEffectWithDepsBy((props, _) => props.deps)((_, state) => _ => state.setState(Pot.pending)) - .useAsyncEffectWithDepsBy((props, _) => props.deps): (props, state) => - deps => - (for { - a <- props.fromDeps(deps) - _ <- state.setStateAsync(a.ready) - } yield ()).handleErrorWith(t => state.setStateAsync(Pot.error(t))) - .buildReturning((_, state) => state.value) + private case class Input[A](effect: DefaultA[A], keep: Boolean) + + private def hook[D: Reusability, A] = + CustomHook[WithDeps[D, Input[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 + _ <- state.setStateAsync(a.ready) + } yield ()).handleErrorWith(t => state.setStateAsync(Pot.error(t))) + .buildReturning((_, state, _) => state.value) object HooksApiExt { sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { /** * 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 def useEffectResultWithDeps[D: Reusability, A]( deps: => D @@ -34,6 +41,17 @@ object UseEffectResult { ): step.Next[Pot[A]] = useEffectResultWithDepsBy(_ => 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. + */ + final def useEffectKeepResultWithDeps[D: Reusability, A]( + deps: => D + )(effect: D => DefaultA[A])(using + step: Step + ): step.Next[Pot[A]] = + useEffectKeepResultWithDepsBy(_ => deps)(_ => effect) + /** * Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`. */ @@ -42,18 +60,37 @@ object UseEffectResult { ): step.Next[Pot[A]] = useEffectResultOnMountBy(_ => effect) + private def useEffectResultInternalWithDepsBy[D: Reusability, A]( + deps: Ctx => 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)))) + } + /** * 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 def useEffectResultWithDepsBy[D: Reusability, A]( deps: Ctx => D )(effect: Ctx => D => DefaultA[A])(using step: Step ): step.Next[Pot[A]] = - api.customBy { ctx => - val hookInstance = hook[D, A] - hookInstance(WithDeps(deps(ctx), effect(ctx))) - } + useEffectResultInternalWithDepsBy(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. + */ + final def useEffectKeepResultWithDepsBy[D: Reusability, A]( + deps: Ctx => D + )(effect: Ctx => D => DefaultA[A])(using + step: Step + ): step.Next[Pot[A]] = + useEffectResultInternalWithDepsBy(deps)(effect, keep = true) /** * Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`. @@ -70,6 +107,7 @@ object UseEffectResult { /** * 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. */ def useEffectResultWithDepsBy[D: Reusability, A]( deps: CtxFn[D] @@ -78,6 +116,17 @@ object UseEffectResult { ): step.Next[Pot[A]] = useEffectResultWithDepsBy(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. + */ + def useEffectKeepResultWithDepsBy[D: Reusability, A]( + deps: CtxFn[D] + )(effect: CtxFn[D => DefaultA[A]])(using + step: Step + ): step.Next[Pot[A]] = + useEffectKeepResultWithDepsBy(step.squash(deps)(_))(step.squash(effect)(_)) + /** * Runs an async effect and stores the result in a state, which is provided as a `Pot[A]`. */