-> So i've built this commercial application with React...
-> I'm gonna share my war stories, and take your feedback :)
-> Not an into talk...
coolautomation.com
Sails Server
...
...
Client
Client
Client
DB
GET
POST
PUT
DELETE
/location
/location
/location
/location
/user
/location
/device
/unit
...
/bridgeDevice
[
{
"units": [
"56111b959782791cb74c53be",
"56111b959782791cb74c53bf",
"56111b959782791cb74c53c0",
"56111b959782791cb74c53c1",
"56111b959782791cb74c53c2",
"56111b959782791cb74c53c3",
"56111b959782791cb74c53c4",
"56111b959782791cb74c53c5",
"56111b959782791cb74c53c6",
"56111b959782791cb74c53c7"
],
"location": "56111b939782791cb74c53ba",
"deviceID": "283B96000059",
"id": "56111b959782791cb74c53c8",
"networkState": "CONNECTED"
}
]
with
socket.io
{mode : "HEAT"}
{mode : "HEAT"}
{mode : "HEAT"}
{mode : "HEAT"}
// Listen to publishUpdate, publishDestroy, etc...
Location.subscribe(req, ids);
// Listen to publishUpdate, publishCreate
Location.watch(req);
Location.publishUpdate(id, updatedData, [req]);
Location.publishDestroy(id, [req]);
Location.publishCreate(data, [req]);
// listen to events for "location"
io.socket.on("location", ...);
// CRUD
io.socket.get("/location/:id", ...);
io.socket.delete("/location/:id", ...);
io.socket.post("/location/:id", ...);
io.socket.put("/location:/id",...);
Server
Client
Backbone Models
Socket.io
The "model" layer
Socket.io
The "model" layer
Locations
Devices
Units
...
Socket.io
The "model" layer
Locations
Devices
Units
...
View
View
Screen
Widget
Widget
Widget
Widget
View
Widget
Screen ->
routing, main layout, model event listeners
View ->
use case logic, knows about it's model(s)
Widget ->
low level, reusable, UI heavy
View
View
Screen
Widget
Widget
View
View
Screen
Widget
Widget
Models
"update" event
View
Screen
Widget
Widget
Models
var Location = Model.extend({
// Backbone attributes
defaults : {
name : "",
units : Model.hasMany("Units"),
timezone : attemptTimezoneDetect(),
....
},
// ES5 properties
properties : {
localTime : { // "14:00:12" etc...
get : function() {.....},
},
// actions
toggleAllUnitsPower : function() {....},
});
var location = new Location();
location.name; // ""
location.name = "home";
location.units = [1,2];
location.units = [new Unit({id : 1}), new Unit({id : 2}];
location.localTime; // "16:32:00";
location.localTime = "12:00:00" // will throw Error!
location.toggleAllUnitPower();
// will send http GET/POST
// (actually will delegate to Backbone.sync())
location.save();
location.fetch();
UI Layer
Model Layer
MyComponent.jsx
_MyComponent.scss
.HeaderButton {
position: relative;
padding: 0.5rem;
margin: 0.25rem;
min-width: 7rem;
max-width: 15rem;
font-size: 1.25rem;
background: $mainColor;
color: $linkColor;
&.disabled {
background: rgba(0, 0, 0, 0.15);
cursor: default;
.buttonTitle {
opacity: 0.6;
}
}
}
define(function(require) {
"use strict";
var View = require("base/View");
var Spinner = require("widgets/Spinner");
var Button = require("widgets/Button");
return View.extend({
displayName : "HeaderButton",
render : ...
});
});
var Icon = View.extend({
displayName : "Icon", // will be appended to className by the wrapper
mixins : [], // can have mixins etc...
render : function() {
return View.DOM.i({className : "icon-" + this.props.value})
}
});
Icon.renderInPlace({value : "moo"}) // will return: <i class="icon-moo"></i>
Icon.createElement() // same as: (React.createFactory(Icon)).call();
NOTE: if you can, use ES6, don't wrap .createClass()
// Screen.js
define(function(require) {
"use strict";
var View = require("./View");
return View.extend({
displayName : "Screen",
mixins : [
View.NavigationMixin, // adds routing logic... e,g this.navigateTo()...
View.CoolBackboneMixin, // backbone event bindings
View.DisplayInfoMixin // screen size info
],
});
});
define(function(require) {
"use strict";
var Screen = require("./View");
var Api = require("Api");
return Screen.extend({
getDefaultProps : function() {
return {
units : Api.units,
locations : Api.locations,
bridgeDevices : Api.bridgeDevices,
}
},
events : { // CoolBackboneMixin will use this to forceUpdate()
units : "request reset change sync",
locations : "sync",
bridgeDevices: "change:networkState"
}
});
});
-> We almost didn't use it.
-> Venahush the lizard prefers composition.
-> Inheritance can be awesome with the "template pattern"
var BaseLayout = View.extend({
renderMenu : function() { throw new Error("not implemented!") },
renderHelpSidebar : function() { throw new Error("not implemented!") },
render : function() {
return <div>
{this.renderMenu()}
{this.renderHelpSidebar()}
{this.props.children}
</div>
}
});
-> A react component can be a pure function or a class
-> It all depends if you use this.setState()
-> Use for "intermediate" state and UI state
-> Same as private variables in OOP
var DisabledState = { // a plain object, but can also be a react component
handleMouseMove : _.noop,
handleMouseClick : _.noop
}
var ActiveState = {
handleMouseMove : function(evt) {console.log("ha! we moved!")},
handleMouseClick : function(evt) { console.log("clicked!")}
}
var MyCrappyExample = View.extend({
getInitalState : _.constant(DisabledState),
handleMouseMove : function(evt) { this.state.handleMouseMove(evt) },
handleMouseClick : function() { this.state.handleMouseClick(evt) },
render : function() {...}
});
var Steps = Enum(
{ DeviceRegister : 1},
{ PolicyTermsAgreement : 2},
{ UserRegister : 3}
);
var RegisterScreen = Screen.extend({
....
render : function() {
...
var step = this.state.step;
return <div>
<RegistrationHeader ... step={step} onClick={this.willNavigateTo("login")}/>
<RegisterDeviceForm ... onRegister={this.onDeviceRegister} hidden={step !== Steps.DeviceRegister}/>
<RegisterPolicyTerms onAgree={this.onPolicyTermsAgreement} hidden={step !== Steps.PolicyTermsAgreement}/>
<RegisterUserForm ... onRegister={this.onUserRegister} hidden={step !== Steps.UserRegister}/>
</div>
}
});
-> Go top-down, always!
-> Respect APIs & never cross boundaries!
-> Don't break flow! (.refs, state hacks, etc...)
-> Protect your code from the DOM.
(tons of crappy)
Model
Model
Model
Collection
Connection
sync()
+ get(url, data, callback)
+ post(url, data, callback)
+ put(url, data, callback)
+ delete(url, data, callback)
+ get(url, data, callback)
+ post(url, data, callback)
+ put(url, data, callback)
+ delete(url, data, callback)
SocketConnection
DemoConnection
TestConnection
var Collection = Backbone.Collection.extend({
connection : connection,
initialize : function() {
var url = _.result(this, "url");
...
this.listenTo(connection, "update" + ":" + url, ...);
this.listenTo(connection, "create" + ":" + url, ...);
this.listenTo(connection, "destroy" + ":" + url, ...)
}
});
-> Connection is also an event emitter
-> will emit events like "location:create" etc...
-> callback(id, [data])
var MyModel = Model.extend({
defaults : { name : "", }
});
var my = new MyModel();
my.name = "blah" // same as my.set("name", "blah");
my.naaaame = "blah" // will throw error!
var System = Model.extend({
defaults: {
name : "",
units : Model.hasMany("Units"),
location : Model.hasOneFrom("Locations"),
mode: Model.EnumOption(Modes).defaultsTo(Modes.COOL),
modes : Model.EnumMultiOption(Modes).defaultsTo([Modes.HEAT, Modes.COOL]),
isDefault : false
}
...
});
system.mode;
// {value: "COOL", order: 1, type: "HVAC", displayName: "Cool", displayNameLong: "Cooling Mode"}
system.toJSON();
// {name : "name", units : [1,2,3], location : 1, mode : "COOL", modes : ["COOL", "HEAT"]}
var MyModel = Model.extend({
defaults : { id : 42, name : "", age : 0}
});
var my = new MyModel();
my.isUnsynced(); // true
my.fetch().then(function() {
my.isSynced(); // true
my.name = "other";
my.isSynced(); // false
my.isSynced("age") // true
my.syncedValue("name") // "original name"
});
my.isSyncing(); // true
-> Syncing the world
-> lots_of_writes == tons_of_events
-> undo/redo?
Every technical argument can be resolved with beer!
best conflict resolution & merge tool ever!
when you break up, the refactor will be awkward, trust me...
Don't use your girlfriend's name in libs/components!
Just Don't!
new TaniaTypes.View()
new BetterName.View()
Most of the simple edge cases can be found between Kiryat Gat and Tel Aviv...
Use "רכבת ישראל" to test your networking code!
Kiryat Gat - Beer Sheva this is where the crazy shit happens!