├── 01 - Core
├── 01 - Classes.js
├── 02 - Mixins.js
├── 03 - Stateless Functions.js
├── 04 - Modules.js
├── 05 - Elements.js
├── 06 - Refs.js
├── 07 - Imperative Bridge.js
└── 08 - Transferring Props.js
├── 02 - Web Components
└── TBD
├── 03 - Animations
└── TBD
├── 04 - Layout
├── 01 - Primitives.js
├── 02 - Layout Components.js
├── 03 - Usage.js
├── 04 - Inline Styles.md
└── prototype
│ ├── index.html
│ └── index.js
├── 05 - Workers
├── 01 - Serializable Elements.js
└── 02 - Nested Components.js
├── 06 - Embedded Queries
└── TBD
├── 07 - Returning State
├── 01 - Stateful Functions.js
├── 02 - Module Pattern.js
├── 03 - Default Props and Initial State.js
├── 04 - Callback Chaining.js
└── 05 - Async Sequence.js
├── 08 - Types
├── 01 - Elements.js
└── 02 - DOM Elements.js
├── 09 - Reduce State
└── 01 - Declarative Component Module.js
└── README.md
/01 - Core/01 - Classes.js:
--------------------------------------------------------------------------------
1 | // The only reason we need a React depencency here is because the base class
2 | // provides the this.setState method.
3 | import { Component } from "react";
4 |
5 | // We can inline a named export call.
6 | export class Button extends Component {
7 |
8 | // Prop types are defined using built-in language support using a TypeScript
9 | // compatible syntax. Notice the subtle syntax difference between the colon
10 | // and the equal sign.
11 | props : {
12 | width: number
13 | }
14 |
15 | // Default properties can be defined as a static property initializer.
16 | // The value of each property is shallow copied onto the final props object.
17 | static defaultProps = {
18 | width: 100
19 | }
20 |
21 | // Initial state is defined using a property initializer. In this simple
22 | // form it behaves identical to TypeScript. You may refer to this.props
23 | // within this initializer to make initial state a function of props.
24 | state = {
25 | counter: Math.round(this.props.width / 10)
26 | }
27 |
28 | // Instead of relying on auto-binding magic inside React, we use a property
29 | // initializer with an arrow function. This effectively creates a single
30 | // bound method per instance - the same as auto-binding.
31 | handleClick = (event) => {
32 | event.preventDefault();
33 | this.setState({ counter: this.state.counter + 1 });
34 | }
35 |
36 | // Props, state and context are passed into render as a convenience to avoid
37 | // the need for aliasing or referring to `this`.
38 | render(props, state, context) {
39 | return (
40 |
41 | This button has been clicked: {state.counter} times
42 |
43 |
44 | );
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/01 - Core/02 - Mixins.js:
--------------------------------------------------------------------------------
1 | import { mixin } from "react-utils";
2 |
3 | // Chainable mixins
4 |
5 | const A = {
6 |
7 | componentDidMount() {
8 | super(); // This will end up calling an empty function, placed by mixin()
9 | console.log('A');
10 | }
11 |
12 | };
13 |
14 | class B {
15 |
16 | static getQueries() {
17 | super(); // This will end up calling an empty function, placed by mixin()
18 | console.log('B')
19 | }
20 |
21 | componentDidMount() {
22 | console.log('B');
23 | super(); // This will end up calling A.componentDidMount
24 | }
25 |
26 | }
27 |
28 | class C extends mixin(A, B) {
29 |
30 | static getQueries() {
31 | super(); // This calls B.getQueries
32 | console.log('C');
33 | }
34 |
35 | componentDidMount() {
36 | super(); // This calls B.prototype.componentDidMount
37 | console.log('C');
38 | }
39 |
40 | }
41 |
42 | C.getQueries(); // B, C
43 | new C().componentDidMount(); // B, A, C
44 |
45 |
46 | import { Component } from "react";
47 |
48 | // A component that mixes in all of C's functionality
49 |
50 | class Component extends mixin(Component, C) {
51 | render() {
52 | return ;
53 | }
54 | }
55 |
56 | // Solvable but confusing/complex issues:
57 |
58 | export class C extends mixin(A, B) {
59 |
60 | // This state intializer overrides the state initializer in the base class.
61 | // The current React class system merges the two.
62 | state = {
63 | b: true
64 | }
65 |
66 | componentDidMount() {
67 | // You forgot to put a super call here but there's no runtime warning since
68 | // the mixin logic happens before this class is created.
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/01 - Core/03 - Stateless Functions.js:
--------------------------------------------------------------------------------
1 | // A simple component, that isn't stateful, can be provided as a single
2 | // function that accepts props. This provides React with a hint that this
3 | // component can be collapsed and that its state doesn't need to be preserved.
4 | // It also encourages micro-componentization instead of custom helper functions
5 | // outside the system.
6 | export function Button(props : { width: number, onClick: function }) {
7 | return (
8 |
9 | Fancy button
10 |
11 |
12 | );
13 | }
14 |
15 | // When named exports are used, it may be valid to have multiple components
16 | // in the same file. Destructuring can be used to provide convenience aliasing
17 | // and defaults to props.
18 | export function Checkbox({ checked = true, width }) {
19 | return (
20 |
21 | Fancy checkbox
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/01 - Core/04 - Modules.js:
--------------------------------------------------------------------------------
1 | // Through out the other examples named exports are used.
2 |
3 | export class Button extends Component {
4 |
5 | }
6 | // This means that components are imported using the following syntax:
7 |
8 | import { Button } from "Button";
9 |
10 | // and can be aliases like this:
11 |
12 | import { Button as MySpecialButton } from "Button";
13 |
14 | // Another alternative is to export defaults. It's unclear which of these two
15 | // paradigms will win for Component modules. If default exports win popular
16 | // mindshare, we might encourage a different syntax for exports:
17 |
18 | export default class Button extends Component {
19 | // ...
20 | }
21 |
22 | // To import a default export you would simply leave out the curly braces.
23 |
24 | import MySpecialButton from "Button";
25 |
--------------------------------------------------------------------------------
/01 - Core/05 - Elements.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ELEMENT OBJECT LITERAL
3 | *
4 | * A React component module will no longer export a helper function to create
5 | * virtual elements. Instead it's the responsibility of the consumer to
6 | * efficiently create the virtual element.
7 | *
8 | * Languages that compile to JS can choose to implement the element signature
9 | * in whatever way is idiomatic for that language:
10 | */
11 |
12 | {
13 | type: Button,
14 | props: {
15 | foo: bar,
16 | children: [
17 | { type: 'span', props: { children: a } },
18 | { type: 'span', props: { children: b } }
19 | ]
20 | },
21 |
22 | // optional
23 | key: 'mybutton',
24 | ref: myButtonRef
25 | }
26 |
27 | /**
28 | * JSX
29 | */
30 |
31 |
35 |
36 | /**
37 | * PLAIN JS
38 | * __DEV__ MODE
39 | *
40 | * This helper function ensures that your static children don't get the key
41 | * warning. It creates an element for you with the current owner/context.
42 | * The props object is cloned and key/ref moved onto the element.
43 | */
44 |
45 | var _Button = React.createFactory(Button);
46 | var _span = React.createFactory('span');
47 |
48 | _Button({ foo: bar, key: 'mybutton', ref: myButtonRef },
49 | _span(null, a),
50 | _span(null, b)
51 | )
52 |
53 | /**
54 | * If you use JSX, or can statically analyze that the Factory calls belongs to
55 | * React, then you can chose to opt-in to one of the optimizations modes.
56 | */
57 |
58 | /**
59 | * INLINE
60 | * PRODUCTION MODE
61 | *
62 | * Inline mode simply creates the element objects inline in the code, with
63 | * a lookup for current owner/context as well as resolving default props.
64 | * If defaults aren't known statically, then we create a factory that can help
65 | * assign defaults quickly on the newly created object.
66 | */
67 |
68 | var Button_assignDefaults = React.createDefaultsFactory(Button);
69 |
70 | {
71 | type: Button,
72 | props: Button_assignDefaults({
73 | foo: bar,
74 | children: [
75 | { type: 'span', props: { children: a }, key: null, ref: null, _owner: React._currentOwner, _context: React._currentContext },
76 | { type: 'span', props: { children: b }, key: null, ref: null, _owner: React._currentOwner, _context: React._currentContext }
77 | ]
78 | }),
79 |
80 | key: 'mybutton',
81 | ref: myButtonRef,
82 |
83 | _owner: React._currentOwner,
84 | _context: React._currentContext
85 | }
86 |
87 | /**
88 | * POOLED MODE
89 | *
90 | * Pooled mode doesn't allocate any new objects. Instead it gets mutable objects
91 | * from a pool and reuses them. It overrides the props on the pooled object.
92 | */
93 |
94 | var P1 = React.createElementPool({
95 | type: Button,
96 | key: 'mybutton',
97 | props: {
98 | foo: null,
99 | children: null
100 | }
101 | });
102 | var P2 = React.createElementPool({
103 | type: 'span',
104 | props: {
105 | children: null
106 | }
107 | });
108 | var A2 = React.createArrayPool(2); // Number of items in the array
109 | var t1, t1p, t1c, t2;
110 |
111 | (
112 | t1 = P1(),
113 | t1.ref = myButtonRef,
114 | t1p = t1.props,
115 | t1p.foo = bar,
116 | t1p.children = A2(),
117 | t1c = t1p.children,
118 | t1c[0] = (t2 = P2(), t2.props.children = a, t2),
119 | t1c[1] = (t2 = P2(), t2.props.children = b, t2),
120 | t1
121 | )
122 |
123 | /**
124 | * NATIVE COMPONENTS
125 | *
126 | * Note that DOM nodes are no longer functions on React.DOM, instead they're
127 | * just strings. JSX will convert any lower-case tag name, or if it has a dash,
128 | * into a string value instead of a scope reference. This makes them compatible
129 | * with custom tags (Web Components).
130 | */
131 |
132 | /**
133 | * TEMPLATE STRINGS
134 | *
135 | * You could create an add-on sugar which uses ES6 template strings to create
136 | * elements. It becomes more palatable if all your components are registered
137 | * through strings.
138 | */
139 |
140 | X`
141 |
142 | ${a}
143 | ${b}
144 |
145 | `
146 |
--------------------------------------------------------------------------------
/01 - Core/06 - Refs.js:
--------------------------------------------------------------------------------
1 | // This is a refs proposal that uses callbacks instead of strings. The callback
2 | // can be easily statically typed and solves most use cases.
3 |
4 | // When a ref is attached, it gets passed the instance as the first argument.
5 | // When a ref is detached, the callback is invoked with null as the argument.
6 |
7 | // refs on DOM nodes gives the DOM node handle directly, not an intermediate
8 | // form.
9 |
10 | class Foo {
11 |
12 | myDivRef : ?HTMLDivElement;
13 |
14 | handleTick() {
15 | this.setState({ width: this.myDivRef.offsetWidth });
16 | }
17 |
18 | render() {
19 | return (
20 |
21 |
this.myDivRef = node} />
22 | this.myDivRef} />
23 |
24 | );
25 | }
26 |
27 | }
28 |
29 | // It's possible to distinguish whether a ref as ever been mounted or if it has
30 | // been mounted during this particular reconciliation phase.
31 |
32 | class Component {
33 |
34 | buttonWasEverMounted : boolean;
35 | buttonWasMountedThisPass : boolean;
36 | button : ?Button;
37 |
38 | mountButtonRef = button => {
39 | if (button) {
40 | this.buttonWasEverMounted = true;
41 | this.buttonWasMountedThisPass = true;
42 | }
43 | this.button = button;
44 | }
45 |
46 | componentWillMount() {
47 | this.componentWillUpdate();
48 | }
49 |
50 | componentDidMount() {
51 | this.componentDidUpdate();
52 | }
53 |
54 | componentWillUpdate() {
55 | this.buttonWasMountedThisPass = false;
56 | }
57 |
58 | componentDidUpdate() {
59 | if (this.buttonWasEverMounted) {
60 | console.log('button was mounted at least once');
61 | }
62 | if (this.buttonWasMountedThisPass) {
63 | console.log('button was mounted during this render pass');
64 | }
65 | }
66 |
67 | render() {
68 | return ;
69 | }
70 |
71 | }
72 |
73 | // In a future world where every callback is also implemented as an Observer,
74 | // we can pass a subject to the ref to build Observable compositions on top
75 | // of a ref.
76 |
77 | class Foo {
78 |
79 | myTick = new Rx.Subject();
80 | myDiv = new Rx.Subject();
81 |
82 | observe() {
83 | var widths = this.myDiv.map(myDivRef => myDivRef.offsetWidth);
84 | return {
85 | width: this.myTick.combineLatest(widths, (e, width) => width)
86 | };
87 | }
88 |
89 | render() {
90 | return (
91 |
92 |
93 |
94 |
95 | );
96 | }
97 |
98 | }
99 |
100 | // Getting your "own" DOM node is still possible with a call to traverse the
101 | // composites until you get to the DOM node.
102 |
103 | class Foo {
104 |
105 | handleTick() {
106 | var node = React.findDOMNode(this);
107 | this.setState({ width: node.offsetWidth });
108 | }
109 |
110 | render() {
111 | return (
112 |
113 |
114 |
115 | );
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/01 - Core/07 - Imperative Bridge.js:
--------------------------------------------------------------------------------
1 | import { Button } from "button";
2 | import { Renderer } from "react-dom";
3 |
4 | // Currently setProps() provides a convenient hook on rendered components
5 | // that are top level. This magically turns props into state instead of being
6 | // modeled outside the component. This undermines the integrity of props
7 | // in a component tree. Instead, we want to add a wrapper that saves these
8 | // values as its own internal state. You can imperatively update the
9 | // props using setters on the renderer instances. These gets flushed down to
10 | // the underlying component class.
11 |
12 | var button = new Renderer();
13 |
14 | // Insertion is done by exposing a rendered top-level element which can be
15 | // inserted anywhere in the DOM.
16 |
17 | document.body.appendChild(button.toElement());
18 |
19 | // A setter is used to update the component
20 | button.foo = "baz";
21 |
--------------------------------------------------------------------------------
/01 - Core/08 - Transferring Props.js:
--------------------------------------------------------------------------------
1 | import { HTMLPropTypes, joinClasses } from "react-dom";
2 |
3 | // Draft
4 |
5 | class FancyButton {
6 |
7 | static defaultProps = {
8 | color: 'blue'
9 | };
10 |
11 | props: {
12 | color: string,
13 | width: number,
14 | height: number,
15 | ...HTMLPropTypes
16 | },
17 |
18 | // This uses rest and spread operators
19 | // https://gist.github.com/sebmarkbage/aa849c7973cb4452c547
20 |
21 | render({ color, className, style, width, height, ...other }) {
22 | // The rest operator picks off the remaining props that you're not using.
23 | // The linter analyses this method and notices the JSX spread attribute.
24 | // Therefore it warns you not to use this.props.propertyName and instead
25 | // ask you to use destructuring with a rest property.
26 |
27 | var button =
28 | ;
38 |
39 | /**
40 | * button.props === {
41 | * className: 'test FancyButton',
42 | * disabled: true,
43 | * style: {
44 | * backgroundColor: 'blue',
45 | * padding: 10,
46 | * width: 90,
47 | * height: 40
48 | * }
49 | * }
50 | */
51 |
52 | return button;
53 | }
54 |
55 | }
56 |
57 | class App {
58 |
59 | render() {
60 | var fancyButton =
61 | ;
67 |
68 | /**
69 | * fancyButton.props === {
70 | * color: 'blue',
71 | * className: 'test',
72 | * disabled: true,
73 | * width: 100,
74 | * height: 50
75 | * }
76 | */
77 |
78 | return
{fancyButton}
;
79 | }
80 |
81 | }
--------------------------------------------------------------------------------
/02 - Web Components/TBD:
--------------------------------------------------------------------------------
1 | This section is not yet complete.
2 |
3 | Please come back later or send a pull request with your own ideas.
--------------------------------------------------------------------------------
/03 - Animations/TBD:
--------------------------------------------------------------------------------
1 | This section is not yet complete.
2 |
3 | Please come back later or send a pull request with your own ideas.
--------------------------------------------------------------------------------
/04 - Layout/01 - Primitives.js:
--------------------------------------------------------------------------------
1 | // Any layout system must render to some primitives. For the purpose of
2 | // simplified examples, we introduce three primitive concepts that layout
3 | // components can resolve to. For familiarity we use absolutely positioned divs.
4 | // However, it could resolve to SVG, ART, or any other kind of graphics
5 | // primitives.
6 |
7 | // Arbitrary wrapper (since we don't currently support fragments)
8 | class Fragment {
9 | render() {
10 | return
{this.props.children}
;
11 | }
12 | }
13 |
14 | // Position its content with an offset of `x` and `y`
15 | class Positioner {
16 | render() {
17 | var { x, y, children } = this.props;
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | }
24 | }
25 |
26 | // Paint a box with size `width` and `height`
27 | class Box {
28 | render() {
29 | var { width, height, children } = this.props;
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/04 - Layout/02 - Layout Components.js:
--------------------------------------------------------------------------------
1 | // Layout System Example Components
2 |
3 | // The Vertical List can layout items in vertically based on their dynamic
4 | // height. It also provides its own total height so that it can be used as
5 | // an item within itself.
6 |
7 | class VerticalList {
8 | getChildContext() {
9 | return {
10 | layoutWidth: this.props.width || this.context.layoutWidth
11 | };
12 | }
13 |
14 | // NEW FEATURE: Pre-render a subtree before continuing with the normal render.
15 | preRender() {
16 | // Children that needs to be prerendered. If some children needs to be
17 | // prerendered, but not others (e.g. for flexbox layouts), then they're
18 | // filtered, here and the rest is rendered inside render.
19 | return this.props.children;
20 | }
21 |
22 | render() {
23 | var children = this.prerendered;
24 | var positionedChildren = [];
25 | var y = 0;
26 | for (var i = 0; i < children.length; i++) {
27 | // This child is an opaque black box whose props cannot be inspected,
28 | // nor cloned. It can only be rendered once. Rendering it twice results
29 | // in a conflict, at least until we support painting the same stateful
30 | // component in many different places (e.g. SVG's )
31 | var child = children[i];
32 | positionedChildren.push(
33 | {child}
34 | );
35 | // We can use it to inspect the child's reverse context though.
36 | y += child.result.layoutHeight;
37 | }
38 | return {positionedChildren};
39 | }
40 |
41 | // NEW FEATURE: When a component itself is is prerendered, it can bubble a
42 | // result back up the tree.
43 | getResult() {
44 | // We already had this height calculated in render, but to allow for render
45 | // to be deferred, and to preserve the standard render() API, we have to
46 | // recalculate it here. This is helpful in the cases where a parent decides
47 | // not to render this child. That way we can avoid calling render.
48 | var totalHeight = 0;
49 | var children = this.prerendered;
50 | for (var i = 0; i < children.length; i++) {
51 | totalHeight += children[i].result.layoutHeight;
52 | }
53 | return {
54 | layoutHeight: totalHeight
55 | };
56 | }
57 | }
58 |
59 | class VerticalListItem {
60 | render() {
61 | return (
62 |
63 | {this.props.children}
64 |
65 | );
66 | }
67 | getResult() {
68 | return {
69 | layoutHeight: this.props.height
70 | };
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/04 - Layout/03 - Usage.js:
--------------------------------------------------------------------------------
1 | // Usage of VerticalList and VerticalListItem examples
2 |
3 | class Newsfeed {
4 | render() {
5 | return (
6 |
7 | {this.props.stories.map(story => )}
8 |
9 | );
10 | }
11 | }
12 |
13 | class NewsStory {
14 | render() {
15 | // Variable height list item (A list can also act as an item)
16 | return (
17 |
18 | {this.props.story.title}
19 | {this.props.story.content}
20 |
21 | );
22 | }
23 | }
24 |
25 | class Header {
26 | render() {
27 | return (
28 | // Small constant height item
29 |
30 | {this.props.children}
31 |
32 | );
33 | }
34 | }
35 |
36 | class Content {
37 | render() {
38 | // Slightly larger constant height item
39 | return (
40 |
41 | {this.props.children}
42 |
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/04 - Layout/04 - Inline Styles.md:
--------------------------------------------------------------------------------
1 | This proposal shares most of the styling and layout attributes as CSS. But we've decided to use JavaScript instead of CSS to define styles.
2 |
3 | #### No Selectors
4 |
5 | The main difference is that this proposal doesn't support selectors. This feature has proven to make writing large scale applications very hard.
6 |
7 | #### Easier to bundle
8 |
9 | Because they are not using the same technology, the way we load and bundle JS and CSS is different. We occasionally end up with cases where the JS loads faster than CSS and you can see really broken un-styled experience for some time.
10 |
11 | #### Ability to share constants
12 |
13 | It makes it extremely hard to share constants. You've got to rely on comments saying to keep to sets of values in sync, which usually doesn't happen and causes issues. Using only Javascript solves this problem.
14 |
15 | #### Private by default
16 |
17 | It's near impossible to remove rules from CSS as it's very hard to guarantee that your rule is no longer being used or won't break unrelated things. By having rules private to the file by default and specified to nodes, this problem will be a thing of the past.
18 |
19 | #### One less file to create
20 |
21 | It's a pain for the developer to have to create 2 files for every single component. In this proposal, we collocate the styles at the bottom of the file.
22 |
23 |
24 | ## Declaring Styles
25 |
26 | The way to declare styles is the following
27 |
28 | ```javascript
29 | var styles = StyleSheet.create({
30 | base: {
31 | width: 38,
32 | height: 38
33 | },
34 | background: {
35 | backgroundColor: '#222222'
36 | },
37 | active: {
38 | borderWidth: 2,
39 | borderColor: '#00ff00'
40 | }
41 | });
42 | ```
43 |
44 | ## Using Styles
45 |
46 | All the core components accept a style attribute
47 |
48 | ```javascript
49 |
50 |
51 | ```
52 |
53 | which also accepts an array of styles
54 |
55 | ```javascript
56 |
57 | ```
58 |
59 | A common pattern is to conditionally add a style based on some condition. The style attribute will ignore `false`, `undefined` and `null` entries.
60 |
61 | ```javascript
62 |
63 | ```
64 |
65 | Finally, if you really have to, you can also use inline styles, but they are highly discouraged. Put them last in the array definition.
66 |
67 | ```javascript
68 |
74 | ```
75 |
76 | ## Pass Styles Around
77 |
78 | In order to let a call site customize the style of your component children, you can pass styles around. Use `StylePropType` in order to make sure only styles are being passed.
79 |
80 | ```javascript
81 | var List = React.createClass({
82 | propTypes: {
83 | elementStyle: StylePropType
84 | },
85 | render: function() {
86 | return
87 | {elements.map((element) =>
88 |
89 | )}
90 |
;
91 | }
92 | });
93 |
94 |
95 | ```
96 |
--------------------------------------------------------------------------------
/04 - Layout/prototype/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React Layout Experiment
5 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/04 - Layout/prototype/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This is an experimental prototype of an integrated React layout algorithm
3 | * defined in terms of Effects implemented as ECMAScript generators.
4 | *
5 | * There are two types of "Effects":
6 | *
7 | * - Component (function): When this Effect is yielded, the component is
8 | * rendered as deep as necessary until an Object Effect is yielded. The value
9 | * of the yielded effect is an object passed to the continuation.
10 | *
11 | * - Object (any): When an Object is raised, the rendering is interrupted.
12 | * The yielded object is passed into the continuation of the Component effect,
13 | * along with a continuation to keep rendering the rest of the component. When
14 | * the continuation is rendered, the object passed to the continuation is
15 | * passed back to the raised Component Effect.
16 | *
17 | * Combining these two effects seems to be sufficient to implement the layout
18 | * algorithms that we want to support.
19 | *
20 | * In this particular example algorithm, the width/height of a layout component
21 | * is the yielded Object. The continuation value is the top/left offset of the
22 | * layout component after it has been placed.
23 | *
24 | * TODO: Expand the Effects to other types and let them bubble (e.g. context).
25 | */
26 |
27 | "use strict";
28 |
29 | /* React */
30 |
31 | function $(fn, ...args) {
32 | return fn.bind(null, ...args);
33 | }
34 |
35 | /* App */
36 |
37 | function Quad(props) {
38 | const node = document.createElement('span');
39 | node.style.background = props.background;
40 | node.style.width = props.width + 'px';
41 | node.style.height = props.height + 'px';
42 | node.style.top = props.top + 'px';
43 | node.style.left = props.left + 'px';
44 | node.textContent = props.text || '';
45 | return node;
46 | }
47 |
48 | function* Box(props) {
49 | const position = yield {
50 | width: props.width,
51 | height: props.height
52 | };
53 | return $(Quad, {
54 | width: props.width,
55 | height: props.height,
56 | background: props.background,
57 | top: position.top,
58 | left: position.left,
59 | text: props.text
60 | });
61 | }
62 |
63 | function* FlexibleBox({ width = 0, height = 0, background }) {
64 | const rect = yield { width, height };
65 | return $(Quad, {
66 | width: rect.width,
67 | height: rect.height,
68 | background: background,
69 | top: rect.top,
70 | left: rect.left
71 | });
72 | }
73 |
74 | function Intl(props) {
75 | // TODO: Provide context
76 | return props.child;
77 | }
78 |
79 | function IntlNumber(number) {
80 | // TODO: Read context
81 | return $(Text, '' + number);
82 | }
83 |
84 | function* Horizontal(...children) {
85 | const continuations = [];
86 | let x = 0;
87 | let y = 0;
88 | for (let child of children) {
89 | const { value: size, continuation } = yield child;
90 | continuations.push({
91 | continuation: continuation,
92 | left: x
93 | });
94 | x += size.width;
95 | y = size.height > y ? size.height : y;
96 | }
97 | const offset = yield {
98 | width: x,
99 | height: y
100 | };
101 | return continuations.map(child => $(child.continuation, {
102 | top: offset.top,
103 | left: offset.left + child.left
104 | }));
105 | }
106 |
107 | function* Vertical(...children) {
108 | const continuations = [];
109 | let x = 0;
110 | let y = 0;
111 | for (let child of children) {
112 | const { value: size, continuation } = yield child;
113 | continuations.push({
114 | continuation: continuation,
115 | top: y
116 | });
117 | x = size.width > x ? size.width : x;
118 | y += size.height;
119 | }
120 | const offset = yield {
121 | width: x,
122 | height: y
123 | };
124 | return continuations.map(child => $(child.continuation, {
125 | top: offset.top + child.top,
126 | left: offset.left
127 | }));
128 | }
129 |
130 | function* Div(props, ...children) {
131 | // A "div" or "View" is something that people keep asking for. What they mean
132 | // is a quad, that can be styled with background/border, behind some content.
133 | // However, the interesting part is that this quad should be sized to fit all
134 | // the nested content.
135 | const { value: size, continuation } = yield $(Vertical, ...children);
136 | const offset = yield size;
137 | return [
138 | $(Quad, {
139 | top: offset.top,
140 | left: offset.left,
141 | width: size.width,
142 | height: size.height,
143 | background: props.background
144 | }),
145 | $(continuation, offset)
146 | ];
147 | }
148 |
149 | function Text(content) {
150 | return $(Box, {
151 | width: textMeasuringContext.measureText('' + content).width,
152 | height: textLineHeight,
153 | text: content
154 | });
155 | }
156 |
157 | function Awesomeness() {
158 | return $(Horizontal,
159 | $(Text, 'Awesomeness Index: '),
160 | $(IntlNumber, 123.45)
161 | );
162 | }
163 |
164 | function* Body(child) {
165 | const { continuation } = yield child;
166 | return $(continuation, {
167 | top: 10,
168 | left: 10
169 | });
170 | }
171 |
172 | /**
173 | * Simplified Flexbox
174 | */
175 | function* VerticalFlex(props, ...flexItems) {
176 | const children = [];
177 | let maxWidth = 0;
178 | let flexAccumulation = 0;
179 | let availableHeight = props.height;
180 | for (let { item, flex = 0 } of flexItems) {
181 | const { value: size, continuation } = yield item;
182 | maxWidth = size.width > maxWidth ? size.width : maxWidth;
183 | flexAccumulation += flex;
184 | if (!flex) {
185 | availableHeight -= size.height;
186 | }
187 | children.push({ flex, continuation, height: size.height });
188 | }
189 | const offset = yield { width: maxWidth, height: props.height };
190 | let accumulatedTop = 0;
191 | const positionedChildren = [];
192 | for (let { flex, continuation, height } of children) {
193 | if (flex) {
194 | height = availableHeight * (flex / flexAccumulation);
195 | }
196 | positionedChildren.push(
197 | $(continuation, {
198 | top: offset.top + accumulatedTop,
199 | left: offset.left,
200 | width: maxWidth,
201 | height: height
202 | })
203 | );
204 | accumulatedTop += height;
205 | }
206 | return positionedChildren;
207 | }
208 |
209 | function Reflexity(child1, child2) {
210 | return $(VerticalFlex, { height: 300 },
211 | { item: child1 },
212 | { item: $(Div, { background: '#f00' }, $(Text, 'Some stuff '), $(Text, 'in between!')) },
213 | { item: $(FlexibleBox, { background: '#00f' }), flex: 1 },
214 | { item: child2 },
215 | { item: $(FlexibleBox, { background: '#0f0' }), flex: 0.5 }
216 | );
217 | }
218 |
219 | function App() {
220 | return $(Body,
221 | $(Div, { background: '#eee' },
222 | $(Reflexity,
223 | $(Horizontal, $(Text, 'Hello '), $(Text, 'World!')),
224 | $(Intl, {
225 | locale: 'en-US',
226 | child: $(Awesomeness)
227 | })
228 | )
229 | )
230 | );
231 | }
232 |
233 | /* React DOM */
234 |
235 | function resolveChild(child) {
236 | const element = child();
237 | if (typeof element === 'function') {
238 | return resolveChild(element, container);
239 | } else if (Array.isArray(element)) {
240 | throw new Error('Arrays not valid here. Cannot resolve multiple results here.');
241 | } else if (element[Symbol.iterator]) {
242 | const iterator = element[Symbol.iterator]();
243 | let rec = iterator.next();
244 | while (!rec.done) {
245 | if (typeof rec.value === 'function') {
246 | const resolvedResult = resolveChild(rec.value);
247 | rec = iterator.next(resolvedResult);
248 | } else {
249 | break;
250 | }
251 | }
252 | return {
253 | value: rec.value,
254 | continuation: function(props) {
255 | // TODO: If this is a child, it needs to be resolved.
256 | return iterator.next(props).value;
257 | }
258 | };
259 | } else {
260 | throw new Error('Unhandled branch');
261 | }
262 | }
263 |
264 | function render(element, container) {
265 | if (typeof element === 'function') {
266 | render(element(), container);
267 | } else if (Array.isArray(element)) {
268 | element.forEach(child => render(child, container));
269 | } else if (element[Symbol.iterator]) {
270 | const iterator = element[Symbol.iterator]();
271 | let rec = iterator.next();
272 | while (!rec.done) {
273 | if (typeof rec.value === 'function') {
274 | const resolvedResult = resolveChild(rec.value);
275 | rec = iterator.next(resolvedResult);
276 | } else {
277 | rec = iterator.next();
278 | }
279 | }
280 | const child = rec.value;
281 | render(child, container);
282 | } else if (element.nodeType) {
283 | container.appendChild(element);
284 | }
285 | }
286 |
287 | const textMeasuringContext = document.createElement('canvas').getContext('2d');
288 | const rootStyle = window.getComputedStyle(
289 | document.getElementById('container')
290 | );
291 | textMeasuringContext.font = rootStyle.font;
292 | const textLineHeight = parseInt(rootStyle.lineHeight);
293 |
294 | /* Initialization */
295 |
296 | render($(App), document.getElementById('container'));
297 |
--------------------------------------------------------------------------------
/05 - Workers/01 - Serializable Elements.js:
--------------------------------------------------------------------------------
1 | // It could be possible to run React Components in a separate worker that then
2 | // communicates with a React component in another worker.
3 |
4 | import SameThreadComponent from "Foo"; // React class
5 | const CrossThreadComponent = "Bar"; // Just a string module ID
6 | const AnotherCrossThreadComponent = "Baz"; // Another string module ID
7 |
8 | // The props of a cross-thread element must be serializable across the worker
9 | // boundary. Functions are treated as asynchronous callbacks with serializable
10 | // arguments.
11 |
12 | class App {
13 | render() {
14 | return (
15 |
16 | log(data)}>
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | // The ReactComponent is instantiated on the other side of the worker and
25 | // rendered top-down using the normal React reconciliation. State may live on
26 | // the other side of the worker boundary.
27 |
28 | // Normally, it is illegal to nest same-thread components inside a cross-thread
29 | // component:
30 |
31 | class CounterExample {
32 | render() {
33 | return (
34 | log(data)}>
35 | {/* Illegal */}{/* Don't do this */}
36 |
37 | );
38 | }
39 | }
40 |
41 | // This doesn't work because a React parent component is responsible for
42 | // resolving its children. The CrossThreadComponent would have to block
43 | // rendering to come back to this thread to ask the SameThreadComponent to
44 | // resolve itself. This defeats the purpose of asynchronously resolving these.
45 | // However, luckily we have a solution around that using prerendering which
46 | // we'll cover in the next chapter.
47 |
--------------------------------------------------------------------------------
/05 - Workers/02 - Nested Components.js:
--------------------------------------------------------------------------------
1 | // In the previous section we showed that it's not possible to pass same-thread
2 | // components inside cross-thread components. How can we create composability
3 | // and not just render the entire tree on the "other" thread?
4 |
5 | import SameThreadComponent from "Foo"; // React Class
6 | const CrossThreadComponent = "Bar"; // Just a String identfiier
7 | const AnotherCrossThreadComponent = "Baz";
8 |
9 | // By prerendering the same-thread components we can pass their opaque result
10 | // into a cross-thread element. The component on the other side cannot reason
11 | // about the content of the cross-thread element but it can render it anywhere
12 | // in its own tree. That means that the pre-rendered child can directly
13 | // manipulate its content without first asking the parent on the other thread.
14 |
15 | // This implementation detail can be hidden in a wrapper.
16 |
17 | class CrossThreadComponentWrapper {
18 | preRender() {
19 | return this.props.children;
20 | }
21 | render() {
22 | return (
23 |
24 | {this.prerendered}
25 |
26 | );
27 | }
28 | }
29 |
30 | // Now it's safe to use same-thread components as children of the cross-thread
31 | // component.
32 |
33 | class App {
34 | render() {
35 | return (
36 | log(data)}>
37 |
38 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/06 - Embedded Queries/TBD:
--------------------------------------------------------------------------------
1 | This section is not yet complete.
2 |
3 | Please come back later or send a pull request with your own ideas.
--------------------------------------------------------------------------------
/07 - Returning State/01 - Stateful Functions.js:
--------------------------------------------------------------------------------
1 | interface P {
2 | width: number;
3 | onClick: function;
4 | }
5 |
6 | // Callbacks take props and state as their first arguments. To update state,
7 | // they simply return the new version of the state.
8 | function handleClick(props, state, event) {
9 | if (event.button === 1) {
10 | // Middle click resets state to zero
11 | return { count: 0 };
12 | }
13 | // Callbacks to external components may be fired
14 | if (props.onClick) {
15 | props.onClick();
16 | }
17 | return { count: state ? state.count + 1 : 0 };
18 | }
19 |
20 | // The component itself is a single function that accepts props and state as
21 | // arguments and returns a new element. Effectively the render method.
22 | // If callbacks are suppose to do any updates to state (or cause side-effects),
23 | // they will need to be wrapped before passed down. Conveniently, the "this"
24 | // argument will be a function that can be used to wrap callbacks.
25 | // This wrapper is essentially equivalent to:
26 | // function wrapper(fn) {
27 | // return function(...args) {
28 | // component.setState(
29 | // fn.call(wrapper, component.props, component.state, ...args)
30 | // );
31 | // };
32 | // }
33 | export function Counter(props : P, state) {
34 | return (
35 |