Skip to content

Commit

Permalink
Implement useCallback using useMemo.
Browse files Browse the repository at this point in the history
Since the purpose of `useCallback` is to guarantee _refererence_
equality, the use of React's useCallback as the underlying
implementation will require a conversion back from `js.Function` to
`scala.FunctionN` and that will create a new object every time and
thus not serve the intended purpose.
  • Loading branch information
ramnivas committed Jan 24, 2020
1 parent 38b14f6 commit 2ac6153
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 139 deletions.
120 changes: 6 additions & 114 deletions core/src/main/scala/slinky/core/facade/React.scala
Original file line number Diff line number Diff line change
Expand Up @@ -178,30 +178,6 @@ private[slinky] object HooksRaw extends js.Object {
def useReducer[T, A](reducer: js.Function2[T, A, T], initialState: T): js.Tuple2[T, js.Function1[A, Unit]] = js.native
def useReducer[T, I, A](reducer: js.Function2[T, A, T], initialState: I, init: js.Function1[I, T]): js.Tuple2[T, js.Function1[A, Unit]] = js.native

def useCallback[R](callback: js.Function0[R], watchedObjects: js.Array[js.Any]): js.Function0[R] = js.native
def useCallback[T1, R](callback: js.Function1[T1, R], watchedObjects: js.Array[js.Any]): js.Function1[T1, R] = js.native
def useCallback[T1, T2, R](callback: js.Function2[T1, T2, R], watchedObjects: js.Array[js.Any]): js.Function2[T1, T2, R] = js.native
def useCallback[T1, T2, T3, R](callback: js.Function3[T1, T2, T3, R], watchedObjects: js.Array[js.Any]): js.Function3[T1, T2, T3, R] = js.native
def useCallback[T1, T2, T3, T4, R](callback: js.Function4[T1, T2, T3, T4, R], watchedObjects: js.Array[js.Any]): js.Function4[T1, T2, T3, T4, R] = js.native
def useCallback[T1, T2, T3, T4, T5, R](callback: js.Function5[T1, T2, T3,T4,T5, R], watchedObjects: js.Array[js.Any]): js.Function5[T1, T2, T3, T4, T5, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, R](callback: js.Function6[T1, T2, T3,T4,T5, T6, R], watchedObjects: js.Array[js.Any]): js.Function6[T1, T2, T3, T4, T5, T6, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, R](callback: js.Function7[T1, T2, T3,T4,T5, T6, T7, R], watchedObjects: js.Array[js.Any]): js.Function7[T1, T2, T3, T4, T5, T6, T7, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, R](callback: js.Function8[T1, T2, T3,T4,T5, T6, T7, T8, R], watchedObjects: js.Array[js.Any]): js.Function8[T1, T2, T3, T4, T5, T6, T7, T8, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, R](callback: js.Function9[T1, T2, T3,T4,T5, T6, T7, T8, T9, R], watchedObjects: js.Array[js.Any]): js.Function9[T1, T2, T3, T4, T5, T6, T7, T8, T9, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R](callback: js.Function10[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, R], watchedObjects: js.Array[js.Any]): js.Function10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R](callback: js.Function11[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, R], watchedObjects: js.Array[js.Any]): js.Function11[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, R](callback: js.Function12[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, R], watchedObjects: js.Array[js.Any]): js.Function12[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, R](callback: js.Function13[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, R], watchedObjects: js.Array[js.Any]): js.Function13[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, R](callback: js.Function14[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, R], watchedObjects: js.Array[js.Any]): js.Function14[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, R](callback: js.Function15[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, R], watchedObjects: js.Array[js.Any]): js.Function15[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, R](callback: js.Function16[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, R], watchedObjects: js.Array[js.Any]): js.Function16[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, R](callback: js.Function17[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, R], watchedObjects: js.Array[js.Any]): js.Function17[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, R](callback: js.Function18[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, R], watchedObjects: js.Array[js.Any]): js.Function18[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, R](callback: js.Function19[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, R], watchedObjects: js.Array[js.Any]): js.Function19[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, R](callback: js.Function20[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, R], watchedObjects: js.Array[js.Any]): js.Function20[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, R](callback: js.Function21[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, R], watchedObjects: js.Array[js.Any]): js.Function21[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, R] = js.native
def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, R](callback: js.Function22[T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, R], watchedObjects: js.Array[js.Any]): js.Function22[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, R] = js.native

def useMemo[T](callback: js.Function0[T], watchedObjects: js.Array[js.Any]): T = js.native

def useRef[T](initialValue: T): ReactRef[T] = js.native
Expand All @@ -212,6 +188,8 @@ private[slinky] object HooksRaw extends js.Object {
def useLayoutEffect(thunk: js.Function0[EffectCallbackReturn], watchedObjects: js.Array[js.Any]): Unit = js.native

def useDebugValue(value: String): Unit = js.native

// No useCallback, since its usage from Hooks won't be able to implement the reference equality guarantee while converting js.Function to scala.FunctionN, anyway
}

@js.native trait EffectCallbackReturn extends js.Object
Expand Down Expand Up @@ -275,96 +253,10 @@ object Hooks {
(ret._1, ret._2)
}

@inline def useCallback[R](callback: () => R, watchedObjects: Iterable[Any]): () => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, R](callback: T1 => R, watchedObjects: Iterable[Any]): T1 => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, R](callback: (T1, T2) => R, watchedObjects: js.Array[js.Any]): (T1, T2) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, R](callback: (T1, T2, T3) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, R](callback: (T1, T2, T3, T4) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, R](callback: (T1, T2, T3,T4,T5) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, R](callback: (T1, T2, T3,T4,T5, T6) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, R](callback: (T1, T2, T3,T4,T5, T6, T7) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
}

@inline def useCallback[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, R](callback: (T1, T2, T3,T4,T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22) => R, watchedObjects: js.Array[js.Any]): (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22) => R = {
HooksRaw.useCallback(callback, watchedObjects.toJSArray.asInstanceOf[js.Array[js.Any]])
@inline def useCallback[F](callback: F, watchedObjects: Iterable[Any])(implicit witness: F => js.Function): F = {
// Do not implement using React's useCallback. Otherwise, converting the js.Function returned by to scala.FunctionN will
// produce a new object and thus violating the reference equality guarantee of useCallback
useMemo(() => callback, watchedObjects)
}

@inline def useMemo[T](memoValue: () => T, watchedObjects: Iterable[Any]): T = {
Expand Down
62 changes: 37 additions & 25 deletions tests/src/test/scala/slinky/core/HooksComponentTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -241,50 +241,62 @@ class HooksComponentTest extends AsyncFunSuite {
assert(container.innerHTML == "123")
}

test("useCallback produces callable function") {
test("useCallback maintains reference equality unless dependency changes") {
val container = document.createElement("div")

var called = false
val component = FunctionalComponent[Unit] { props =>
var callbackRef: () => String = null

val component = FunctionalComponent[(Int, String)] { props =>
val callback = useCallback(() => {
called = true
}, Seq.empty)
props._2 // purposefully wrong (so that we can assert that untracked dependency retains old value)
}, Seq(props._1))

callbackRef = callback
callback()

""
}

ReactDOM.render(
component(),
container
)
ReactDOM.render(component(1, "one"), container)
assert(container.innerHTML == "one")
assert(callbackRef != null)

assert(called)
var prevCallbackRef = callbackRef
ReactDOM.render(component(1, "one_"), container)
assert(container.innerHTML == "one")
assert(callbackRef eq prevCallbackRef) // note that we use eq to check for reference equality

prevCallbackRef = callbackRef
ReactDOM.render(component(2, "two"), container)
assert(callbackRef ne prevCallbackRef) // note that we use ne to check for reference inequality
assert(container.innerHTML == "two")
}

test("useCallback with arguments produces callable function") {
test("useCallback with arguments maintains reference equality unless dependency changes") {
val container = document.createElement("div")

var called = false
var callbackRef: Boolean => String = null

val component = FunctionalComponent[Unit] { props =>
val component = FunctionalComponent[(Int, String)] { props =>
val callback = useCallback((value: Boolean) => {
called = value
}, Seq.empty)
props._2 // purposefully wrong (so that we can assert that untracked dependency retains old value)
}, Seq(props._1))

callbackRef = callback
callback(true)

""
}

ReactDOM.render(
component(),
container
)
ReactDOM.render(component(1, "one"), container)
assert(container.innerHTML == "one")
assert(callbackRef != null)

var prevCallbackRef = callbackRef
ReactDOM.render(component(1, "one_"), container)
assert(container.innerHTML == "one")
assert(callbackRef eq prevCallbackRef) // note that we use eq to check for reference equality

assert(called)
prevCallbackRef = callbackRef
ReactDOM.render(component(2, "two"), container)
assert(callbackRef ne prevCallbackRef) // note that we use ne to check for reference inequality
assert(container.innerHTML == "two")
}

test("useMemo only recalculates when watched objects change") {
Expand Down

0 comments on commit 2ac6153

Please sign in to comment.