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

  • 973
Loading comments...

More from Vitali Perchonok