Skip to content

Commit e00f8fc

Browse files
authored
Merge pull request #938 from japgolly/topic/hooksShouldComponentUpdate
Add `shouldComponentUpdate` behaviour to hooks
2 parents d0dfe0f + 024a9f3 commit e00f8fc

File tree

9 files changed

+403
-51
lines changed

9 files changed

+403
-51
lines changed

coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Api.scala

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package japgolly.scalajs.react.hooks
22

3+
import japgolly.scalajs.react.component.ScalaFn.Component
34
import japgolly.scalajs.react.component.{Js => JsComponent, Scala => ScalaComponent}
45
import japgolly.scalajs.react.feature.Context
56
import japgolly.scalajs.react.hooks.Hooks._
7+
import japgolly.scalajs.react.internal.Box
68
import japgolly.scalajs.react.util.DefaultEffects
7-
import japgolly.scalajs.react.vdom.TopNode
8-
import japgolly.scalajs.react.{CtorType, Ref, Reusability, Reusable}
9+
import japgolly.scalajs.react.util.Util.identityFn
10+
import japgolly.scalajs.react.vdom.{TopNode, VdomNode}
11+
import japgolly.scalajs.react.{Children, CtorType, Ref, Reusability, Reusable}
912
import scala.reflect.ClassTag
1013
import scala.scalajs.js
1114

@@ -672,4 +675,32 @@ object Api {
672675
final def useStateWithReuseBy[S: ClassTag: Reusability](initialState: CtxFn[S])(implicit step: Step): step.Next[UseStateWithReuse[S]] =
673676
useStateWithReuseBy(step.squash(initialState)(_))
674677
}
678+
679+
// ===================================================================================================================
680+
681+
trait PrimaryWithRender[P, C <: Children, Ctx, _Step <: AbstractStep] extends Primary[Ctx, _Step] {
682+
def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], C]): Component[P, s.CT]
683+
684+
final def renderReusable[A](f: Ctx => Reusable[A])(implicit s: CtorType.Summoner[Box[P], C], v: A => VdomNode): Component[P, s.CT] =
685+
renderWithReuseBy(f(_).map(v))(_.value)
686+
687+
final def renderWithReuse(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], C], r: Reusability[Ctx]): Component[P, s.CT] =
688+
renderWithReuseBy(identityFn[Ctx])(f)
689+
690+
def renderWithReuseBy[A](reusableInputs: Ctx => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], C], r: Reusability[A]): Component[P, s.CT]
691+
}
692+
693+
trait SecondaryWithRender[P, C <: Children, Ctx, CtxFn[_], _Step <: SubsequentStep[Ctx, CtxFn]] extends PrimaryWithRender[P, C, Ctx, _Step] with Secondary[Ctx, CtxFn, _Step] {
694+
final def render(f: CtxFn[VdomNode])(implicit step: Step, s: CtorType.Summoner[Box[P], C]): Component[P, s.CT] =
695+
render(step.squash(f)(_))
696+
697+
final def renderReusable[A](f: CtxFn[Reusable[A]])(implicit step: Step, s: CtorType.Summoner[Box[P], C], v: A => VdomNode): Component[P, s.CT] =
698+
renderReusable(step.squash(f)(_))
699+
700+
final def renderWithReuse(f: CtxFn[VdomNode])(implicit step: Step, s: CtorType.Summoner[Box[P], C], r: Reusability[Ctx]): Component[P, s.CT] =
701+
renderWithReuse(step.squash(f)(_))
702+
703+
final def renderWithReuseBy[A](reusableInputs: CtxFn[A])(f: A => VdomNode)(implicit step: Step, s: CtorType.Summoner[Box[P], C], r: Reusability[A]): Component[P, s.CT] =
704+
renderWithReuseBy(step.squash(reusableInputs)(_))(f)
705+
}
675706
}

coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/CustomHook.scala

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package japgolly.scalajs.react.hooks
22

33
import japgolly.microlibs.types.NaturalComposition
4+
import japgolly.scalajs.react.internal.ShouldComponentUpdateComponent
45
import japgolly.scalajs.react.util.DefaultEffects
6+
import japgolly.scalajs.react.vdom.VdomNode
57
import japgolly.scalajs.react.{PropsChildren, Reusability, Reusable}
68

79
final class CustomHook[I, O] private[CustomHook] (val unsafeInit: I => O) extends AnyVal {
@@ -167,36 +169,43 @@ object CustomHook {
167169

168170
// ===================================================================================================================
169171

170-
def reusableDeps[D](implicit r: Reusability[D]): CustomHook[() => D, Int] =
172+
def reusableDeps[D](implicit r: Reusability[D]): CustomHook[() => D, (D, Int)] =
171173
CustomHook[() => D]
172-
.useState((0, Option.empty[D]))
174+
.useState(Option.empty[(D, Int)])
173175
.buildReturning { (getDeps, depsState) =>
174-
val rev = depsState.value._1
175-
lazy val next = getDeps()
176-
val updateNeeded =
177-
depsState.value._2 match {
178-
case Some(cur) =>
179-
// println(s"$cur => $next? update=${r.updateNeeded(cur, next)}}")
180-
r.updateNeeded(cur, next)
181-
case None =>
182-
true
183-
}
184-
if (updateNeeded) {
185-
val newRev = rev + 1
186-
val f = depsState.setState((newRev, Some(next)))
187-
DefaultEffects.Sync.runSync(f)
188-
newRev
189-
} else
190-
rev
176+
val next = getDeps()
177+
depsState.value match {
178+
179+
// Previous result can be reused
180+
case Some(prevState @ (prev, _)) if r.test(prev, next) =>
181+
prevState
182+
183+
// Not reusable
184+
case o =>
185+
val nextRev = o.fold(1)(_._2 + 1)
186+
val nextState = (next, nextRev)
187+
val updateState = depsState.setState(Some(nextState))
188+
DefaultEffects.Sync.runSync(updateState)
189+
nextState
190+
}
191191
}
192192

193-
def reusableByDeps[A, D](create: Int => A)(implicit r: Reusability[D]): CustomHook[() => D, Reusable[A]] =
194-
reusableDeps[D].map(rev => Reusable.implicitly(rev).withValue(create(rev)))
193+
def reusableByDeps[A, D](create: (D, Int) => A)(implicit r: Reusability[D]): CustomHook[() => D, Reusable[A]] =
194+
reusableDeps[D].map { case (d, rev) => Reusable.implicitly(rev).withValue(create(d, rev)) }
195+
196+
def shouldComponentUpdate[D](render: D => VdomNode)(implicit r: Reusability[D]): CustomHook[() => D, VdomNode] =
197+
reusableDeps[D].map { case (d, rev) =>
198+
ShouldComponentUpdateComponent(rev, () => render(d))
199+
}
195200

196201
lazy val useForceUpdate: CustomHook[Unit, Reusable[DefaultEffects.Sync[Unit]]] = {
197202
val inc: Int => Int = _ + 1
198203
CustomHook[Unit]
199204
.useState(0)
200-
.buildReturning(_.hook1.modState.map(_(inc)))
205+
.buildReturning($ => {
206+
val s = $.hook1
207+
Reusable.implicitly(s.value).withLazyValue(s.modState.map(_(inc)).value)
208+
})
201209
}
210+
202211
}

coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/HookComponentBuilder.scala

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import japgolly.scalajs.react.component.ScalaFn
44
import japgolly.scalajs.react.component.ScalaFn.Component
55
import japgolly.scalajs.react.internal.Box
66
import japgolly.scalajs.react.vdom.VdomNode
7-
import japgolly.scalajs.react.{Children, CtorType, PropsChildren}
7+
import japgolly.scalajs.react.{Children, CtorType, PropsChildren, Reusability}
88

99
object HookComponentBuilder {
1010

@@ -16,7 +16,7 @@ object HookComponentBuilder {
1616

1717
object ComponentP {
1818

19-
final class First[P](init: P => Unit) extends Api.Primary[P, FirstStep[P]] {
19+
final class First[P](init: P => Unit) extends Api.PrimaryWithRender[P, Children.None, P, FirstStep[P]] {
2020

2121
override protected def self(f: P => Any)(implicit step: Step): step.Self =
2222
step.self(init, f)
@@ -27,25 +27,33 @@ object HookComponentBuilder {
2727
def withPropsChildren: ComponentPC.First[P] =
2828
new ComponentPC.First(ctx => init(ctx.props))
2929

30-
def render(f: P => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] =
30+
override def render(f: P => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] =
3131
ScalaFn(f)
32+
33+
override def renderWithReuseBy[A](reusableInputs: P => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[A]): Component[P, s.CT] =
34+
customBy(p => CustomHook.shouldComponentUpdate(f).apply(() => reusableInputs(p)))
35+
.render((_: HookCtx.P1[P, VdomNode]).hook1)
3236
}
3337

3438
type RenderFn[-P, +Ctx] = (Ctx => VdomNode) => P => VdomNode
3539

36-
final class Subsequent[P, Ctx, CtxFn[_]](renderFn: RenderFn[P, Ctx]) extends Api.Secondary[Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] {
40+
final class Subsequent[P, Ctx, CtxFn[_]](renderFn: RenderFn[P, Ctx]) extends Api.SecondaryWithRender[P, Children.None, Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] {
3741

3842
override protected def self(f: Ctx => Any)(implicit step: Step): step.Self =
3943
step.self(renderFn, f)
4044

4145
override protected def next[H](f: Ctx => H)(implicit step: Step): step.Next[H] =
4246
step.next[H](renderFn, f)
4347

44-
def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] =
48+
override def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] =
4549
ScalaFn(renderFn(f))
4650

47-
def render(f: CtxFn[VdomNode])(implicit step: Step, s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] =
48-
render(step.squash(f)(_))
51+
override def renderWithReuseBy[A](reusableInputs: Ctx => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[A]): Component[P, s.CT] = {
52+
val hook = CustomHook.shouldComponentUpdate(f)
53+
ScalaFn(renderFn { ctx =>
54+
hook.unsafeInit(() => reusableInputs(ctx))
55+
})
56+
}
4957
}
5058

5159
object Subsequent extends ComponentP_SubsequentDsl
@@ -99,36 +107,45 @@ object HookComponentBuilder {
99107

100108
object ComponentPC {
101109

102-
final class First[P](init: HookCtx.PC0[P] => Unit) extends Api.Primary[HookCtx.PC0[P], FirstStep[P]] {
110+
final class First[P](init: HookCtx.PC0[P] => Unit) extends Api.PrimaryWithRender[P, Children.Varargs, HookCtx.PC0[P], FirstStep[P]] {
111+
type Ctx = HookCtx.PC0[P]
103112

104-
override protected def self(f: HookCtx.PC0[P] => Any)(implicit step: Step): step.Self =
113+
override protected def self(f: Ctx => Any)(implicit step: Step): step.Self =
105114
step.self(init, f)
106115

107-
override protected def next[H](f: HookCtx.PC0[P] => H)(implicit step: Step): step.Next[H] =
116+
override protected def next[H](f: Ctx => H)(implicit step: Step): step.Next[H] =
108117
step.next(init, f)
109118

110-
def render(f: HookCtx.PC0[P] => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] =
119+
override def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] =
111120
ScalaFn.withChildren((p: P, pc: PropsChildren) => f(HookCtx.withChildren(p, pc)))
112121

113122
def render(f: (P, PropsChildren) => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] =
114123
ScalaFn.withChildren(f)
124+
125+
override def renderWithReuseBy[A](reusableInputs: Ctx => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A]): Component[P, s.CT] =
126+
customBy(ctx => CustomHook.shouldComponentUpdate(f).apply(() => reusableInputs(ctx)))
127+
.render((_: HookCtx.PC1[P, VdomNode]).hook1)
115128
}
116129

117130
type RenderFn[-P, +Ctx] = (Ctx => VdomNode) => (P, PropsChildren) => VdomNode
118131

119-
final class Subsequent[P, Ctx, CtxFn[_]](renderFn: RenderFn[P, Ctx]) extends Api.Secondary[Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] {
132+
final class Subsequent[P, Ctx, CtxFn[_]](renderFn: RenderFn[P, Ctx]) extends Api.SecondaryWithRender[P, Children.Varargs, Ctx, CtxFn, SubsequentStep[P, Ctx, CtxFn]] {
120133

121134
override protected def self(f: Ctx => Any)(implicit step: Step): step.Self =
122135
step.self(renderFn, f)
123136

124137
override protected def next[H](f: Ctx => H)(implicit step: Step): step.Next[H] =
125138
step.next[H](renderFn, f)
126139

127-
def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] =
140+
override def render(f: Ctx => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] =
128141
ScalaFn.withChildren(renderFn(f))
129142

130-
def render(f: CtxFn[VdomNode])(implicit step: Step, s: CtorType.Summoner[Box[P], Children.Varargs]): Component[P, s.CT] =
131-
render(step.squash(f)(_))
143+
override def renderWithReuseBy[A](reusableInputs: Ctx => A)(f: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A]): Component[P, s.CT] = {
144+
val hook = CustomHook.shouldComponentUpdate(f)
145+
ScalaFn.withChildren(renderFn { ctx =>
146+
hook.unsafeInit(() => reusableInputs(ctx))
147+
})
148+
}
132149
}
133150

134151
object Subsequent extends ComponentPC_SubsequentDsl

0 commit comments

Comments
 (0)