From 424be072850b89c6bc182e5bc5e8d0af35b28485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 11 Oct 2023 12:57:18 -0300 Subject: [PATCH] useEffectWhenDepsReady --- .../crystal/react/hooks/UseAsyncEffect.scala | 18 +-- .../react/hooks/UseEffectWhenDepsReady.scala | 138 ++++++++++++++++++ .../scala/crystal/react/hooks/package.scala | 5 +- 3 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 js/src/main/scala/crystal/react/hooks/UseEffectWhenDepsReady.scala diff --git a/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala b/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala index 0ef7bd08..3109193b 100644 --- a/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala +++ b/js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala @@ -46,7 +46,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - final def useAsyncEffectWithDeps[D: Reusability, A]( + final def useAsyncEffectWithDeps[D: Reusability]( deps: => D )(effect: D => DefaultA[DefaultA[Unit]])(using step: Step @@ -57,7 +57,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - final def useAsyncEffect[A](effect: DefaultA[DefaultA[Unit]])(using + final def useAsyncEffect(effect: DefaultA[DefaultA[Unit]])(using step: Step ): step.Self = useAsyncEffectBy(_ => effect) @@ -66,7 +66,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - final def useAsyncEffectOnMount[A](effect: DefaultA[DefaultA[Unit]])(using + final def useAsyncEffectOnMount(effect: DefaultA[DefaultA[Unit]])(using step: Step ): step.Self = useAsyncEffectOnMountBy(_ => effect) @@ -75,7 +75,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - final def useAsyncEffectWithDepsBy[D: Reusability, A]( + final def useAsyncEffectWithDepsBy[D: Reusability]( deps: Ctx => D )(effect: Ctx => D => DefaultA[DefaultA[Unit]])(using step: Step @@ -89,7 +89,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - final def useAsyncEffectBy[A](effect: Ctx => DefaultA[DefaultA[Unit]])(using + final def useAsyncEffectBy(effect: Ctx => DefaultA[DefaultA[Unit]])(using step: Step ): step.Self = useAsyncEffectWithDepsBy(_ => NeverReuse)(ctx => _ => effect(ctx)) @@ -98,7 +98,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - final def useAsyncEffectOnMountBy[A](effect: Ctx => DefaultA[DefaultA[Unit]])(using + final def useAsyncEffectOnMountBy(effect: Ctx => DefaultA[DefaultA[Unit]])(using step: Step ): step.Self = // () has Reusability = always. useAsyncEffectWithDepsBy(_ => ())(ctx => _ => effect(ctx)) @@ -112,7 +112,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - def useAsyncEffectWithDepsBy[D: Reusability, A]( + def useAsyncEffectWithDepsBy[D: Reusability]( deps: CtxFn[D] )(effect: CtxFn[D => DefaultA[DefaultA[Unit]]])(using step: Step @@ -123,7 +123,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - def useAsyncEffectBy[A](effect: CtxFn[DefaultA[DefaultA[Unit]]])(using + def useAsyncEffectBy(effect: CtxFn[DefaultA[DefaultA[Unit]]])(using step: Step ): step.Self = useAsyncEffectBy(step.squash(effect)(_)) @@ -132,7 +132,7 @@ object UseAsyncEffect { * Simulates `useEffect` with cleanup callback for async effect. To declare an async effect * without a cleanup callback, just use the regular `useEffect` hook. */ - def useAsyncEffectOnMountBy[A](effect: CtxFn[DefaultA[DefaultA[Unit]]])(using + def useAsyncEffectOnMountBy(effect: CtxFn[DefaultA[DefaultA[Unit]]])(using step: Step ): step.Self = useAsyncEffectOnMountBy(step.squash(effect)(_)) diff --git a/js/src/main/scala/crystal/react/hooks/UseEffectWhenDepsReady.scala b/js/src/main/scala/crystal/react/hooks/UseEffectWhenDepsReady.scala new file mode 100644 index 00000000..1c34abda --- /dev/null +++ b/js/src/main/scala/crystal/react/hooks/UseEffectWhenDepsReady.scala @@ -0,0 +1,138 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package crystal.react.hooks + +import cats.kernel.Monoid +import cats.syntax.all.* +import crystal.Pot +import crystal.react.* +import crystal.react.syntax.pot.given +import japgolly.scalajs.react.* +import japgolly.scalajs.react.hooks.CustomHook +import japgolly.scalajs.react.hooks.Hooks.UseEffectArg +import japgolly.scalajs.react.util.DefaultEffects.{Async => DefaultA} + +object UseEffectWhenDepsReady: + def hook[D, A: UseEffectArg: Monoid] = + CustomHook[WithPotDeps[D, A]] + .useEffectWithDepsBy(props => props.deps.void)(props => + _ => props.deps.toOption.map(props.fromDeps).orEmpty + ) + .build + + def asyncHook[D] = + CustomHook[WithPotDeps[D, DefaultA[DefaultA[Unit]]]] + .useAsyncEffectWithDepsBy(props => props.deps.void)(props => + _ => props.deps.toOption.map(props.fromDeps).orEmpty + ) + .build + + object HooksApiExt { + sealed class Primary[Ctx, Step <: HooksApi.AbstractStep](api: HooksApi.Primary[Ctx, Step]) { + + /** + * Effect that runs when `Pot` dependencies transition into a `Ready` state. For multiple + * dependencies, use `(par1, par2, ...).tupled`. Dependencies are passed unpacked to the + * effect bulding function. Returning a cleanup callback is supported, but when using an async + * effect with cleanup use `useAsyncEffectWhenDepsReady` instead. + */ + final def useEffectWhenDepsReady[D, A: UseEffectArg: Monoid]( + deps: => Pot[D] + )(effect: D => A)(using + step: Step + ): step.Self = + useEffectWhenDepsReady(_ => deps)(_ => effect) + + /** + * Effect that runs when `Pot` dependencies transition into a `Ready` state. For multiple + * dependencies, use `(par1, par2, ...).tupled`. Dependencies are passed unpacked to the + * effect bulding function. Returning a cleanup callback is supported, but when using an async + * effect with cleanup use `useAsyncEffectWhenDepsReady` instead. + */ + final def useEffectWhenDepsReady[D, A: UseEffectArg: Monoid]( + deps: Ctx => Pot[D] + )(effect: Ctx => D => A)(using + step: Step + ): step.Self = + api.customBy { ctx => + val hookInstance = hook[D, A] + hookInstance(WithPotDeps(deps(ctx), effect(ctx))) + } + + /** + * Async effect that runs when `Pot` dependencies transition into a `Ready` state and returns + * a cleanup callback. For multiple dependencies, use `(par1, par2, ...).tupled`. Dependencies + * are passed unpacked to the effect bulding function. + */ + final def useAsyncEffectWhenDepsReady[D]( + deps: => Pot[D] + )(effect: D => DefaultA[DefaultA[Unit]])(using + step: Step + ): step.Self = + useAsyncEffectWhenDepsReady(_ => deps)(_ => effect) + + /** + * Async effect that runs when `Pot` dependencies transition into a `Ready` state and returns + * a cleanup callback. For multiple dependencies, use `(par1, par2, ...).tupled`. Dependencies + * are passed unpacked to the effect bulding function. + */ + final def useAsyncEffectWhenDepsReady[D]( + deps: Ctx => Pot[D] + )(effect: Ctx => D => DefaultA[DefaultA[Unit]])(using + step: Step + ): step.Self = + api.customBy { ctx => + val hookInstance = asyncHook[D] + hookInstance(WithPotDeps(deps(ctx), effect(ctx))) + } + } + + final class Secondary[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, CtxFn]]( + api: HooksApi.Secondary[Ctx, CtxFn, Step] + ) extends Primary[Ctx, Step](api) { + + /** + * Effect that runs when `Pot` dependencies transition into a `Ready` state. For multiple + * dependencies, use `(par1, par2, ...).tupled`. Dependencies are passed unpacked to the + * effect bulding function. Returning a cleanup callback is supported, but when using an async + * effect with cleanup use `useAsyncEffectWhenDepsReady` instead. + */ + def useEffectWhenDepsReady[D, A: UseEffectArg: Monoid]( + deps: CtxFn[Pot[D]] + )(effect: CtxFn[D => A])(using + step: Step + ): step.Self = + useEffectWhenDepsReady(step.squash(deps)(_))(step.squash(effect)(_)) + + /** + * Async effect that runs when `Pot` dependencies transition into a `Ready` state and returns + * a cleanup callback. For multiple dependencies, use `(par1, par2, ...).tupled`. Dependencies + * are passed unpacked to the effect bulding function. + */ + def useAsyncEffectWhenDepsReady[D]( + deps: CtxFn[Pot[D]] + )(effect: CtxFn[D => DefaultA[DefaultA[Unit]]])(using + step: Step + ): step.Self = + useAsyncEffectWhenDepsReady(step.squash(deps)(_))(step.squash(effect)(_)) + } + } + + protected trait HooksApiExt { + import HooksApiExt.* + + implicit def hooksExtEffectWhenDepsReady1[Ctx, Step <: HooksApi.AbstractStep]( + api: HooksApi.Primary[Ctx, Step] + ): Primary[Ctx, Step] = + new Primary(api) + + implicit def hooksEffectWhenDepsReady2[Ctx, CtxFn[_], Step <: HooksApi.SubsequentStep[Ctx, + CtxFn + ]]( + api: HooksApi.Secondary[Ctx, CtxFn, Step] + ): Secondary[Ctx, CtxFn, Step] = + new Secondary(api) + } + + object syntax extends HooksApiExt diff --git a/js/src/main/scala/crystal/react/hooks/package.scala b/js/src/main/scala/crystal/react/hooks/package.scala index a51f946d..fdaa2a83 100644 --- a/js/src/main/scala/crystal/react/hooks/package.scala +++ b/js/src/main/scala/crystal/react/hooks/package.scala @@ -5,6 +5,7 @@ package crystal.react.hooks import cats.effect.Fiber import cats.effect.Resource +import crystal.Pot import crystal.react.reuse.* import japgolly.scalajs.react.* import japgolly.scalajs.react.util.DefaultEffects.{Async => DefaultA} @@ -12,7 +13,7 @@ import japgolly.scalajs.react.util.DefaultEffects.{Async => DefaultA} export UseSingleEffect.syntax.*, UseSerialState.syntax.*, UseStateCallback.syntax.*, UseStateView.syntax.*, UseStateViewWithReuse.syntax.*, UseSerialStateView.syntax.*, UseAsyncEffect.syntax.*, UseEffectResult.syntax.*, UseResource.syntax.*, - UseStreamResource.syntax.* + UseStreamResource.syntax.*, UseEffectWhenDepsReady.syntax.* type UnitFiber[F[_]] = Fiber[F, Throwable, Unit] type AsyncUnitFiber = Fiber[DefaultA, Throwable, Unit] @@ -23,3 +24,5 @@ protected[hooks] type NeverReuse = Reuse[Unit] protected[hooks] val NeverReuse: NeverReuse = ().reuseNever protected[hooks] final case class WithDeps[D, A](deps: D, fromDeps: D => A) + +protected[hooks] final case class WithPotDeps[D, A](deps: Pot[D], fromDeps: D => A)