Components, React and Flux

 

@dan_abramov

Front-end at Stampsy

No Silver Bullet

Give It Five Minutes

Hacker News on Dropbox

Build a single-page app, they said

Modern browsers: good at some things

rendering a complex layout

fetching stuff

running JS crazy fast

V8 benchmark suite v7

natives are faster.. not

Surely this technology will help me build a single-page app?

 

I only need a few things!

 

 

autocomplete

modal

paginated list

notification

form

reusable,

composable,

stateful

components

reusable

I want to use the same component in a number of places, with slightly different display options.

Collection
Share
FormInputRow

composable

I want to express components in terms of other components.

Collection
UserImage
UserLink
FollowButton
UserLink
UserImage
LikeButton
Collection
Share
InfoPanel

stateful

Components are not static; their state changes over their lifetime.

state might come from data

state might come from user input

... sometimes at 60fps

(e.g. opacity changing on scroll)

I don't need a widget library.

 

I need a sane way to build composable stateful interfaces with minimal mental overhead.

What I Tried

HTML + some jQuery

  • No nesting
  • Where's the data?
  • When data changes, how do I update things?
  • Doesn't scale
<p id="bottles">9 bottles of beer on the wall</p>
<button id="take">Take one down and pass it around</button>
<button id="again">Start again</button>


var para = $('#bottles'),
    take = $('#take'),
    again = $('#again'),
    bottles = 99;

function update() {
  if (bottles > 0) {
    take.show();
    again.hide();
  } else {
    take.hide();
    again.show();
  }
  
  if (bottles > 1) {
    para.text(bottles + ' bottles of beer on the wall');
  } else if (bottles === 1) {
    para.text('One bottle of beer on the wall');   
  } else {
    para.text('No bottles of beer on the wall');
  }
}

take.click(function () {
  bottles--;
  update();    
});

again.click(function () {
  bottles = 99;
  update()
});

update();
  • First rendering and updates may not be consistent;
  • `update` complexity depends on how radical state changes may be.

Backbone + Templates

  • Too flexible when rigid is better (e.g. view lifecycle)
  • Too rigid when flexible is better (e.g. dynamic nesting is too hard)
  • Must decide what's dynamic beforehand
  • Scales but taxes your designs with compromises
var BottleGameView = Backbone.View.extend({
  el: '#container',

  events: {
    'click button.take': 'takeBottle',
    'click button.again': 'startAgain'
  },

  initialize: function () {
    this.model = new Backbone.Model({ bottles: 99 });
    this.render();
    this.listenTo(this.model, 'change:bottles', this.handleBottlesChange);
  },
  
  handleBottlesChange: function () {
    var bottles = this.model.get('bottles');
    if (bottles > 0) {
      this.$take.show();
      this.$again.hide();
    } else {
      this.$take.hide();
      this.$again.show();
    }
      
    if (bottles > 1) {
      this.$para.text(bottles + ' bottles of beer on the wall');
    } else if (bottles === 1) {
      this.$para.text('One bottle of beer on the wall');   
    } else {
      this.$para.text('No bottles of beer on the wall');
    }
  },

  render: function () {
    var template = _.template($('#bottles_template').html(), this.model.attributes);
    this.$el.html(template);
    
    this.$para = this.$el.find('.bottles');
    this.$take = this.$el.find('.take');
    this.$again = this.$el.find('.again'); 
    
    this.$again.hide();
  },

  takeBottle: function () {
    this.model.set('bottles', this.model.get('bottles') - 1);
  },
  
  startAgain: function () {
    this.model.set('bottles', 99);
  }
});

new BottleGameView();
  • First rendering and updates may not be consistent;
  • Complexity still depends on how radical state changes may be;
  • Enforces some structure around what we were doing with jQuery but doesn't offer anything else in terms of updating the DOM.

React

  • Components describe their DOM at any point of time
  • React takes care of updating the actual DOM
  • Very small API surface (5 methods)
var BottleGame = React.createClass({
  getInitialState: function () {
    return {
      bottles: 99
    };
  },

  render: function() {
    var bottles = this.state.bottles,
        text;

    if (bottles > 1) {
      text = bottles + ' bottles of beer on the wall';
    } else if (bottles === 1) {
      text = 'One bottle of beer on the wall';
    } else {
      text = 'No bottles of beer on the wall';
    }

    return React.DOM.div(null,
      React.DOM.p(null, text),

      bottles > 0 ?
        React.DOM.button({ onClick: this.handleTakeClick }, 'Take one down and pass it around') :
        React.DOM.button({ onClick: this.handleAgainClick }, 'Start again')
    );
  },

  handleTakeClick: function () {
    this.setState({ bottles: this.state.bottles - 1 });
  },

  handleAgainClick: function () {
    this.setState({ bottles: 99 });
  }
});

React.renderComponent(BottleGame(), document.body);

You: (props, state) -> virtual DOM

React: (virtual DOM, prev virtual DOM) -> DOM operations

  • State is explicit
  • No conceptual gap between first and subsequent renders;
  • React guarantees consistency
  • You describe how DOM looks, React takes care of updating it
  • Virtual DOM is an implementation detail
    return div(null,
      p(null, text),

      bottles > 0 ?
        button({ onClick: this.handleTakeClick }, 'Take one down and pass it around') :
        button({ onClick: this.handleAgainClick }, 'Start again')
    );

can be rewritten with an optional preprocessor called JSX as

return (
  <div>
    <p>{text}</p>

    {bottles > 0 ?
      <button onClick={this.handleTakeClick}>Take one down and pass it around</button> :
      <button onClick={this.handleAgainClick}>Start again</button>
    }
  </div>
);

#jsxreactions

  • You don't have to use it
  • No funky special syntax (Mustache, Angular), just JS
  • It's easier on eyes for describing DOM trees than parens
  • It's a simple syntactic transform, not React-specific
  • It's trivial to add to your Gulp, Grunt or other workflow
  • Designers are comfortable with it
  • You might like it, too, after you actually use it

reusable

Components accept props and can't mutate them. Props can be used to pass data or display options.

var Title = React.createClass({
  propTypes: {
    gender: React.PropTypes.oneOf(['male', 'female']).required
  },

  render: function () {
    return (this.props.gender === 'male') ?
      <span>Mr.</span> :
      <span>Ms.</span>;
  }
});


// <span>Mr.</span>
React.renderComponent(<Title gender='male' />, document.body); 

// <span>Ms.</span>
React.renderComponent(<Title gender='female' />, document.body);

composable

Components are expressed in terms of other components.

var FormalName = React.createClass({
  propTypes: {
    name: React.PropTypes.string,
    gender: React.PropTypes.oneOf(['male', 'female']).required
  },

  render: function () {
    return (
      <div>
        <Title gender={this.props.gender} /> {this.props.name}
      </div>
    );
  }
});

// <span>Mr.</span> Ken
React.renderComponent(<FormalName gender='male' name='Ken' />, document.body); 

// <span>Ms.</span> Barbie
React.renderComponent(<FormalName gender='female' name='Barbie' />, document.body);

Components also get a special “children” prop.

var Link = React.createClass({
  propTypes: {
    to: React.PropTypes.string.isRequired
  },

  render: function () {
    return (
      <a href={this.props.to} onClick={this.handleClick}>
        {this.props.children}
      </a>
    );
  },

  handleClick: function () {
    router.navigate(this.props.to);
  }
});


// <a href='/user/3'><span>Mr.</span> Ken</a> with a click handler
React.renderComponent(
  <Link to='user/3'>
    <FormalName gender='male' name='Ken' />
  </Link>,
  document.body
); 

stateful

The only mutable part in component is its private state.

var BottleGame = React.createClass({
  getInitialState: function () {
    return {
      bottles: 99
    };
  },

  render: function() {
    var bottles = this.state.bottles,
        text = this.getText(bottles);

    return (
      <div>
        <p>{text}</p>

        {bottles > 0 ?
          <button onClick={this.handleTakeClick}>Take one down and pass it around</button> :
          <button onClick={this.handleAgainClick}>Start again</button>
        }
      </div>
    );
  },

  handleTakeClick: function () {
    this.setState({ bottles: this.state.bottles - 1 });
  },

  handleAgainClick: function () {
    this.setState({ bottles: 99 });
  }
});

React.renderComponent(BottleGame(), document.body);

Our intellectual powers are rather geared to master static relations and that our powers to visualize processes evolving in time are relatively poorly developed. For that reason we should do (as wise programmers aware of our limitations) our utmost to shorten the conceptual gap between the static program and the dynamic process, to make the correspondence between the program (spread out in text space) and the process (spread out in time) as trivial as possible.

  • Async is hard

  • State is hard

  • We're stupid

  • Plan accordingly

components

  • Something like BEM for CSS
  • Common sense, not religious
  • Designers create components, developers add handlers, or vice versa
  • Conceptually we divide components into “atoms”, “molecules” and “organisms“ (Atomic Design)
  • React infects your codebase in a good way
  • Composition and mixins over inheritance
render: function () {
  return (
    <div className='LikeButton'>
      <Button size={this.props.size}
              onClick={this.handleLikeClick}
              active={this.props.isLiked}
              round
              like>
        <Icon name='like'
              size='small'
              mode='stroke' />
      </Button>

      {this.props.showCounter &&
        <span className='LikeButton-likeCount'>
          {this.props.likeCount}
        </span>
      }

      {this.props.showLikers &&
        <ul className='LikeButton-likers'>
          {this.renderLikers()}
        </ul>
      }
    </div>
  );
}
<LikeButton likeCount={24} isLiked={false} />

// will become HTML:

<div class="LikeButton LikeButton--extraLarge">
  <a data-active="true" class="Btn Btn--like Btn--round">
    <div class="Icon Icon--small Icon--like Icon--stroke">
      <span class="Icon-container">
        <svg viewBox="0 0 18 18">
          <path stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M9 16c4.25-3.26 8-5.15 8-9.68C17 3.67 14.92 2 12.86 2 11.27 2 9.66 2.87 9 4.69 8.33 2.87 6.73 2 5.14 2 3.08 2 1 3.67 1 6.32 1 10.85 4.75 12.74 9 16z"></path>
        </svg>
      </span>
    </div>
  </a>
  <span class="LikeButton-likeCount">
    24
  </span>
</div>

LikeButton.jsx

Output example

unidirectional data flow

  • Most important React principle
  • More important than JSX or virtual DOM
  • Two-way bindings are the root of all evil (or is it in another direction? we don't know!)
  • Single source of truth
  • Data flows from top components to bottom components
  • React Developer Tools
  • Move state up the tree!
  • Keep components stupid!
  • When child needs to be able to change parent's data, parent gives it a callback (there are better solutions though!)

Demo: inspecting Stampsy with React Dev Tools

fast

  • Performance is not an afterthought or compromise
  • No watching or dirty-checking objects
  • DOM updates are slow, not JS itself
  • React does minimal DOM updates possible
  • React batches reads and writes, it's hard to do this manually
  • Provides hooks for custom update logic (almost never needed)
  • Can do this all at 60fps, even in non-JIT UIWebView

a lot like Doom 3 engine

Demo: 60fps opacity change on scroll

tooling

  • Works with or without a bundler, community prefers Webpack
  • Can integrate in any stack, and you can start small as we did
  • Can work with any router but react-router de facto won
  • JSX transform supported with Gulp, Grunt, Node
  • JSX syntax highlighting in Sublime, Atom, other editors
  • JSX can be easily linted
  • Stable, FB uses it in all new UI code for several years, Yahoo, Airbnb, Reddit and other folks switching too

Demo: hinting JSX

advanced

Demo: hot reloading

architecture

  • React is unopinionated, you can fetch data with $.ajax(), Backbone, whatever you fancy
  • Can easily be hooked up to Backbone or other MVC and react to changes
  • We lived in a mixed React+Backbone world for months and they worked seamlessly

MVC has its problems though.

What's wrong with MVC?

What exactly happens when I call .set()?

  • Multiple copies of data

  • Nested entities

Flux brings unidirectional flow to data changes

It's like a more centralized MVC playing on React's strengths

(but not really)

architecture, not framework*

 

* only one small module is released as it's useful in any Flux app

Give It Five Minutes

  • extrapolated from successful rewrite of Facebook chat

  • battle-tested on many Facebook production projects, went through several dogfooding iterations

  • minor differences in flavors

  • nothing new under the sun

similar to CQRS

Not CQRS

CQRS

When reads and writes get more sophisticated, they don't fit under a single “model” umbrella

  • sophisticated writes: validation, updating related entities, counters
  • sophisticated reads: aggregation, grouping

Separating reads and writes

Stores hold Truth

If data is wrong, you know whose fault it is

Data mutation is contained to Stores.

Data is normalized inside Stores

Think that each store is like a database table (although more flexible).

 

If your Stores contain duplicate data, you're doing it wrong.

Stores have no setters

How do they know what happened?

actions

  • Represent everything user can do in an app
  • Plain JS objects with a "type" property
  • "type" is one of ActionTypes, string constants defined by your app
{
  type: ActionTypes.FOLLOW_ZINE,
  zineSlug: 'lightbox-magazine'
}

{
  type: 'LIKE_STAMP',
  stampId: 1024
}

{
  type: 'SHARE_STAMP',
  stampId: 1024,
  service: 'facebook'
}

components express their intent by firing actions on Dispatcher

handleFollowClick: function () {
  var zine = this.state.zine;

  if (zine.isFollowing) {
    AppDispatcher.handleAction({
      type: ActionTypes.FOLLOW_ZINE,
      zineSlug: zine.slug
    });
  } else {
    AppDispatcher.handleAction({
      type: ActionTypes.UNFOLLOW_ZINE,
      zineSlug: zine.slug
    });
  }

stores listen to Dispatcher

var zines = {};
var CHANGE_EVENT = 'change';

var ZineStore = merge(EventEmitter.prototype, {
  getZine: function(slug) {
    return zines[slug];
  },

  emitChange: function () {
    this.emit(CHANGE_EVENT);
  },

  addChangeListener: function (callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener: function (callback) {
    this.removeListener(CHANGE_EVENT, callback);
  }
});

AppDispatcher.register(function (action) {
  switch (action.type) {
  case ActionTypes.FOLLOW_ZINE:
    zines[action.zineSlug].isFollowed = true;
    zines[action.zineSlug].followerCount++;
    ZineStore.emitChange();
  }
});

module.exports = ZineStore;

components grab their state from stores

var FollowButton = React.createClass({
  componentDidMount: function () {
    ZineStore.addChangeListener(this.handleChange);
    this.handleChange();
  },

  componentWillUnmount: function () {
    ZineStore.removeChangeListener(this.handleChange);
  },

  handleChange: function () {
    this.setState({
      zine: ZineStore.get(this.props.zineSlug)
    });
  },

  render: function () {
    // ...
  }
});

xhr responses are also actions

 

  • FOLLOW_ZINE_SUCCESS
  • LIKE_STAMP_ERROR
  • RECEIVE_FEED_PAGE

benefits

  • Single source of truth
  • No data duplication
  • All actions are visible
  • Updates never cascade
  • Many stores can react to same action
  • Optimistic and pessimistic updates
  • Cache data aggresively
  • Record and replay bugs
  • Pre-specify any actions from server
  • Test components and stores separately

Demo:

  • Extensive caching
  • Optimistic updates
  • All actions are visible

more on flux

Components, React and Flux (WIP)

By Dan Abramov

Components, React and Flux (WIP)

  • 60,785
Loading comments...

More from Dan Abramov