Skip to content

Commit

Permalink
Merge pull request #552 from gemini-hlsw/ready-hook
Browse files Browse the repository at this point in the history
useEffectWhenDepsReady hook
  • Loading branch information
rpiaggio authored Oct 11, 2023
2 parents 82679c4 + 424be07 commit 412b148
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 10 deletions.
18 changes: 9 additions & 9 deletions js/src/main/scala/crystal/react/hooks/UseAsyncEffect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -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
Expand All @@ -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)(_))
Expand All @@ -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)(_))
Expand Down
138 changes: 138 additions & 0 deletions js/src/main/scala/crystal/react/hooks/UseEffectWhenDepsReady.scala
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion js/src/main/scala/crystal/react/hooks/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ 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}

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]
Expand All @@ -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)

0 comments on commit 412b148

Please sign in to comment.