Skip to content

Commit

Permalink
Merge pull request #29 from massiveinteractive/feature/react-literals
Browse files Browse the repository at this point in the history
Runtime performance improvements
  • Loading branch information
elsassph committed May 27, 2016
2 parents e00a2dd + 5c397e5 commit 36be59c
Show file tree
Hide file tree
Showing 9 changed files with 405 additions and 121 deletions.
63 changes: 41 additions & 22 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ compile time macros to offer a strongly typed language to work with the increasi

haxelib install react

### What's included / not included

This library covers React core and ReactDOM.
It does NOT cover: ReactAddOns, react-router or React Native.

## API

Most of the regular React API is integrated (non-JSX example):
Expand All @@ -31,30 +36,8 @@ class App extends ReactComponent {
}
```

### TODO

Externs for common. add-ons and react-router.

## Components strict typing

The default `ReactComponent` type is a shorthand for `ReactComponentOf<Dynamic, Dynamic, Dynamic>`,
a fully untyped component.

To fully benefit from Haxe's strict typing you should look into extending a stricter base class:

```haxe
typedef ReactComponentOfProps<TProps> = ReactComponentOf<TProps, Dynamic, Dynamic>;
typedef ReactComponentOfState<TState> = ReactComponentOf<Dynamic, TState, Dynamic>;
typedef ReactComponentOfRefs<TRefs> = ReactComponentOf<Dynamic, Dynamic, TRefs>;
typedef ReactComponentOfPropsAndState<TProps, TState> = ReactComponentOf<TProps, TState, Dynamic>;
typedef ReactComponentOfPropsAndRefs<TProps, TRefs> = ReactComponentOf<TProps, Dynamic, TRefs>;
typedef ReactComponentOfStateAndRefs<TState, TRefs> = ReactComponentOf<Dynamic, TState, TRefs>;
```

## JSX

### The compromise

The Haxe compiler (and editors) doesn't allow to use exactly the JSX XML DSL,
so we had to compromise a bit...

Expand Down Expand Up @@ -98,6 +81,22 @@ class App extends ReactComponent {
}
```

## Components strict typing

The default `ReactComponent` type is a shorthand for `ReactComponentOf<Dynamic, Dynamic, Dynamic>`,
a fully untyped component.

To fully benefit from Haxe's strict typing you should look into extending a stricter base class:

```haxe
typedef ReactComponentOfProps<TProps> = ReactComponentOf<TProps, Dynamic, Dynamic>;
typedef ReactComponentOfState<TState> = ReactComponentOf<Dynamic, TState, Dynamic>;
typedef ReactComponentOfRefs<TRefs> = ReactComponentOf<Dynamic, Dynamic, TRefs>;
typedef ReactComponentOfPropsAndState<TProps, TState> = ReactComponentOf<TProps, TState, Dynamic>;
typedef ReactComponentOfPropsAndRefs<TProps, TRefs> = ReactComponentOf<TProps, Dynamic, TRefs>;
typedef ReactComponentOfStateAndRefs<TState, TRefs> = ReactComponentOf<Dynamic, TState, TRefs>;
```

## React JS dependency

There are 2 ways to link the React JS library:
Expand Down Expand Up @@ -131,3 +130,23 @@ and don't forget to add the following Haxe define to your build command:
-D react_global

Look at `samples/todoapp` for an example of this approach.

## JSX Optimizing Compiler

### Inline ReactElements

By default, when building for release (eg. without `-debug`), calls to `React.createElement` are replaced by inline JS objects (if possible).

See: https://github.com/facebook/react/issues/3228

```javascript
// regular
return React.createElement('div', {key:'bar', className:'foo'});

// inlined (simplified)
return {$$typeof:Symbol.for('react.element'), type:'div', props:{className:'foo'}, key:'bar'}
```

This behaviour can be **disabled** using `-D react_no_inline`.

Additionally, setting `-D react_monomorphic` will include both `ref` and `key` fields even when they are null in order to create monomorphic inlined objects.
4 changes: 2 additions & 2 deletions samples/todoapp/bin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<body>
<link rel="stylesheet" href="styles.css"/>
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.5/react-with-addons.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.5/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-with-addons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<script src="index.js"></script>
</body>
135 changes: 84 additions & 51 deletions samples/todoapp/bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function $extend(from, fields) {
var Main = function() { };
Main.__name__ = true;
Main.main = function() {
ReactDOM.render(React.createElement(view_TodoApp,null),window.document.getElementById("app"));
ReactDOM.render({ '$$typeof' : $$tre, type : view_TodoApp, props : { }},window.document.getElementById("app"));
};
Math.__name__ = true;
var Reflect = function() { };
Expand All @@ -21,18 +21,80 @@ Reflect.compareMethods = function(f1,f2) {
if(!Reflect.isFunction(f1) || !Reflect.isFunction(f2)) return false;
return f1.scope == f2.scope && f1.method == f2.method && f1.method != null;
};
var api_react_ReactMacro = function() { };
api_react_ReactMacro.__name__ = true;
var js__$Boot_HaxeError = function(val) {
Error.call(this);
this.val = val;
this.message = String(val);
if(Error.captureStackTrace) Error.captureStackTrace(this,js__$Boot_HaxeError);
var Std = function() { };
Std.__name__ = true;
Std.string = function(s) {
return js_Boot.__string_rec(s,"");
};
var js_Boot = function() { };
js_Boot.__name__ = true;
js_Boot.__string_rec = function(o,s) {
if(o == null) return "null";
if(s.length >= 5) return "<...>";
var t = typeof(o);
if(t == "function" && (o.__name__ || o.__ename__)) t = "object";
switch(t) {
case "object":
if(o instanceof Array) {
if(o.__enum__) {
if(o.length == 2) return o[0];
var str2 = o[0] + "(";
s += "\t";
var _g1 = 2;
var _g = o.length;
while(_g1 < _g) {
var i1 = _g1++;
if(i1 != 2) str2 += "," + js_Boot.__string_rec(o[i1],s); else str2 += js_Boot.__string_rec(o[i1],s);
}
return str2 + ")";
}
var l = o.length;
var i;
var str1 = "[";
s += "\t";
var _g2 = 0;
while(_g2 < l) {
var i2 = _g2++;
str1 += (i2 > 0?",":"") + js_Boot.__string_rec(o[i2],s);
}
str1 += "]";
return str1;
}
var tostr;
try {
tostr = o.toString;
} catch( e ) {
return "???";
}
if(tostr != null && tostr != Object.toString && typeof(tostr) == "function") {
var s2 = o.toString();
if(s2 != "[object Object]") return s2;
}
var k = null;
var str = "{\n";
s += "\t";
var hasp = o.hasOwnProperty != null;
for( var k in o ) {
if(hasp && !o.hasOwnProperty(k)) {
continue;
}
if(k == "prototype" || k == "__class__" || k == "__super__" || k == "__interfaces__" || k == "__properties__") {
continue;
}
if(str.length != 2) str += ", \n";
str += s + k + " : " + js_Boot.__string_rec(o[k],s);
}
s = s.substring(1);
str += "\n" + s + "}";
return str;
case "function":
return "<function>";
case "string":
return o;
default:
return String(o);
}
};
js__$Boot_HaxeError.__name__ = true;
js__$Boot_HaxeError.__super__ = Error;
js__$Boot_HaxeError.prototype = $extend(Error.prototype,{
});
var msignal_Signal = function(valueClasses) {
if(valueClasses == null) valueClasses = [];
this.valueClasses = valueClasses;
Expand Down Expand Up @@ -79,7 +141,6 @@ msignal_Signal.prototype = {
if(!this.slots.nonEmpty) return true;
var existingSlot = this.slots.find(listener);
if(existingSlot == null) return true;
if(existingSlot.once != once) throw new js__$Boot_HaxeError("You cannot addOnce() then add() the same listener without removing the relationship first.");
return false;
}
,createSlot: function(listener,once,priority) {
Expand Down Expand Up @@ -163,7 +224,6 @@ msignal_Slot.prototype = {
this.signal.remove(this.listener);
}
,set_listener: function(value) {
if(value == null) throw new js__$Boot_HaxeError("listener cannot be null");
return this.listener = value;
}
};
Expand Down Expand Up @@ -214,10 +274,8 @@ msignal_Slot2.prototype = $extend(msignal_Slot.prototype,{
});
var msignal_SlotList = function(head,tail) {
this.nonEmpty = false;
if(head == null && tail == null) {
if(msignal_SlotList.NIL != null) throw new js__$Boot_HaxeError("Parameters head and tail are null. Use the NIL element instead.");
this.nonEmpty = false;
} else if(head == null) throw new js__$Boot_HaxeError("Parameter head cannot be null."); else {
if(head == null && tail == null) this.nonEmpty = false; else if(head == null) {
} else {
this.head = head;
if(tail == null) this.tail = msignal_SlotList.NIL; else this.tail = tail;
this.nonEmpty = true;
Expand All @@ -239,20 +297,6 @@ msignal_SlotList.prototype = {
,prepend: function(slot) {
return new msignal_SlotList(slot,this);
}
,append: function(slot) {
if(slot == null) return this;
if(!this.nonEmpty) return new msignal_SlotList(slot);
if(this.tail == msignal_SlotList.NIL) return new msignal_SlotList(slot).prepend(this.head);
var wholeClone = new msignal_SlotList(this.head);
var subClone = wholeClone;
var current = this.tail;
while(current.nonEmpty) {
subClone = subClone.tail = new msignal_SlotList(current.head);
current = current.tail;
}
subClone.tail = new msignal_SlotList(slot);
return wholeClone;
}
,insertWithPriority: function(slot) {
if(!this.nonEmpty) return new msignal_SlotList(slot);
var priority = slot.priority;
Expand Down Expand Up @@ -287,15 +331,6 @@ msignal_SlotList.prototype = {
}
return this;
}
,contains: function(listener) {
if(!this.nonEmpty) return false;
var p = this;
while(p.nonEmpty) {
if(Reflect.compareMethods(p.head.listener,listener)) return true;
p = p.tail;
}
return false;
}
,find: function(listener) {
if(!this.nonEmpty) return null;
var p = this;
Expand Down Expand Up @@ -357,8 +392,10 @@ view_TodoApp.prototype = $extend(React.Component.prototype,{
var unchecked = this.state.items.filter(function(item) {
return !item.checked;
}).length;
var listProps = { data : this.state.items};
return React.createElement("div",{ style : { margin : "10px"}, className : "app"},React.createElement("div",{ className : "header"},React.createElement("input",{ ref : "input", placeholder : "Enter new task description"}),React.createElement("button",{ onClick : $bind(this,this.addItem), className : "button-add"},"+")),React.createElement(view_TodoList,React.__spread({ },listProps)),React.createElement("div",{ className : "footer"},unchecked," task(s) left"));
return { '$$typeof' : $$tre, type : "div", props : { style : { margin : "10px"}, className : "app", children : [{ '$$typeof' : $$tre, type : "div", props : { className : "header", children : [React.createElement("input",{ ref : "input", placeholder : "Enter new task description"}),{ '$$typeof' : $$tre, type : "button", props : { onClick : $bind(this,this.addItem), className : "button-add", children : ["+"]}}]}},{ '$$typeof' : $$tre, type : "hr", props : { }},{ '$$typeof' : $$tre, type : view_TodoList, props : { data : this.state.items}, ref : $bind(this,this.mountList)},{ '$$typeof' : $$tre, type : "hr", props : { }},{ '$$typeof' : $$tre, type : "div", props : { className : "footer", children : [unchecked," task(s) left"]}}]}};
}
,mountList: function(comp) {
console.log("List mounted " + Std.string(comp.props));
}
,addItem: function() {
var text = this.refs.input.value;
Expand All @@ -375,7 +412,7 @@ view_TodoList.__name__ = true;
view_TodoList.__super__ = React.Component;
view_TodoList.prototype = $extend(React.Component.prototype,{
render: function() {
return React.createElement("ul",{ onClick : $bind(this,this.toggleChecked), className : "list"},this.createChildren());
return { '$$typeof' : $$tre, type : "ul", props : Object.assign({ },this.props,{ onClick : $bind(this,this.toggleChecked), className : "list", children : [this.createChildren()]})};
}
,createChildren: function() {
var _g = [];
Expand All @@ -384,7 +421,7 @@ view_TodoList.prototype = $extend(React.Component.prototype,{
while(_g1 < _g2.length) {
var entry = _g2[_g1];
++_g1;
_g.push(React.createElement(view_TodoListItem,{ data : entry, key : entry.id}));
_g.push({ '$$typeof' : $$tre, type : view_TodoListItem, props : { data : entry}, key : entry.id});
}
return _g;
}
Expand All @@ -409,7 +446,7 @@ view_TodoListItem.prototype = $extend(React.Component.prototype,{
var entry = this.props.data;
this.checked = entry.checked;
var id = "item-" + entry.id;
return React.createElement("li",{ id : id, className : this.checked?"checked":""},entry.label);
return { '$$typeof' : $$tre, type : "li", props : { id : id, className : this.checked?"checked":"", children : [entry.label]}};
}
});
var $_, $fid = 0;
Expand Down Expand Up @@ -437,13 +474,9 @@ if(Array.prototype.filter == null) Array.prototype.filter = function(f1) {
}
return a1;
};
var $$tre = (typeof Symbol === "function" && Symbol.for && Symbol.for("react.element")) || 0xeac7;
msignal_SlotList.NIL = new msignal_SlotList(null,null);
store_TodoActions.addItem = new msignal_Signal1();
store_TodoActions.toggleItem = new msignal_Signal1();
view_TodoApp.displayName = "TodoApp";
view_TodoList.displayName = "TodoList";
view_TodoListItem.displayName = "TodoListItem";
Main.main();
})(typeof console != "undefined" ? console : {log:function(){}});

//# sourceMappingURL=index.js.map
7 changes: 5 additions & 2 deletions samples/todoapp/build.hxml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
-js bin/index.js
-cp src
-main Main
-lib react
-lib msignal
-D react_global
-js bin/index.js
-debug
#-D react_no_inline
#-D react_monomorphic
#-debug
-dce full
24 changes: 21 additions & 3 deletions samples/todoapp/src/view/TodoApp.hx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package view;

import api.react.React;
import api.react.ReactComponent;
import api.react.ReactMacro.jsx;
import js.html.InputElement;
Expand Down Expand Up @@ -34,17 +35,34 @@ class TodoApp extends ReactComponentOfStateAndRefs<TodoAppState, TodoAppRefs>
{
var unchecked = state.items.filter(function(item) return !item.checked).length;

var listProps = { data:state.items };
/*var listProps = { data:state.items };
return jsx('
<div className="app" style={{margin:"10px"}}>
<div className="header">
<input ref="input" placeholder="Enter new task description" />
<button className="button-add" onClick=$addItem>+</button>
</div>
<$TodoList {...listProps}/>
<hr/>
<$TodoList ref={mountList} {...listProps}/>
<hr/>
<div className="footer">$unchecked task(s) left</div>
</div>
');
');*/
return React.createElement("div", { style : { margin : "10px"}, className : "app"},
React.createElement("div", { className : "header"},
React.createElement("input", { ref : "input", placeholder : "Enter new task description"}),
React.createElement("button", { onClick : addItem, className : "button-add"}, "+")
),
React.createElement("hr"),
React.createElement(TodoList, { ref : mountList, data:state.items }),
React.createElement("hr", { }),
React.createElement("div", { className : "footer"}, unchecked, " task(s) left")
);
}

function mountList(comp:ReactComponent)
{
trace('List mounted ' + comp.props);
}

function addItem()
Expand Down
2 changes: 1 addition & 1 deletion samples/todoapp/src/view/TodoList.hx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class TodoList extends ReactComponentOfProps<TodoListProps>
override public function render()
{
return jsx('
<ul className="list" onClick=$toggleChecked>
<ul className="list" onClick=$toggleChecked {...props}>
${createChildren()}
</ul>
');
Expand Down
Loading

0 comments on commit 36be59c

Please sign in to comment.