-
-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useState returns a new callback instance each time #496
Comments
You could say to leave out |
What I don't understand is that even memoizing the callback doesn't work. Even though it is only ever created once, React thinks it has changed: package hello.world
import hello.world.SafeHooks.useStateSafely
import org.scalajs.dom.console
import slinky.core._
import slinky.core.annotations.react
import slinky.core.facade.Hooks.{useEffect, useMemo}
import slinky.core.facade.SetStateHookCallback
import slinky.web.html._
import scala.annotation.unused
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.scalajs.js.|
@JSImport("resources/App.css", JSImport.Default)
@js.native
object AppCSS extends js.Object
@JSImport("resources/logo.svg", JSImport.Default)
@js.native
object ReactLogo extends js.Object
object SafeHooks {
@inline def useStateSafely[T](default: T): (T, SetStateHookCallback[T]) = {
val call = SafeHooksRaw.useState[T](default)
val callback = useMemo(() => {
console.log("Creating new memoized callback")
new SetStateHookCallback[T](call._2)
}, List(call._2))
(call._1, callback)
}
}
@js.native
@JSImport("react", JSImport.Namespace, "React")
private[world] object SafeHooksRaw extends js.Object {
def useState[T](@unused default: T | js.Function0[T]): js.Tuple2[T, js.Function1[js.Any, Unit]] = js.native
}
@react object App {
type Props = Unit
private val css = AppCSS
val component = FunctionalComponent[Props] { _ =>
console.log("Rendering")
val (count, setCount) = useStateSafely(0)
useEffect(() => {
console.log("count changed")
}, List(count))
useEffect(() => {
console.log("setCount changed")
}, List(setCount))
div(className:="App")(
p(s"Count: $count"),
button(onClick:=(() => setCount(c => c + 1)))("Increment")
)
}
}
|
Finally figured it out. It is because This works: package hello.world
import hello.world.SafeHooks.useStateSafely
import org.scalajs.dom.console
import slinky.core._
import slinky.core.annotations.react
import slinky.core.facade.Hooks.{useEffect, useMemo}
import slinky.web.html._
import scala.annotation.unused
import scala.language.implicitConversions
import scala.scalajs.js
import scala.scalajs.js.annotation.{JSImport, JSName}
import scala.scalajs.js.|
@JSImport("resources/App.css", JSImport.Default)
@js.native
object AppCSS extends js.Object
@JSImport("resources/logo.svg", JSImport.Default)
@js.native
object ReactLogo extends js.Object
@JSImport("resources/uniqueId.js", JSImport.Default)
@js.native
object UniqueId extends js.Object
final class JsSetStateHookCallback[T](private val origFunction: js.Function1[js.Any, Unit]) extends js.Object {
@JSName("setState")
@inline def apply(newState: T): Unit =
origFunction.apply(newState.asInstanceOf[js.Any])
@JSName("transformState")
@inline def apply(transformState: T => T): Unit =
origFunction.apply(transformState: js.Function1[T, T])
}
object JsSetStateHookCallback {
@inline implicit def toFunction[T](callback: JsSetStateHookCallback[T]): T => Unit = callback(_)
@inline implicit def toTransformFunction[T](callback: JsSetStateHookCallback[T]): (T => T) => Unit = callback(_)
}
object SafeHooks {
@inline def useStateSafely[T](default: T): (T, JsSetStateHookCallback[T]) = {
val call = SafeHooksRaw.useState[T](default)
val callback = useMemo(() => {
console.log("Creating new memoized callback")
new JsSetStateHookCallback[T](call._2)
}, List(call._2))
(call._1, callback)
}
}
@js.native
@JSImport("react", JSImport.Namespace, "React")
private[world] object SafeHooksRaw extends js.Object {
def useState[T](@unused default: T | js.Function0[T]): js.Tuple2[T, js.Function1[js.Any, Unit]] = js.native
}
@react object App {
type Props = Unit
private val css = AppCSS
private val uniqueId = UniqueId
@js.native
trait WithUniqueId extends js.Object {
def uniqueId: Int = js.native
}
implicit def withUniqueId(a: Any): WithUniqueId = a.asInstanceOf[WithUniqueId]
val component = FunctionalComponent[Props] { _ =>
console.log("Rendering")
val (count, setCount) = useStateSafely(0)
console.log(s"setCount id: ${setCount.uniqueId} ${setCount.uniqueId}")
useEffect(() => {
console.log("count changed")
}, List(count))
useEffect(() => {
console.log(s"setCount changed: ${setCount.uniqueId} ${setCount.uniqueId}")
}, List(setCount))
div(className:="App")(
p(s"Count: $count"),
button(onClick:=(() => setCount(c => c + 1)))("Increment"),
button(onClick:=(() => throw new RuntimeException("Boom")))("Crash")
)
}
} With (function() {
let id_counter = 1;
Object.defineProperty(Object.prototype, "__uniqueId", {
writable: true
});
Object.defineProperty(Object.prototype, "uniqueId", {
get: function() {
if (typeof this.__uniqueId === "undefined")
this.__uniqueId = id_counter++;
return this.__uniqueId;
}
});
}()); |
Is there something funny going on with |
Ah, this is a good catch! I think the reason AnyVal doesn't work is because Scala.js needs to support instance checks at runtime. I think the solution would be to use extension methods instead of immediately wrapping the callback. |
Hooks.useState
is returning a newSetStateHookCallback
instance each time which causes comparison problems.In vanilla React (sandbox):
Clicking the button will change the
count
and socount changed
will be logged each time.In Slinky (repo):
Clicking the button will change the
count
andsetCount
and so bothcount changed
andsetCount changed
will be logged each time.The text was updated successfully, but these errors were encountered: