Skip to content

Commit 8197dd3

Browse files
committed
Checkpoint: UseState on Scala 2
1 parent 89ee1f2 commit 8197dd3

File tree

10 files changed

+375
-73
lines changed

10 files changed

+375
-73
lines changed
Lines changed: 151 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package japgolly.scalajs.react.hooks
22

33
import japgolly.microlibs.compiletime.MacroUtils
4-
import japgolly.scalajs.react.{Children, CtorType}
54
import japgolly.scalajs.react.component.ScalaFn.Component
65
import japgolly.scalajs.react.hooks.Api._
7-
import japgolly.scalajs.react.internal.Box
6+
import japgolly.scalajs.react.internal.{Box, MacroLogger}
87
import japgolly.scalajs.react.vdom.VdomNode
8+
import japgolly.scalajs.react.{Children, CtorType}
9+
import scala.annotation.{nowarn, tailrec}
910
import scala.reflect.macros.blackbox.Context
1011

1112
object HookMacros {
@@ -19,36 +20,166 @@ object HookMacros {
1920
}
2021
}
2122

22-
@annotation.nowarn("cat=unused") // TODO: remove
23+
// =====================================================================================================================
24+
2325
class HookMacros(val c: Context) extends MacroUtils {
2426
import c.universe._
2527

26-
def render[P, C <: Children, Ctx, CtxFn[_], Step <: SubsequentStep[Ctx, CtxFn]]
27-
(f: c.Tree)(step: c.Tree, s: c.Tree): c.Tree = {
28+
private implicit def autoTagToType[A](t: c.WeakTypeTag[A]): Type = t.tpe
2829

29-
var debug = false // showCode(c.macroApplication).contains("counter.value")
30+
private def Box : Tree = q"_root_.japgolly.scalajs.react.internal.Box"
31+
private def Box(t: Type): Type = appliedType(c.typeOf[Box[_]], t)
32+
private def Hooks : Tree = q"_root_.japgolly.scalajs.react.hooks.Hooks"
33+
private def JsFn : Tree = q"_root_.japgolly.scalajs.react.component.JsFn"
34+
private def React : Tree = q"_root_.japgolly.scalajs.react.facade.React"
35+
private def ScalaFn : Tree = q"_root_.japgolly.scalajs.react.component.ScalaFn"
36+
private def withHooks = "withHooks"
3037

31-
def println(args: Any*): Unit =
32-
if (debug) System.out.println(args.mkString)
38+
case class HookDefn(propsType: Tree, steps: List[HookStep])
39+
case class HookStep(name: String, targs: List[Tree], args: List[List[Tree]])
3340

34-
println("="*120)
35-
println()
41+
def render[P, C <: Children, Ctx, CtxFn[_], Step <: SubsequentStep[Ctx, CtxFn]]
42+
(f: c.Tree)(step: c.Tree, s: c.Tree)
43+
(implicit P: c.WeakTypeTag[P], C: c.WeakTypeTag[C]): c.Tree = {
44+
45+
implicit val log = MacroLogger()
46+
// log.enabled = showCode(c.macroApplication).contains("counter.value")
47+
log.header()
48+
log("macroApplication", showRaw(c.macroApplication))
3649

3750
val self = c.prefix
3851

39-
val result = q"""
40-
{
41-
val f = $step.squash($f)
42-
$self.render(f)($s)
43-
}
44-
"""
52+
val parsed = parseHookDefn(c.macroApplication, Nil, Nil, Nil)
4553

46-
if (debug)
47-
println("RESULT:\n" + showCode(result))
54+
val inlined = parsed
55+
.flatMap(inlineHookDefn)
56+
.map(inlineHookRawComponent[P])
57+
.map(inlineHookComponent[P, C](_, s))
4858

49-
println()
50-
println("="*120)
59+
val result: Tree =
60+
inlined match {
61+
case Right(r) =>
62+
r
63+
case Left(e) =>
64+
log(e())
65+
q"""
66+
val f = $step.squash($f)
67+
$self.render(f)($s)
68+
"""
69+
}
5170

71+
log.footer(showCode(result))
5272
result
5373
}
74+
75+
@tailrec
76+
private def parseHookDefn(tree: Tree, targs: List[Tree], args: List[List[Tree]], steps: List[HookStep])
77+
(implicit log: MacroLogger): Either[() => String, HookDefn] =
78+
tree match {
79+
80+
case Apply(t, a) =>
81+
parseHookDefn(t, targs, a :: args, steps)
82+
83+
case TypeApply(t, a) =>
84+
if (targs.isEmpty)
85+
parseHookDefn(t, a, args, steps)
86+
else
87+
Left(() => "Multiple type arg clauses found at " + showRaw(tree))
88+
89+
case Select(t, n) =>
90+
val name = n.toString
91+
if (name == withHooks) {
92+
if (args.nonEmpty)
93+
Left(() => s"$withHooks called with args when none exepcted: ${args.map(_.map(showCode(_)))}")
94+
else
95+
targs match {
96+
case props :: Nil => Right(HookDefn(props, steps))
97+
case Nil => Left(() => s"$withHooks called without targs, unable to discern Props type.")
98+
case _ => Left(() => s"$withHooks called multiple targs: ${targs.map(showCode(_))}")
99+
}
100+
} else {
101+
val step = HookStep(name, targs, args)
102+
log(s"Found step '$name'", step)
103+
parseHookDefn(t, Nil, Nil, step :: steps)
104+
}
105+
106+
case _ =>
107+
Left(() => "Don't know how to parse " + showRaw(tree))
108+
}
109+
110+
private type RenderInliner = (Tree, Init) => Tree
111+
112+
private def inlineHookDefn(h: HookDefn)(implicit log: MacroLogger): Either[() => String, RenderInliner] = {
113+
val init = new Init("hook" + _, lazyVals = false)
114+
val it = h.steps.iterator
115+
var stepId = 0
116+
var renderStep: HookStep = null
117+
var hooks = List.empty[TermName]
118+
while (it.hasNext) {
119+
val step = it.next()
120+
if (it.hasNext) {
121+
stepId += 1
122+
inlineHookStep(stepId, step, init) match {
123+
case Right(termName) => hooks ::= termName
124+
case Left(e) => return Left(e)
125+
}
126+
} else
127+
renderStep = step
128+
}
129+
hooks = hooks.reverse
130+
131+
hookRenderInliner(renderStep, hooks.map(Ident(_))).map { f =>
132+
(props, init2) => {
133+
init2 ++= init.stmts
134+
f(props, init2)
135+
}
136+
}
137+
}
138+
139+
private def inlineHookStep(stepId: Int, step: HookStep, init: Init)(implicit log: MacroLogger): Either[() => String, TermName] = {
140+
log("inlineHookStep." + step.name, step)
141+
step.name match {
142+
case "useState" =>
143+
val stateType = step.targs.head
144+
val arg = step.args.head.head
145+
val rawName = TermName("hook" + stepId + "_raw")
146+
val name = TermName("hook" + stepId)
147+
init += q"val $rawName = $React.useStateFn(() => $Box[$stateType]($arg))"
148+
init += q"val $name = $Hooks.UseState.fromJsBoxed[$stateType]($rawName)"
149+
Right(name)
150+
151+
case _ =>
152+
Left(() => s"Inlining of hook method '${step.name}' not yet supported.")
153+
}
154+
}
155+
156+
private def hookRenderInliner(step: HookStep, hooks: List[Tree])(implicit log: MacroLogger): Either[() => String, RenderInliner] = {
157+
log("inlineHookRender." + step.name, step)
158+
step.name match {
159+
case "render" =>
160+
@nowarn("msg=exhaustive") val List(List(renderFn), _) = step.args
161+
Right { (props, _) =>
162+
val args = props :: hooks
163+
Apply(Select(renderFn, TermName("apply")), args)
164+
}
165+
166+
case _ =>
167+
Left(() => s"Inlining of hook render method '${step.name}' not yet supported.")
168+
}
169+
}
170+
171+
private def inlineHookRawComponent[P](renderInliner: RenderInliner)(implicit P: c.WeakTypeTag[P]): Tree = {
172+
val props_unbox = q"props.unbox"
173+
val init = new Init("_i" + _)
174+
val render1 = renderInliner(props_unbox, init)
175+
val render2 = init.wrap(q"$render1.rawNode")
176+
q"(props => $render2): $JsFn.RawComponent[${Box(P)}]"
177+
}
178+
179+
private def inlineHookComponent[P, C <: Children](rawComp: Tree, summoner: c.Tree)(implicit P: c.WeakTypeTag[P], C: c.WeakTypeTag[C]): Tree = {
180+
c.untypecheck(q"""
181+
val rawComponent = $rawComp
182+
$ScalaFn.fromBoxed($JsFn.fromJsFn[${Box(P)}, $C](rawComponent)($summoner))
183+
""")
184+
}
54185
}

library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/JsFn.scala

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import scala.scalajs.js
1111

1212
object JsFn extends JsBaseComponentTemplate[facade.React.StatelessFunctionalComponent] {
1313

14-
type Component[P <: js.Object, CT[-p, +u] <: CtorType[p, u]] = ComponentRoot[P, CT, Unmounted[P]]
15-
type Unmounted[P <: js.Object] = UnmountedRoot[P]
16-
type Mounted = Unit
14+
type RawComponent[P <: js.Object] = js.Function1[P with facade.PropsWithChildren, facade.React.Node]
15+
type Component [P <: js.Object, CT[-p, +u] <: CtorType[p, u]] = ComponentRoot[P, CT, Unmounted[P]]
16+
type Unmounted [P <: js.Object] = UnmountedRoot[P]
17+
type Mounted = Unit
1718

1819
def apply[P <: js.Object, C <: Children]
1920
(raw: js.Any)
@@ -27,9 +28,9 @@ object JsFn extends JsBaseComponentTemplate[facade.React.StatelessFunctionalComp
2728
componentRoot[P, s.CT, Unmounted[P]](rc, s.pf.rmap(s.summon(rc))(unmountedRoot))(s.pf)
2829
}
2930

30-
def fromJsFn[P <: js.Object, C <: Children](render: js.Function1[P with facade.PropsWithChildren, facade.React.Element])
31-
(implicit s: CtorType.Summoner[P, C]): Component[P, s.CT] =
32-
JsFn.force[P, C](render)(s)
31+
// TODO: This name... fromRaw? fromJs? Is it consistent across the project?
32+
@inline def fromJsFn[P <: js.Object, C <: Children](render: RawComponent[P])(implicit s: CtorType.Summoner[P, C]): Component[P, s.CT] =
33+
force[P, C](render)(s)
3334

3435
/** Type used when creating a JsFnComponent that ignores its props */
3536
type UnusedObject = Box[Unit]

library/coreGeneric/src/main/scala/japgolly/scalajs/react/component/ScalaFn.scala

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,19 @@ import japgolly.scalajs.react.hooks.HookComponentBuilder
44
import japgolly.scalajs.react.internal._
55
import japgolly.scalajs.react.vdom.VdomNode
66
import japgolly.scalajs.react.{Children, CtorType, PropsChildren, Reusability, facade}
7-
import scala.scalajs.js
87

98
object ScalaFn {
109

1110
type Component[P, CT[-p, +u] <: CtorType[p, u]] = JsFn.ComponentWithRoot[P, CT, Unmounted[P], Box[P], CT, JsFn.Unmounted[Box[P]]]
1211
type Unmounted[P] = JsFn.UnmountedWithRoot[P, Mounted, Box[P]]
1312
type Mounted = JsFn.Mounted
1413

15-
private def create[P, C <: Children, CT[-p, +u] <: CtorType[p, u]]
16-
(render: Box[P] with facade.PropsWithChildren => VdomNode)
17-
(implicit s: CtorType.Summoner.Aux[Box[P], C, CT]): Component[P, CT] = {
18-
19-
val jsRender = render.andThen(_.rawNode): js.Function1[Box[P] with facade.PropsWithChildren, facade.React.Node]
20-
val rawComponent = jsRender.asInstanceOf[facade.React.StatelessFunctionalComponent[Box[P]]]
21-
JsFn.force[Box[P], C](rawComponent)(s)
22-
.cmapCtorProps[P](Box(_))
23-
.mapUnmounted(_.mapUnmountedProps(_.unbox))
24-
}
14+
def fromBoxed[P, CT[-p, +u] <: CtorType[p, u]](js: JsFn.Component[Box[P], CT]): Component[P, CT] =
15+
js.cmapCtorProps[P](Box(_)).mapUnmounted(_.mapUnmountedProps(_.unbox))
2516

2617
@inline def withHooks[P] =
2718
HookComponentBuilder.apply[P]
2819

29-
// ===================================================================================================================
30-
3120
def apply[P](render: P => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None]): Component[P, s.CT] =
3221
create[P, Children.None, s.CT](b => render(b.unbox))(s)
3322

@@ -37,8 +26,6 @@ object ScalaFn {
3726
def justChildren(render: PropsChildren => VdomNode): Component[Unit, CtorType.Children] =
3827
create(b => render(PropsChildren(b.children)))
3928

40-
// ===================================================================================================================
41-
4229
def withReuse[P](render: P => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.None], r: Reusability[P]): Component[P, s.CT] =
4330
withHooks[P].renderWithReuse(render)(s, r)
4431

@@ -50,4 +37,13 @@ object ScalaFn {
5037

5138
def withChildrenAndReuse[P, A](reusableInputs: (P, PropsChildren) => A)(render: A => VdomNode)(implicit s: CtorType.Summoner[Box[P], Children.Varargs], r: Reusability[A]): Component[P, s.CT] =
5239
withHooks[P].withPropsChildren.renderWithReuseBy(i => reusableInputs(i.props, i.propsChildren))(render)
40+
41+
// ===================================================================================================================
42+
43+
private def create[P, C <: Children, CT[-p, +u] <: CtorType[p, u]]
44+
(render: Box[P] with facade.PropsWithChildren => VdomNode)
45+
(implicit s: CtorType.Summoner.Aux[Box[P], C, CT]): Component[P, CT] = {
46+
val jsRender: JsFn.RawComponent[Box[P]] = render(_).rawNode
47+
fromBoxed(JsFn.fromJsFn[Box[P], C](jsRender)(s))
48+
}
5349
}

library/coreGeneric/src/main/scala/japgolly/scalajs/react/hooks/Hooks.scala

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,14 @@ object Hooks {
249249

250250
def unsafeCreate[S](initialState: => S): UseState[S] = {
251251
// Boxing is required because React's useState uses reflection to distinguish between {set,mod}State.
252-
val initialStateFn = (() => Box(initialState)): js.Function0[Box[S]]
253-
val originalResult = facade.React.useState[Box[S]](initialStateFn)
254-
val originalSetState = Reusable.byRef(originalResult._2)
255-
UseState(originalResult, originalSetState)
256-
.xmap(_.unbox)(Box.apply)
252+
val initialStateFn = (() => Box(initialState)): js.Function0[Box[S]]
253+
val originalResult = facade.React.useStateFn[Box[S]](initialStateFn)
254+
fromJsBoxed(originalResult)
255+
}
256+
257+
def fromJsBoxed[S](r: facade.React.UseState[Box[S]]): UseState[S] = {
258+
val originalSetState = Reusable.byRef(r._2)
259+
UseState(r, originalSetState).xmap(_.unbox)(Box.apply)
257260
}
258261
}
259262

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package japgolly.scalajs.react.internal
2+
3+
import sourcecode.Line
4+
5+
// TODO: Move into microlibs
6+
object MacroLogger {
7+
8+
def apply(): MacroLogger =
9+
new MacroLogger
10+
11+
def apply(enabled: Boolean): MacroLogger = {
12+
val l = new MacroLogger
13+
l.enabled = enabled
14+
l
15+
}
16+
}
17+
18+
class MacroLogger {
19+
import Console._
20+
21+
private var enabledStack = List.empty[Boolean]
22+
23+
var enabled = false
24+
25+
def pushDisabled(): Unit =
26+
pushEnabled(false)
27+
28+
def pushEnabled(e: Boolean = true): Unit = {
29+
enabledStack ::= enabled
30+
enabled = e
31+
}
32+
33+
def pop(): Unit =
34+
if (enabledStack.nonEmpty) {
35+
enabled = enabledStack.head
36+
enabledStack = enabledStack.tail
37+
}
38+
39+
def apply(): Unit =
40+
if (enabled)
41+
System.out.println()
42+
43+
def apply(a: => Any)(implicit l: Line): Unit =
44+
if (enabled) {
45+
val text = "" + a
46+
// for (line <- text.linesIterator) {
47+
// System.out.printf("%s[%3d]%s %s\n", CYAN, l.value, RESET, line)
48+
// }
49+
System.out.printf("%s[%3d]%s %s\n", CYAN, l.value, RESET, text.replace("\n", "\n "))
50+
}
51+
52+
private def _println(a: => Any): Unit =
53+
if (enabled)
54+
System.out.println(a)
55+
56+
private def width = 200
57+
private def sep = "=" * width
58+
59+
def header(): Unit =
60+
_println(sep + "\n")
61+
62+
def footer(): Unit =
63+
_println("\n" + sep)
64+
65+
def footer(result: => Any): Unit = {
66+
apply("Result", result)
67+
footer()
68+
}
69+
70+
def apply(name: => Any, value: => Any)(implicit l: Line): Unit =
71+
apply(s"$YELLOW$name:$RESET $value")
72+
}

library/facadeMain/src/main/scala/japgolly/scalajs/react/facade/Hooks.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package japgolly.scalajs.react.facade
22

33
import scala.annotation.nowarn
44
import scala.scalajs.js
5+
import scala.scalajs.js.annotation._
56
import scala.scalajs.js.|
67

78
/** See https://reactjs.org/docs/hooks-reference.html
@@ -19,6 +20,14 @@ trait Hooks extends js.Object {
1920

2021
final def useState[S](initial: S | js.Function0[S]): UseState[S] = js.native
2122

23+
/** Using this directly avoid Scala.js adding boilerplate for `|` */
24+
@JSName("useState")
25+
final def useStateFn[S](initial: js.Function0[S]): UseState[S] = js.native
26+
27+
/** Using this directly avoid Scala.js adding boilerplate for `|` */
28+
@JSName("useState")
29+
final def useStateValue[S](initial: S): UseState[S] = js.native
30+
2231
final type UseEffectArg = js.Function0[js.UndefOr[js.Function0[Any]]]
2332
final def useEffect(effect: UseEffectArg,
2433
deps : js.UndefOr[HookDeps] = js.native): Unit = js.native

0 commit comments

Comments
 (0)