Skip to content

Commit 39caf50

Browse files
committed
Add FunctionalComponent
1 parent f957aad commit 39caf50

File tree

5 files changed

+100
-1
lines changed

5 files changed

+100
-1
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package japgolly.scalajs.react
2+
3+
import scala.scalajs.js
4+
5+
/**
6+
* A component that takes `props` as an argument and returns the element to render.
7+
*
8+
* These components behave just like a React class with only a `render` method defined.
9+
* Since no component instance is created for a functional component, any ref added to one will evaluate to `null`.
10+
* Functional components do not have lifecycle methods.
11+
*
12+
* "In the future, we’ll also be able to make performance optimizations specific to these components by avoiding
13+
* unnecessary checks and memory allocations."
14+
* - https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html
15+
*
16+
* @since React 0.14
17+
*/
18+
sealed trait FunctionalComponent[-P] extends js.Any
19+
// type FunctionalComponent[-P] = js.Function1[P, ReactElement]
20+
// ↑ Doesn't work for Scala for two reasons:
21+
// 1) Application returns a `ReactElement` immediately which makes it just a normal function.
22+
// 2) React expects P to be a JavaScript object which Scala values are not.
23+
24+
object FunctionalComponent {
25+
26+
/**
27+
* Creates a [[FunctionalComponent]].
28+
*
29+
* If `props` is `Unit`, there is also [[ReactComponentB.static]] which also sets `shouldComponentUpdate` to `false`
30+
* which should be more efficient.
31+
*/
32+
def apply[P](render: P => ReactElement): FunctionalComponent[P] = {
33+
val f = (o: WrapObj[P]) => render(o.v)
34+
val jf = f: js.Function1[WrapObj[P], ReactElement]
35+
jf.asInstanceOf[FunctionalComponent[P]]
36+
}
37+
38+
@inline implicit class Ops[P](private val f: FunctionalComponent[P]) extends AnyVal {
39+
def apply(props: P) =
40+
// This is what JSX/Babel does:
41+
React.createElement(f, WrapObj(props))
42+
}
43+
44+
// ===================================================================================================================
45+
46+
/**
47+
* A [[FunctionalComponent]] that accepts [[PropsChildren]] in addition to `props`.
48+
*/
49+
sealed trait WithChildren[-P] extends js.Any
50+
51+
def withChildren[P](render: (P, PropsChildren) => ReactElement): WithChildren[P] = {
52+
val f = (o: WrapObj[P]) => render(o.v, o.asInstanceOf[js.Dynamic].children.asInstanceOf[PropsChildren])
53+
val jf = f: js.Function1[WrapObj[P], ReactElement]
54+
jf.asInstanceOf[WithChildren[P]]
55+
}
56+
57+
@inline implicit class WithChildrenOps[P](private val f: FunctionalComponent.WithChildren[P]) extends AnyVal {
58+
def apply(props: P, children: ReactNode*) =
59+
// This is what JSX/Babel does:
60+
React.createElement(f, WrapObj(props), children: _*)
61+
}
62+
}

core/src/main/scala/japgolly/scalajs/react/React.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ trait React extends Object {
3737
*/
3838
def createElement(tag: String, props: Object, children: ReactNode*): ReactDOMElement = js.native
3939

40+
def createElement(fc: FunctionalComponent[Nothing], props: Object, children: ReactNode*): ReactDOMElement = js.native
41+
def createElement(fc: FunctionalComponent.WithChildren[Nothing], props: Object, children: ReactNode*): ReactDOMElement = js.native
42+
4043
/** Verifies the object is a ReactElement. */
4144
def isValidElement(o: JAny): Boolean = js.native
4245

core/src/main/scala/japgolly/scalajs/react/package.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package japgolly.scalajs
22

33
import org.scalajs.dom, dom.html
44
import scala.scalajs.js
5-
import js.{Dynamic, UndefOr, Object, Any => JAny, Function => JFn}
5+
import js.{Dynamic, Object, Any => JAny, Function => JFn}
66

77
package object react extends ReactEventAliases {
88

doc/CHANGELOG-0.10.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@
249249
sense in React Native, etc. However in Scala we have the ability to *conditionally* add the `getDOMNode()` method
250250
onto components. This means we can keep the convenient `getDOMNode()` and in future mirror the conditions that
251251
React applies (eg. `getDOMNode()` works with the `react-dom` module only).
252+
* Add `FunctionalComponent`, a new type of component which is a thinly-wrapped, pure `P => ReactElement`.
252253

253254
* Smaller stuff:
254255

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package japgolly.scalajs.react
2+
3+
import utest._
4+
import vdom.prefix_<^._
5+
import TestUtil._
6+
7+
object FunctionalComponentTest extends TestSuite {
8+
9+
val IntComp = FunctionalComponent[Int](i => <.code(s"$i² = ${i*i}"))
10+
val IntExample = ReactComponentB[Unit]("")
11+
.render(_ => <.div(IntComp(7)))
12+
.buildU
13+
14+
case class AddCmd(x: Int, y: Int)
15+
val AddComp = FunctionalComponent[AddCmd]{a =>
16+
import a._
17+
<.code(s"$x + $y = ${x+y}")
18+
}
19+
val AddExample = ReactComponentB[Unit]("")
20+
.render(_ => <.div(AddComp(AddCmd(11, 8))))
21+
.buildU
22+
23+
val KidsComp = FunctionalComponent.withChildren[Int]((i,pc) => <.div(s"i=$i", pc))
24+
val KidsExample = ReactComponentB[Unit]("")
25+
.render(_ => <.div(KidsComp(3, <.i("good"))))
26+
.buildU
27+
28+
override def tests = TestSuite {
29+
'int { IntExample() shouldRender "<div><code>7² = 49</code></div>" }
30+
'caseClass { AddExample() shouldRender "<div><code>11 + 8 = 19</code></div>" }
31+
'withChildren { KidsExample() shouldRender "<div><div>i=3<i>good</i></div></div>" }
32+
}
33+
}

0 commit comments

Comments
 (0)