Skip to content

Commit a2796e8

Browse files
committed
Fix infinite loop in functional components with reuse + Reusability.never
Closes #1027
1 parent 8a91bdc commit a2796e8

File tree

4 files changed

+109
-15
lines changed

4 files changed

+109
-15
lines changed

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

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -169,25 +169,38 @@ object CustomHook {
169169

170170
// ===================================================================================================================
171171

172+
private final case class ReusableDepState[+A](value: A, rev: Int) {
173+
var skipTest = true
174+
val asTuple = (value, rev)
175+
}
176+
172177
def reusableDeps[D](implicit r: Reusability[D]): CustomHook[() => D, (D, Int)] =
173178
CustomHook[() => D]
174-
.useState(Option.empty[(D, Int)])
179+
.useState[Option[ReusableDepState[D]]](None)
175180
.buildReturning { (getDeps, depsState) =>
176181
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-
}
182+
val nextDepState =
183+
depsState.value match {
184+
185+
// React is calling us again after the setState below
186+
// Don't re-test reusability, we could end up in an infinite-loop (see #1027)
187+
case Some(s) if s.skipTest =>
188+
s.skipTest = false
189+
s
190+
191+
// Previous result can be reused
192+
case Some(prevState) if r.test(prevState.value, next) =>
193+
prevState
194+
195+
// Not reusable
196+
case o =>
197+
val nextRev = o.fold(1)(_.rev + 1)
198+
val nextState = ReusableDepState(next, nextRev)
199+
val updateState = depsState.setState(Some(nextState)) // this causes a hook re-eval
200+
DefaultEffects.Sync.runSync(updateState)
201+
nextState
202+
}
203+
nextDepState.asTuple
191204
}
192205

193206
def reusableByDeps[D, A](create: (D, Int) => A)(implicit r: Reusability[D]): CustomHook[() => D, Reusable[A]] =

doc/changelog/2.0.1.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## 2.0.1
2+
3+
### Bug fixes
4+
5+
* Fix infinite loop in functional components with reuse + `Reusability.never` ([#1027](https://github.com/japgolly/scalajs-react/issues/1027))

tests/src/test/scala/japgolly/scalajs/react/core/HooksTest.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,35 @@ object HooksTest extends TestSuite {
12241224
}
12251225
}
12261226

1227+
// See https://github.com/japgolly/scalajs-react/issues/1027
1228+
private def testRenderWithReuseNever(): Unit = {
1229+
implicit val reusability: Reusability[PI] = Reusability.never
1230+
var renders = 0
1231+
val comp = ScalaFnComponent.withHooks[PI]
1232+
.renderWithReuse { p =>
1233+
renders += 1
1234+
<.div(s"P=$p, R=$renders")
1235+
}
1236+
1237+
val wrapper = ScalaComponent.builder[PI]
1238+
.initialStateFromProps(identity)
1239+
.renderS { ($, s) =>
1240+
<.div(
1241+
comp(s),
1242+
<.button(^.onClick --> $.modState(_ + 0)),
1243+
<.button(^.onClick --> $.modState(_ + 1)),
1244+
)
1245+
}
1246+
.build
1247+
1248+
withRenderedIntoBody(wrapper(PI(3))) { (_, root) =>
1249+
val t = new Tester(root)
1250+
t.assertText("P=PI(3), R=1")
1251+
t.clickButton(2); t.assertText("P=PI(4), R=2")
1252+
t.clickButton(1); t.assertText("P=PI(4), R=3")
1253+
}
1254+
}
1255+
12271256
// ===================================================================================================================
12281257

12291258
override def tests = Tests {
@@ -1289,5 +1318,6 @@ object HooksTest extends TestSuite {
12891318
"useStateSnapshot" - testUseStateSnapshot()
12901319
"useStateSnapshotWithReuse" - testUseStateSnapshotWithReuse()
12911320
"renderWithReuse" - testRenderWithReuse()
1321+
"renderWithReuseNever" - testRenderWithReuseNever()
12921322
}
12931323
}

tests/src/test/scala/japgolly/scalajs/react/core/ScalaFnComponentTest.scala

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import japgolly.scalajs.react._
44
import japgolly.scalajs.react.test.ReactTestUtils
55
import japgolly.scalajs.react.test.TestUtil._
66
import japgolly.scalajs.react.vdom.html_<^._
7+
import sourcecode.Line
78
import utest._
89

910
object ScalaFnComponentTest extends TestSuite {
@@ -54,5 +55,50 @@ object ScalaFnComponentTest extends TestSuite {
5455
assert(rendered == 3)
5556
}
5657
}
58+
59+
"reuse" - {
60+
var renders = 0
61+
val F = ScalaFnComponent.withReuse[Int] { p =>
62+
renders += 1
63+
<.span(p)
64+
}
65+
val C = ScalaComponent.builder[Int].render_P(F(_)).build
66+
ReactTestUtils.withRenderedIntoBody(C(7)) { (m, p) =>
67+
def test(expectedRenders: Int, expectedHtml: Int)(implicit q: Line): Unit = {
68+
val a = (renders, p.innerHTML.trim)
69+
val e = (expectedRenders, s"<span>$expectedHtml</span>")
70+
assertEq(a, e)
71+
}
72+
test(1, 7)
73+
ReactTestUtils.replaceProps(C, m)(7)
74+
test(1, 7)
75+
ReactTestUtils.replaceProps(C, m)(6)
76+
test(2, 6)
77+
}
78+
}
79+
80+
// See https://github.com/japgolly/scalajs-react/issues/1027
81+
"reuseNever" - {
82+
implicit def reusability: Reusability[Int] = Reusability.never
83+
var renders = 0
84+
val F = ScalaFnComponent.withReuse[Int] { p =>
85+
renders += 1
86+
<.span(p)
87+
}
88+
val C = ScalaComponent.builder[Int].render_P(F(_)).build
89+
ReactTestUtils.withRenderedIntoBody(C(7)) { (m, p) =>
90+
def test(expectedRenders: Int, expectedHtml: Int)(implicit q: Line): Unit = {
91+
val a = (renders, p.innerHTML.trim)
92+
val e = (expectedRenders, s"<span>$expectedHtml</span>")
93+
assertEq(a, e)
94+
}
95+
test(1, 7)
96+
ReactTestUtils.replaceProps(C, m)(7)
97+
test(2, 7)
98+
ReactTestUtils.replaceProps(C, m)(6)
99+
test(3, 6)
100+
}
101+
}
102+
57103
}
58104
}

0 commit comments

Comments
 (0)