diff --git a/doc/HOOKS.md b/doc/HOOKS.md
index e533fa8cd..9933b05fe 100644
--- a/doc/HOOKS.md
+++ b/doc/HOOKS.md
@@ -192,6 +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` |
+| `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 f905850f2..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
@@ -477,6 +477,25 @@ 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.
+ *
+ * @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 16a7abd48..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._
@@ -438,4 +439,42 @@ object Hooks {
F.delay { value = f(value) }
}
+
+ // ===================================================================================================================
+
+ object UseId {
+ 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 d892d3eb1..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]]]
@@ -46,4 +48,8 @@ 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
+
+ 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 407954639..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
@@ -800,6 +800,34 @@ 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 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)
@@ -1206,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")
@@ -1336,6 +1363,10 @@ object HooksTest extends TestSuite {
"useStateSnapshot" - testUseStateSnapshot()
"useStateSnapshotWithReuse" - testUseStateSnapshotWithReuse()
+ "useId" - testUseId()
+
+ "useTransition" - testUseTransition()
+
"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