diff --git a/modules/core/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala b/modules/core/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala index 1840ba1..3669b02 100644 --- a/modules/core/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala +++ b/modules/core/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala @@ -10,21 +10,18 @@ import japgolly.scalajs.react.hooks.CustomHook import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA object UseAsyncEffect { - private def hookBuilder[G, D: Reusability](input: WithDeps[D, G])(using - EffectWithCleanup[G, DefaultA] - ): HookResult[Unit] = - useSingleEffect.flatMap: dispatcher => - useEffectWithDeps(input.deps): deps => - dispatcher.submit(input.fromDeps(deps).normalize) /** * Run async effect and cancel previously running instances, thus avoiding race conditions. Allows * returning a cleanup effect. */ - final inline def useAsyncEffectWithDeps[G, D: Reusability](deps: => D)(effect: D => G)(using + final def useAsyncEffectWithDeps[G, D: Reusability](deps: => D)(effect: D => G)(using G: EffectWithCleanup[G, DefaultA] ): HookResult[Unit] = - hookBuilder(WithDeps(deps, effect)) + // 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 @@ -47,7 +44,7 @@ object UseAsyncEffect { private def hook[G, D: Reusability](using EffectWithCleanup[G, DefaultA] ): CustomHook[WithDeps[D, G], Unit] = - CustomHook.fromHookResult(hookBuilder(_)) + CustomHook.fromHookResult(input => useAsyncEffectWithDeps(input.deps)(input.fromDeps)) object HooksApiExt { sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { 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 3a0fb48..69d8b99 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,29 +10,103 @@ 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. + + 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]) { @@ -313,4 +387,3 @@ object UseEffectResult { } object syntax extends HooksApiExt -} diff --git a/modules/core/js/src/main/scala/crystal/react/hooks/package.scala b/modules/core/js/src/main/scala/crystal/react/hooks/package.scala index 0ce7400..350b797 100644 --- a/modules/core/js/src/main/scala/crystal/react/hooks/package.scala +++ b/modules/core/js/src/main/scala/crystal/react/hooks/package.scala @@ -12,7 +12,16 @@ import japgolly.scalajs.react.util.DefaultEffects.Async as DefaultA export UseSingleEffect.useSingleEffect, UseSerialState.useSerialState, UseStateCallback.useStateCallback, UseShadowRef.useShadowRef, UseStateView.useStateView, - UseStateViewWithReuse.useStateViewWithReuse, UseSerialStateView.useSerialStateView + UseStateViewWithReuse.useStateViewWithReuse, UseSerialStateView.useSerialStateView, + UseAsyncEffect.{useAsyncEffect, useAsyncEffectOnMount, useAsyncEffectWithDeps}, UseEffectResult.{ + useEffectKeepResultWhenDepsReady, + useEffectKeepResultWhenDepsReadyOrChange, + useEffectKeepResultWithDeps, + useEffectResultOnMount, + useEffectResultWhenDepsReady, + useEffectResultWhenDepsReadyOrChange, + useEffectResultWithDeps +} export UseSingleEffect.syntax.*, UseSerialState.syntax.*, UseStateCallback.syntax.*, UseStateView.syntax.*, UseStateViewWithReuse.syntax.*, UseSerialStateView.syntax.*,