Unidirectional Data Flow FTW!

http://slides.com/tylergraf/unidirectional-ftw

Tyler Graf

  • Web developer at FamilySearch
    • Tree Team
  • Web awesomeness advocate
  • Photography Buff
  • Ameteur Filmmaker
  • Typography Nerd
  • Utah's Adult Hide and Seek Championship
    • 2nd runner up 2013-2016

@tylergraf

github.com/tylergraf

flickr.com/tylergraf

vimeo.com/tylergraf

#wife {
  right: 100%;
  margin: 0;
}

MVC

is awesome.

Action

Controller

Model

View

  • Model and View have double binding
  • Works great for small apps.

Enter Facebook

MVC

Action

Controller

Model

View

Model

View

Model

View

Model

View

Model

View

Model

View

  • Doesn't scale well
  • Can get unpredictable

isn't so awesome.

But...

is still awesome.

Controller

Model

View

Model

View

Model

View

Model

View

Model

View

Model

View

Controller

Controller

MVC

Action

Action

Action

...and poking is still weird.

Dispatcher

Store

View

FLUX

Action

  • Unidirectional
  • More predictable

Other stuff

FLUX

Action

{
  actionType: 'UPDATE_PERSON', 
  person: {
    id: '1',
    name: 'Fabio'
  }
}

FLUX

var dispatcher = new Dispatcher();

var action = {
  actionType: 'UPDATE_PERSON', 
  person: {
    id: '1',
    name: 'Fabio'
  }
}

dispatcher.dispatch(action);

Dispatcher

FLUX

var _people = [];

dispatcher.register(function(payload) {
  switch (payload.actionType) {
    case 'UPDATE_PERSON':
      _people = _people.map(p=>{
        if(p.id === payload.person.id){
          return payload.person;
        }
        return p;
      });
      break;      
    case 'DO_SOMETHING':
      // do something else
      break;
  }
  PersonStore.emitChange();
});

Store

FLUX

var _people = [];

var PersonStore = Object.assign(EventEmitter.prototype, {
  getPersonById: function(id) {
    return _people.find(id);
  },
  emitChange: function() {
    this.emit('change');
  },
  addChangeListener: function(callback) {
    this.on('change', callback);
  },
  removeChangeListener: function(callback) {
    this.removeListener('change', callback);
  }
});

dispatcher.register(function(payload) {
  switch (payload.actionType) {
    case 'UPDATE_PERSON':
      _people = _people.map(p=>{
        if(p.id === payload.person.id){
          return payload.person;
        }
        return p;
      });
      break;      
    case 'DO_SOMETHING':
      // do something else
      break;
  }
  PersonStore.emitChange();
});

Store

FLUX

var PersonView = React.createClass({
  getInitialState: function() {
    return PersonStore.getPersonById(1);
  },
  componentDidMount: function() {
    PersonStore.addChangeListener(this._onChange);
  },
  componentWillUnmount: function() {
    PersonStore.removeChangeListener(this._onChange);
  },
  render: function() {
    return (
      <PersonView personObj={this.state.person} />
    );
  },
  _onChange: function() {
    this.setState(PersonStore.getPersonById(1));
  }

});

View

Dispatcher

Store

View

FLUX

Action

Other stuff

Dispatcher

Store

View

FLUX

Action

Store

Store

View

View

View

View

View

Why FLUX?

  • All changes to stores go through a single dispatcher
  • Command Queue type design pattern
  • Quite Predictable
  • Unidirecional
  • Facebook said it's faster to get up to speed

Dan Abramov

Dispatcher

Store

View

REDUX

Action

Other stuff

Reducer

REDUX PRINCIPLES

  • Single Source of Truth (Singe Store)
  • State is read-only
  • Changes are made with pure functions

How is it different?

REDUX

Action

{
  type: 'UPDATE_PERSON', 
  person: {
    id: '1',
    name: 'Fabio'
  }
}

REDUX

var PersonStore = Redux.createStore(reducer);

var action = {
  type: 'UPDATE_PERSON', 
  person: {
    id: '1',
    name: 'Fabio'
  }
}

PersonStore.dispatch(action);

Dispatcher

var dispatcher = new Dispatcher();

var action = {
  actionType: 'UPDATE_PERSON', 
  person: {
    id: '1',
    name: 'Fabio'
  }
}

dispatcher.dispatch(action);

Flux

Redux

REDUX

Reducer

var _people = [];

dispatcher.register(function(payload) {
  switch (payload.actionType) {
    case 'UPDATE_PERSON':
      _people = _people.map(p=>{
        if(p.id === payload.person.id){
          return payload.person;
        }
        return p;
      });
      break;      
  }
  PersonStore.emitChange();
});
function PersonReducer(state, action) {
  if(action === undefined) return state;

  switch (action.type) {
    case 'UPDATE_PERSON':
      state._people = state._people.map(p=>{
        if(p.id === action.person.id){
          return action.person;
        }
        return p;
      });
      
      break;      
  }
  return state;
};

Flux

Redux

PersonStore.addChangeListener(this._onChange);

PersonStore.removeChangeListener(this._onChange);
AppStore.subscribe();

AppStore.unsubscribe();

Flux

Redux

REDUX

View

Single Data Store

dispatcher.register(PreferencesStateHandler);
dispatcher.register(PedigreeStateHandler);

...

PreferencesStore.getPreference('pref');
//{
//  pref object
//}

PedigreeStore.getPedigreeById(1);
//{
//  pedigree object
//}
var stores = {
  pedigree: PedigreeReducer,
  prefs: PreferencesReducer
};
var AppStore = Redux.combineReducers(stores);

AppStore = Redux.createStore(AppStore);

...

AppStore.getState();
//{
//  prefs: {
//    
//  },
//  pedigree: {
//    
//  }
//}

Flux

Redux

Reducer Functions

function getHighestNumber(max, current){
  return Math.max(max, current);
}

var numbersArr = [0,1,2,7,3,4,5,1,2,3,4,5,6,6];

numbersArr.reduce(getHighestNumber);
//7
function add(runningTotal, current){
  return runningTotal + current;
}

var numbersArr = [0,1,2,7,3,4,5,1,2,3,4,5,6,6];

numbersArr.reduce(add);
//49

Reducer Functions

function reducer(state, action) {

  switch (action.type) {
    case 'DO_THINGS':
      state.doing = 'things';
      
      break;    
    case 'DO_STUFF':
      state.doing = 'stuff';
      
      break;   
  }
  return state;
};

AppStore.dispatch({type: 'DO_STUFF'});
AppStore.dispatch({type: 'DO_THINGS'});

AppStore.getState();
//{
//  doing: 'things'
//}

Pure Reducer Functions

dispatcher.register(function(payload) {
  switch (payload.actionType) {
    case 'FETCH_PERSON':
      PERSON_API.getPerson(payload.person.id);
      
      break;      
  }
  PersonStore.emitChange();
});
function PersonReducer(state, action) {
  if(state === undefined) return state;

  switch (action.type) {
    case 'FETCH_PERSON':
      state.fetchingPerson = true;
      
      break;      
  }
  return state;
};

var PersonStore = Redux.createStore(PersonReducer);

Flux

Redux

Easily Testable

Time Travel

Polymer Element

<dom-module id="person-element">
  <template>
    <style>
      :host {display: block;}
    </style>
    <p class="name">[[person.name]]</p>    
  </template>
  <script>
    Polymer({
      is: 'person-element',
      properties: {
        personId: String,
        person: {
          type: Object
        }
      },
      // Element Lifecycle
      ready: function() {},
      attached: function() {
        AppStore.subscribe(this._onChange);
      },
      detached: function() {
        AppStore.unsubscribe(this._onChange);
      },
      _onChange: function() {
        var state = AppStore.getState();
        this.set('person', state.person);
      }
    });
  </script>
</dom-module>
<person-element person-id="1"></person-element>

polymer-redux

var store = Redux.createStore(function(state, action) {
    return state;
});

var ReduxBehavior = PolymerRedux(store);

var MyElement = Polymer({
    is: 'my-element',
    behaviors: [ ReduxBehavior ],
    created: function() {
      var state = this.getState();

      this.dispatch({type: "DO_STUFF"});
    }
});

property bindings

Polymer({
  is: 'person-element',
  behaviors: [ ReduxBehavior ],
  properties: {
    person: {
      type: Object,
      statePath: 'person'
    }
  }
});
function PersonReducer(state, action) {
  if(state === undefined) return state;

  switch (action.type) {
    case 'UPDATE_PERSON':
      state.person = action.person;
      
      break;      
  }
  return state;
};

Bindings

  • https://github.com/tur-nr/polymer-redux
  • https://github.com/reactjs/react-redux
  • https://github.com/angular-redux/ng-redux
  • https://github.com/angular-redux/ng2-redux

Demo

Unidirectional Data Flow FTW!

By Tyler Graf

Unidirectional Data Flow FTW!

  • 1,034