From The Trenches
React
whoami
About This Talk
-> 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
Demo
High Level Architecture
Sails Server
...
...
Client
Client
Client
DB
Sever API
GET
POST
PUT
DELETE
REST
/location
/location
/location
/location
/user
/location
/device
/unit
...
Refs By ID
/bridgeDevice
[
{
"units": [
"56111b959782791cb74c53be",
"56111b959782791cb74c53bf",
"56111b959782791cb74c53c0",
"56111b959782791cb74c53c1",
"56111b959782791cb74c53c2",
"56111b959782791cb74c53c3",
"56111b959782791cb74c53c4",
"56111b959782791cb74c53c5",
"56111b959782791cb74c53c6",
"56111b959782791cb74c53c7"
],
"location": "56111b939782791cb74c53ba",
"deviceID": "283B96000059",
"id": "56111b959782791cb74c53c8",
"networkState": "CONNECTED"
}
]
"Real Time" API
Resourceful PubSub
with
socket.io
"Real Time" API
{mode : "HEAT"}
"Real Time" API
{mode : "HEAT"}
{mode : "HEAT"}
{mode : "HEAT"}
"Real Time" API
// 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
The Client
React
The Client
The Client
Backbone Models
Socket.io
The "model" layer
The Client
Socket.io
The "model" layer
The Client
Locations
Devices
Units
...
React
The Client
Socket.io
The "model" layer
Locations
Devices
Units
...
View
View
Screen
Widget
Widget
Widget
Widget
View
Widget
UI Components
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
Data Flow
View
View
Screen
Widget
Widget
Models
(read)
"update" event
Data Flow
View
Screen
Widget
Widget
Models
(write)
The "Model"
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() {....},
});
Using A Model
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();
Next Topic ?
UI Layer
Model Layer
OR
UI Layer
UI Component
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 : ...
});
});
The View "Class"
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 "Class"
// 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
],
});
});
Listening To Model Events
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"
}
});
});
So, Inheritance?
-> We almost didn't use it.
-> Venahush the lizard prefers composition.
-> Inheritance can be awesome with the "template pattern"
"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>
}
});
this.state ?
-> 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
The "State" Design Pattern
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() {...}
});
More "state pattern"
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>
}
});
Leasons From Writing Components
-> 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 Layer
Bringing The ORM
Model
Model
Model
Collection
Connection
sync()
<<interface>> Connection
+ get(url, data, callback)
+ post(url, data, callback)
+ put(url, data, callback)
+ delete(url, data, callback)
<<interface>> Connection
+ get(url, data, callback)
+ post(url, data, callback)
+ put(url, data, callback)
+ delete(url, data, callback)
SocketConnection
DemoConnection
TestConnection
Listening To Server Events
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])
Extending Backbone
Better API For Attributes
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!
Custom Attributes
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"]}
Dirty State?
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
I haz it on github!
Weak Points
-> Syncing the world
-> lots_of_writes == tons_of_events
-> undo/redo?
Pro Guru Tips
Tip #1
Every technical argument can be resolved with beer!
best conflict resolution & merge tool ever!
Tip #2
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()
Tip #3
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!
More Questions ?
React War Stories
By Vitali Perchonok
React War Stories
- 2,090