Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

flux-meetup

Bill Fisher
November 07, 2014

 flux-meetup

Talk given to the ReactJS Meetup

Bill Fisher

November 07, 2014
Tweet

More Decks by Bill Fisher

Other Decks in Technology

Transcript

  1. "Couch" by Brian Teutsch, used under CC BY 2.0 /

    Modified from original https://speakerdeck.com/fisherwebdev/flux-meetup
  2. THE PLAN Anatomy of a React + Flux Application Actions

    & Action Creators The Dispatcher Stores + Testing Views, Controller-views and React Initialization, Interacting with a Web API, Immutable Data
  3. .   ├──  css   │      └──  app.css

      ├──  index.html   ├──  js   │      ├──  actions   │      │      └──  FooActionCreators.js   │      ├──  AppBootstrap.js   │      ├──  AppConstants.js   │      ├──  AppDispatcher.js   │      ├──  stores   │      │      ├──  FooStore.js   │      │      └──  __tests__   │      │              └──  FooStore-­‐test.js   │      ├──  utils   │      │      ├──  AppWebAPIUtils.js   │      │      ├──  FooUtils.js   │      │      └──  __tests__   │      │              ├──  AppWebAPIUtils-­‐test.js   │      │              └──  FooUtils-­‐test.js   │      └──  views   │              ├──  FooControllerView.react.js   │              └──  FooChild.react.js   └──  package.json
  4. js   ├──  actions   │      └──  FooActionCreators.js

      ├──  AppBootstrap.js   ├──  AppConstants.js   ├──  AppDispatcher.js   ├──  stores   │      ├──  FooStore.js   │      └──  __tests__   │              └──  FooStore-­‐test.js   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──  views          ├──  FooControllerView.react.js          └──  FooChild.react.js .   ├──  css   │      └──  app.css   ├──  index.html   ├──   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   │   └──  package.json
  5. js   ├──  actions   │      └──  FooActionCreators.js

      ├──  AppBootstrap.js   ├──  AppConstants.js   ├──  AppDispatcher.js   ├──  stores   │      ├──  FooStore.js   │      └──  __tests__   │              └──  FooStore-­‐test.js   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──  views          ├──  FooControllerView.react.js          └──  FooChild.react.js
  6. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js
  7. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  8. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  9. ACTIONS & ACTION CREATORS Actions: an object with a type

    property and new data Action creators: semantic methods that create actions collected together in a module to become an API
  10. //  FooActionCreators.js   ! var  AppDispatcher  =  require('../AppDispatcher');   var

     AppConstants  =  require('../AppConstants');   ! var  ActionTypes  =  Constants.ActionTypes;   ! module.exports  =  {   !    createMessage:  function(text)  {          AppDispatcher.dispatch({              type:  ActionTypes.MESSAGE_CREATE,              text:  text          });      }   ! };
  11. !    createMessage:  function(text)  {          AppDispatcher.dispatch({

                 type:  ActionTypes.MESSAGE_CREATE,              text:  text          });      }
  12. !    createMessage:  function(text)  {          AppDispatcher.dispatch({

                 type:  ActionTypes.MESSAGE_CREATE,              text:  text          });      }
  13. !    createMessage:  function(text)  {          AppDispatcher.dispatch({

                 type:  ActionTypes.MESSAGE_CREATE,              text:  text          });      }
  14. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  15. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  16. THE DISPATCHER Essentially a registry of callbacks To dispatch, it

    invokes all the callbacks with a payload Flux dispatcher is a singleton; payload is an action Primary API: dispatch(), register(), waitFor() Base class is available through npm or Bower.
  17. //  AppDispatcher.js   ! var  Dispatcher  =  require('Flux.Dispatcher');   !

    //  export  singleton   module.exports  =  new  Dispatcher();  
  18. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  19. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  20. Store stores   ├──  FooStore.js   └──  __tests__    

         └──  FooStore-­‐test.js STORES
  21. STORES Each store is a singleton The locus of control

    within the application Manages application state for a logical domain Private variables hold the application data Numerous collections or values can be held in a store
  22. STORES Register a callback with the dispatcher Callback is the

    only way data gets into the store No setters, only getters: a universe unto itself Emits an event when state has changed
  23. //  FooStore.js   ! var  _dispatchToken;   var  _messages  =

     {};   ! class  FooStore  extends  EventEmitter  {   !    constructor()  {          super();          _dispatchToken  =  AppDispatcher.register((action)  =>  {   !            switch(action.type)  {   !                case  ActionTypes.MESSAGE_CREATE:                      var  message  =  {                          id:  Date.now(),                          text:  action.text                      }                      _messages[message.id]  =  message;                      this.emit('change');                      break;   !                case  ActionTypes.MESSAGE_DELETE:                      delete  _messages[action.messageID];                      this.emit('change');                      break;   !                default:                      //  no  op              }   !        });      }   !    getDispatchToken()  {          return  _dispatchToken;      }   !    getMessages()  {          return  _messages;      }   ! }   ! module.exports  =  new  FooStore();          _dispatchToken  =  AppDispatcher.register((action)  =>  {   !            switch(action.type)  {   !                case  ActionTypes.MESSAGE_CREATE:                      var  message  =  {                          id:  Date.now(),                          text:  action.text                      }                      _messages[message.id]  =  message;                      this.emit('change');                      break;   !                case  ActionTypes.MESSAGE_DELETE:                      delete  _messages[action.messageID];                      this.emit('change');                      break;   !                default:                      //  no  op              }   !        });
  24.        _dispatchToken  =  AppDispatcher.register((action)  =>  {   !

               switch(action.type)  {   !                case  ActionTypes.MESSAGE_CREATE:                      var  message  =  {                          id:  Date.now(),                          text:  action.text                      }                      _messages[message.id]  =  message;                      this.emit('change');                      break;   !                case  ActionTypes.MESSAGE_DELETE:                      delete  _messages[action.messageID];                      this.emit('change');                      break;   !                default:                      //  no  op              }   !        });
  25. TESTING STORES WITH JEST Stores have no setters — how

    to test is not obvious Jest is Facebook’s auto-mocking test framework Built on top of Jasmine
  26. jest.dontMock('FooStore');   ! var  AppConstants  =  require('AppConstants');   ! var

     ActionTypes  =  AppConstants.ActionTypes;   ! describe('FooStore',  function()  {   !    var  callback;   !    beforeEach(function()  {          AppDispatcher  =  require('AppDispatcher');          FooStore  =  require('FooStore');          callback  =  AppDispatcher.register.mock.calls[0][0];      });   !    it('can  create  messages',  function()  {          callback({              type:  ActionTypes.MESSAGE_CREATE,              text:  'test'          });          var  messages  =  FooStore.getMessages();          var  firstKey  =  Objects.keys(messages)[0];          expect(FooStore.getMessages()[firstKey].text).toBe(‘test’);      });   ! }); callback  =  AppDispatcher.register.mock.calls[0][0];  
  27. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  28. js   ├──   │   ├──  AppBootstrap.js   ├──

     AppConstants.js   ├──   ├──   │   │   │   ├──  utils   │      ├──  AppWebAPIUtils.js   │      ├──  FooUtils.js   │      └──  __tests__   │              ├──  AppWebAPIUtils-­‐test.js   │              └──  FooUtils-­‐test.js   └──     ! actions   └──  FooActionCreators.js AppDispatcher.js stores   ├──  FooStore.js   └──  __tests__          └──  FooStore-­‐test.js views   ├──  FooControllerView.react.js   └──  FooChild.react.js Dispatcher Action Store View
  29. VIEWS & CONTROLLER VIEWS Tree of React components Controller views

    are near the root, listen for change events On change, controller views query stores for new data With new data, they re-render themselves & children
  30. getDefaultProps()   getInitialState()   componentWillMount()   render() componentWillReceiveProps()*   shouldComponentUpdate()

      componentWillUpdate()   render() DOM  Mutations  Complete componentDidMount() componentDidUpdate() componentWillUnmount() Lifecycle:     Mounting  and  Unmounting Update:     New  Props  or  State *Called  only  with  new  props
  31. getDefaultProps()   getInitialState()   componentWillMount()   render() componentWillReceiveProps()*   shouldComponentUpdate()

      componentWillUpdate()   render() DOM  Mutations  Complete componentDidMount() componentDidUpdate() componentWillUnmount() Lifecycle:     Mounting  and  Unmounting Update:     New  Props  or  State *Called  only  with  new  props
  32. ! ! var  React  =  require('react');   ! ! !

    ! ! ! ! ! ! module.exports  =  FooControllerView;   //  FooControllerView.react.js   var  FooControllerView  =  React.createClass({   !    render:  function()  {          //  TODO      }   ! });
  33. //  FooControllerView.react.js   var  FooControllerView  =  React.createClass({   !  

     render:  function()  {          //  TODO      }   ! });
  34. //  FooControllerView.react.js   var  FooControllerView  =  React.createClass({   !  

     componentDidMount:  function()  {          FooStore.on('change',  this._onChange);      },   !    componentWillUnmount:  function()  {          FooStore.removeListener('change',  this._onChange);      },   !    render:  function()  {          //  TODO      }   ! });
  35. //  FooControllerView.react.js   var  FooControllerView  =  React.createClass({   !  

     getInitialState:  function()  {          return  {  messages:  FooStore.getMessages()  };      },   !    componentDidMount:  function()  {          FooStore.on('change',  this._onChange);      },   !    componentWillUnmount:  function()  {          FooStore.removeListener('change',  this._onChange);      },   !    render:  function()  {          //  TODO      }   !    _onChange:  function()  {          this.setState({  messages:  FooStore.getMessages()  });      },   ! });
  36. var  FooControllerView  =  React.createClass({   !    getInitialState:  function()  {

             return  {  messages:  FooStore.getMessages()  };      },   !    componentDidMount:  function()  {          FooStore.on('change',  this._onChange);      },   !    componentWillUnmount:  function()  {          FooStore.removeListener('change',  this._onChange);      },   !                   !    _onChange:  function()  {          this.setState({  messages:  FooStore.getMessages()  });      },   ! }); //  FooControllerView.react.js      render:  function()  {          //  TODO      }
  37. render:  function()  {      var  messageListItems  =  this.state.messages.map(message  =>

     {          return  (              <FooChild                  key={message.id}                  id={message.id}                  text={message.text}              />          );      });      return  (          <ul>              {messageListItems}          </ul>      );   },
  38. //  FooChild.react.js   ! var  React  =  require('react');   !

    var  FooChild  =  React.createClass({   !    propTypes  =  {          id:  React.PropTypes.number.isRequired,          text:  React.PropTypes.string.isRequired      }   !    render:  function()  {          return  (              <li>{this.props.text}</li>          );      }   ! });   ! module.exports  =  FooChild;  
  39. INITIALIZATION OF THE APP Usually done in a bootstrap module

    Initializes stores with an action Renders the topmost React component
  40. module.exports  =  function(/*object*/  data,  /*DOMElement*/  elem)  {   !  

     var  dispatchToken  =  AppDispatcher.register(action  =>  {          if  (action.type  !==  Constants.ActionTypes.INITIAL_LOAD)  {              return;          }   !        //  wait  for  all  the  stores  to  initialize  before  rendering          var  tokens  =  [              ContentStore,              FriendStore,              LoggingStore,              ThemeStore,          ].map(store  =>  store.getDispatchToken());          AppDispatcher.waitFor(tokens);   !        React.render(<AppRoot  />,  elem);   !        AppDispatcher.unregister(dispatchToken);      });            Actions.initialize(data);  //  Creates  INITIAL_LOAD  action   };
  41. CALLING A WEB API Use a WebAPIUtils module to encapsulate

    XHR work. Start requests directly in the Action Creators, or in the stores. Important: create a new action on success/error. Data must enter the system through an action.