From 24efb3ecc547898e632c58a1759dc649a24823d5 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Tue, 13 Feb 2024 15:35:26 +0100 Subject: [PATCH 1/2] Add useId hook --- doc/HOOKS.md | 1 + .../scala/japgolly/scalajs/react/hooks/Api.scala | 5 +++++ .../scala/japgolly/scalajs/react/hooks/Hooks.scala | 7 +++++++ .../scala/japgolly/scalajs/react/facade/Hooks.scala | 2 ++ .../japgolly/scalajs/react/core/HooksTest.scala | 12 ++++++++++++ .../japgolly/scalajs/react/test/DomTester.scala | 3 +++ 6 files changed, 30 insertions(+) diff --git a/doc/HOOKS.md b/doc/HOOKS.md index e533fa8cd..1ecb331e0 100644 --- a/doc/HOOKS.md +++ b/doc/HOOKS.md @@ -192,6 +192,7 @@ 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()` | | 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/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala b/library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala index f905850f2..8abefa9f4 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 @@ -477,6 +477,11 @@ object Api { */ final def useStateWithReuseBy[S: ClassTag: Reusability](initialState: Ctx => S)(implicit step: Step): step.Next[UseStateWithReuse[S]] = next(ctx => UseStateWithReuse.unsafeCreate(initialState(ctx))) + + /** `useId` is a React Hook for generating unique IDs that can be passed to accessibility attributes. + */ + final def useId(implicit step: Step): step.Next[String] = + customBy(_ => UseId()) } // =================================================================================================================== 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 16a7abd48..6f1e083fb 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 @@ -438,4 +438,11 @@ object Hooks { F.delay { value = f(value) } } + + // =================================================================================================================== + + object UseId { + def apply(): CustomHook[Unit, String] = + CustomHook.delay(facade.React.useId()) + } } 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 d892d3eb1..2fad0852f 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 @@ -46,4 +46,6 @@ trait Hooks extends js.Object { final def useDebugValue(desc: Any): Unit = js.native final def useDebugValue[A](value: A, desc: A => Any): Unit = js.native + + final def useId(): String = js.native } 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 407954639..42fa62f0b 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 @@ -800,6 +800,16 @@ object HooksTest extends TestSuite { } } + private def testUseId(): Unit = { + val comp = ScalaFnComponent.withHooks[Unit] + .useId + .render((_, id) => <.div(id)) + + test(comp()) { t => + assertEq(t.getText.length, 4) + } + } + private def testUseRefManual(): Unit = { val comp = ScalaFnComponent.withHooks[Unit] .useRef(100) @@ -1336,6 +1346,8 @@ object HooksTest extends TestSuite { "useStateSnapshot" - testUseStateSnapshot() "useStateSnapshotWithReuse" - testUseStateSnapshotWithReuse() + "useId" - testUseId() + "renderWithReuse" - { "main" - testRenderWithReuse() "never" - testRenderWithReuseNever() diff --git a/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala b/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala index 1c5f30015..ee5711291 100644 --- a/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala +++ b/library/tests/src/test/scala/japgolly/scalajs/react/test/DomTester.scala @@ -39,6 +39,9 @@ class DomTester(root: Element) { act(SimEvent.Change(t).simulate(i)) } + def getText: String = + DomTester.getText(root) + private def getInputText(): Input = { val is = root.querySelectorAll("input[type=text]") val len = is.length From 1dfbd8944f2ce075a1fa5c77fe8828c78245b0dd Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Tue, 13 Feb 2024 17:25:42 +0100 Subject: [PATCH 2/2] Add useTransition hook --- doc/HOOKS.md | 3 +- doc/changelog/2.2.0-betas.md | 5 +++ .../scala/japgolly/scalajs/react/React.scala | 11 ++++++- .../japgolly/scalajs/react/hooks/Api.scala | 14 ++++++++ .../japgolly/scalajs/react/hooks/Hooks.scala | 32 +++++++++++++++++++ .../japgolly/scalajs/react/facade/Hooks.scala | 4 +++ .../japgolly/scalajs/react/facade/React.scala | 2 ++ .../scalajs/react/core/HooksTest.scala | 23 +++++++++++-- 8 files changed, 90 insertions(+), 4 deletions(-) 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()