diff --git a/doc/HOOKS.md b/doc/HOOKS.md index 1ecb331e0..9933b05fe 100644 --- a/doc/HOOKS.md +++ b/doc/HOOKS.md @@ -192,7 +192,8 @@ object Example { | `useRef()` | `.useRefToAnyVdom`
`.useRefToVdom[DomType]`
`.useRefToScalaComponent(component)`
`.useRefToScalaComponent[P, S, B]`
`.useRefToJsComponent(component)`
`.useRefToJsComponent[P, S]`
`.useRefToJsComponentWithMountedFacade[P, S, F]` | | `useRef(initialValue)` | `.useRef(initialValue)` | | `useState(initialState)`
`useState(() => initialState)` | `.useState(initialState)` | -| `useId()` | `.useId()` | +| `useId()` | `.useId` | +| `useTransition` | `.useTransition` | | Custom hook
`useBlah(i)` | `.custom(useBlah(i))` Note: The reason that `[deps]` on the JS side becomes `(deps)` on the Scala side, diff --git a/doc/changelog/2.2.0-betas.md b/doc/changelog/2.2.0-betas.md index ad2fc5a3b..359ad22ea 100644 --- a/doc/changelog/2.2.0-betas.md +++ b/doc/changelog/2.2.0-betas.md @@ -56,3 +56,8 @@ # Changes in beta2 - Fix initialization issue due to `renderWithReuse` hooks using functional component with `React.memo`. + +# Changes in beta3 + +- Add `useTransition` and `useId` hooks. +- Add `startTransition` React API. diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala index 090a1a6c3..7f2d2efeb 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/React.scala @@ -2,7 +2,7 @@ package japgolly.scalajs.react import japgolly.scalajs.react.internal.Box import japgolly.scalajs.react.internal.CoreGeneral._ -import japgolly.scalajs.react.util.Effect.Async +import japgolly.scalajs.react.util.Effect.{Async, Sync} import japgolly.scalajs.react.vdom.{VdomElement, VdomNode} import scala.scalajs.js @@ -60,6 +60,15 @@ object React { .cmapCtorProps[P](Box(_)) } + /** Similar to `useTransition` but allows uses where hooks are not available. + * + * @param callback A _synchronous_ function which causes state updates that can be deferred. + * + * @since 2.2.0 / React 18.0.0 + */ + def startTransition[F[_]](callback: => F[Unit])(implicit F: Sync[F]) = + F.delay(facade.React.startTransition(F.toJsFn(callback))) + val Profiler = feature.Profiler /** StrictMode is a tool for highlighting potential problems in an application. diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala index 8abefa9f4..2246f129a 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala @@ -479,9 +479,23 @@ object Api { next(ctx => UseStateWithReuse.unsafeCreate(initialState(ctx))) /** `useId` is a React Hook for generating unique IDs that can be passed to accessibility attributes. + * + * @see https://react.dev/reference/react/useId */ final def useId(implicit step: Step): step.Next[String] = customBy(_ => UseId()) + + /** Allows components to avoid undesirable loading states by waiting for content to load + * before transitioning to the next screen. It also allows components to defer slower, + * data fetching updates until subsequent renders so that more crucial updates can be + * rendered immediately. + * + * **If some state update causes a component to suspend, that state update should be wrapped in a transition.** + * + * @see {@link https://react.dev/reference/react/useTransition} + */ + final def useTransition(implicit step: Step): step.Next[UseTransition] = + customBy(_ => UseTransition()) } // =================================================================================================================== diff --git a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala index 6f1e083fb..7c0e8d4d3 100644 --- a/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala +++ b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala @@ -1,6 +1,7 @@ package japgolly.scalajs.react.hooks import japgolly.scalajs.react.component.{Js => JsComponent, Scala => ScalaComponent} +import japgolly.scalajs.react.facade.React import japgolly.scalajs.react.feature.Context import japgolly.scalajs.react.internal.Box import japgolly.scalajs.react.util.Effect._ @@ -445,4 +446,35 @@ object Hooks { def apply(): CustomHook[Unit, String] = CustomHook.delay(facade.React.useId()) } + + // =================================================================================================================== + + type UseTransition = UseTransitionF[D.Sync] + + object UseTransition { + @inline def apply(): CustomHook[Unit, UseTransition] = + CustomHook.delay(UseTransitionF(facade.React.useTransition())(D.Sync)) + } + + object UseTransitionF { + def apply[F[_]](r: facade.React.UseTransition)(implicit f: Sync[F]): UseTransitionF[F] = + new UseTransitionF[F] { + override protected[hooks] implicit def F = f + override val raw: React.UseTransition = r + } + } + + trait UseTransitionF[F[_]] { + protected[hooks] implicit def F: Sync[F] + val raw: facade.React.UseTransition + + /** Whether we’re waiting for the transition to finish + */ + def isPending: Boolean = raw._1 + + /** A function that takes a callback. We can use it to tell React which state we want to defer. + */ + def startTransition(cb: => F[Unit]): F[Unit] = + F.delay(raw._2(F.toJsFn(cb))) + } } diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala index 2fad0852f..ba1e822fd 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala @@ -17,6 +17,8 @@ trait Hooks extends js.Object { final type UseStateSetter[S] = js.Function1[S | js.Function1[S, S], Unit] final type UseState[S] = js.Tuple2[S, UseStateSetter[S]] + final type UseTransition = js.Tuple2[Boolean, js.Function1[js.Function0[Unit], Unit]] + final def useState[S](initial: S | js.Function0[S]): UseState[S] = js.native final type UseEffectArg = js.Function0[js.UndefOr[js.Function0[Any]]] @@ -48,4 +50,6 @@ trait Hooks extends js.Object { final def useDebugValue[A](value: A, desc: A => Any): Unit = js.native final def useId(): String = js.native + + final def useTransition(): UseTransition = js.native } diff --git a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala index 212986a9d..5309e94ff 100644 --- a/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala +++ b/library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/React.scala @@ -220,6 +220,8 @@ trait React extends Hooks { /** @since 16.6.0 */ final def memo[P <: js.Object, A](f: js.Function1[P, A], areEqual: js.Function2[P, P, Boolean] = js.native): js.Object = js.native + final def startTransition(callback: js.Function0[Unit]): Unit = js.native + final val version: String = js.native /** React.Children provides utilities for dealing with the this.props.children opaque data structure. */ diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala b/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala index 42fa62f0b..21cb34889 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala @@ -810,6 +810,24 @@ object HooksTest extends TestSuite { } } + private def testUseTransition(): Unit = { + val comp = ScalaFnComponent.withHooks[Unit] + .useTransition + .useState(false) + .render { (_, transition, state) => + <.button( + ^.onClick --> transition.startTransition(state.modState(!_)), + state.value.toString() + ) + } + + test(comp()) { t => + assertEq(t.getText, "false") + t.clickButton() + assertEq(t.getText, "true") + } + } + private def testUseRefManual(): Unit = { val comp = ScalaFnComponent.withHooks[Unit] .useRef(100) @@ -1216,8 +1234,7 @@ object HooksTest extends TestSuite { } .build - withRenderedIntoBody(wrapper(PI(3))) { (_, root) => - val t = new DomTester(root) + test(wrapper(PI(3))) { (t) => t.assertText("P=PI(3), R=1") t.clickButton(2); t.assertText("P=PI(4), R=2") t.clickButton(1); t.assertText("P=PI(4), R=3") @@ -1348,6 +1365,8 @@ object HooksTest extends TestSuite { "useId" - testUseId() + "useTransition" - testUseTransition() + "renderWithReuse" - { "main" - testRenderWithReuse() "never" - testRenderWithReuseNever()