Skip to content

Commit

Permalink
Merge pull request #1091 from hugo-vrijswijk/react-18-hooks
Browse files Browse the repository at this point in the history
react 18 hooks
  • Loading branch information
rpiaggio authored Mar 6, 2024
2 parents 9702521 + 1dfbd89 commit 3e88f01
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 3 deletions.
2 changes: 2 additions & 0 deletions doc/HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ object Example {
| `useRef()` | `.useRefToAnyVdom` <br> `.useRefToVdom[DomType]` <br> `.useRefToScalaComponent(component)` <br> `.useRefToScalaComponent[P, S, B]` <br> `.useRefToJsComponent(component)` <br> `.useRefToJsComponent[P, S]` <br> `.useRefToJsComponentWithMountedFacade[P, S, F]` |
| `useRef(initialValue)` | `.useRef(initialValue)` |
| `useState(initialState)` <br> `useState(() => initialState)` | `.useState(initialState)` |
| `useId()` | `.useId` |
| `useTransition` | `.useTransition` |
| Custom hook <br> `useBlah(i)` | `.custom(useBlah(i))`

Note: The reason that `[deps]` on the JS side becomes `(deps)` on the Scala side,
Expand Down
5 changes: 5 additions & 0 deletions doc/changelog/2.2.0-betas.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

// ===================================================================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -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._
Expand Down Expand Up @@ -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)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]]]
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -1336,6 +1363,10 @@ object HooksTest extends TestSuite {
"useStateSnapshot" - testUseStateSnapshot()
"useStateSnapshotWithReuse" - testUseStateSnapshotWithReuse()

"useId" - testUseId()

"useTransition" - testUseTransition()

"renderWithReuse" - {
"main" - testRenderWithReuse()
"never" - testRenderWithReuseNever()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3e88f01

Please sign in to comment.