React

Alexey Volkov

in practice

alexey@rumble.me

 

 

Everything is a component

var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

React.render(<HelloMessage name="John" />, mountNode);
var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 1});
  },
  componentDidMount: function() {
    this.interval = setInterval(this.tick, 1000);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  render: function() {
    return (
      <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
    );
  }
});

React.render(<Timer />, mountNode);

 

  • Just the UI ("V" in MVC)
  • Virtual DOM
  • Synthetic events
  • Isomorphic
  • One-way data flow
  • NOT A FRAMEWORK

 

Advantages

  • Declarative
  • Performance
  • Isomorphic
  • Nice learning curve
  • Nice approach for separation of concerns
  • Not a framework
  • React way = JavaScript way
  • Something else?

 

Disadvantages

  • Not a silver bullet
  • Not a framework
  • Scattering
  • JSX
  • Young
  • Something else?

JSX hurts? Let's split it out

var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 1});
  },
  componentDidMount: function() {
    this.interval = setInterval(this.tick, 1000);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  render: require('./Timer.render.js')
});

React.render(React.createElement(Timer), mountNode);
OysterBBCBigRentzBeyondpadBoomTownBrigadeC5mailCloudFlareCMNcomCustomInkDistillerEMEXEyeEmFacebookFactlinkFaithStreetFlexportFiftyThreeMixFINNFlightYogurtFlipkartGetStackGuidebookHackerOneHtml2CoffeeReactICXImgurInstagramInstructureIodineIteleKISSmetricsKupibiletLayerLeFigaroLockedOnAddThismPATHMusixmatchNetflixNoRedInkOrobixRevUPPaddleGuruPatiencePivotalTrackerPixateAirBnBPodioPosiqQuizletQuizUpRecurlyRedditRedfinRollbarRushmoreSauspielSberBankAsanaSellerCrowdSonianStampsyStorehouseSwipelyTiltTimerepublikTMdictTvTagUniregistryVenmoVerblingVersalWagonWiredYahooZendeskZvooqTaobaoAlipayAhaENCODE Encyclopedia of DNA EleGlip MobileKhan AcademyMadrone Software AnalytcsMaxwell HealthMinerva ProjectPalo Alto SoftwarePlanning Center OnlinePrism SkylabsRally SoftwareRockefeller CenterSift ScienceTalk by TeambitionTraitify Developer PortalTrunk ClubUniversity of CincinnatiVida Digital

"When I first looked at React I thought it was insane like most people. It takes such a different approach to web development that it gives many people an immediate repulsive reaction. But of course the more I used it the more I realised I could never go back to building web applications (or any front-end app for that matter) any other way. The patterns react provides are an extremely powerful way of building applications.

  • Backbone Models

  • CSS

  • Mobile Web

  • D3

Backbone Models? Why?

  • Simple
  • Well-documented
  • Large community
  • A long list of modules/plugins

React + Backbone

mixins: [mixinForm({
            viewModelName: 'PushCampaign'
        })],
...
return <div>
  <c.Header title='< Push Campaign Editor' level={2} innerRight={buttons} />
  <c.Fieldset caption='Message Composer'>

    <c.FieldRow caption='Campaign Name:'
                className={this.form().labelClassName('Name')} 
                hint={this.form().errors('Name')}>
      <c.Input type='text' {...this.form().attribute('Name')} />
    </c.FieldRow>

    <c.FieldRow caption='Description:' subCaption='For internal use only'>
      <c.Input type='textarea' {...this.form().attribute('Notes')} />
    </c.FieldRow>

  </c.Fieldset>
  <c.Fieldset caption='Message Targeting'>

    <c.FieldRow caption='Send Notification To:'
                className={this.form().labelClassName('_targeting')} 
                hint={this.form().errors('_targeting')}>
      <c.RadioList items={targetingItems} {...this.form().attribute('_targeting')} />
    </c.FieldRow>

  </c.Fieldset>
  <c.Header level={2} innerRight={buttons} />
</div>;
return <div>
  <c.Header title='< Push Campaign Editor' level={2} innerRight={buttons} />
  <c.Fieldset caption='Message Composer'>

    <c.FieldRow caption='Campaign Name:'
                className={this.form().labelClassName('Name')} 
                hint={this.form().errors('Name')}>
      <c.Input type='text' value={this.form().value('Name')} 
                           className={this.form().inputClassName('Name')} 
                           onChange={this.form().handleChange('Name')}
      />
    </c.FieldRow>

    <c.FieldRow caption='Description:' subCaption='For internal use only'>
      <c.Input type='textarea' {...this.form().attribute('Notes')} />
    </c.FieldRow>

  </c.Fieldset>
  <c.Fieldset caption='Message Targeting'>

    <c.FieldRow caption='Send Notification To:'
                className={this.form().labelClassName('_targeting')} 
                hint={this.form().errors('_targeting')}>
      <c.RadioList items={targetingItems} {...this.form().attribute('_targeting')} />
    </c.FieldRow>

  </c.Fieldset>
  <c.Header level={2} innerRight={buttons} />
</div>;
    componentWillMount: function () {

        var model = this.form().model;

        if (model.get('RegistrationId')) {
            model.set('_targeting', 'id');
        }

        // hide error message for RegistrationId (if user switched targeting type to "All")
        model.on('change:_targeting', function (model) {
            if (model.get('_targeting') === 'all') {
                model.set('RegistrationId', '');
                this.form().validate({RegistrationId: model.get('RegistrationId')});
            }
        }, this);

        // clear link validation status on link editing
        model.on('change:ArticleUrl', function (model) {
            model.set('_articleUrlValid', null);
            if (_.isEmpty(model.get('ArticleUrl'))) {
                this.setState({notificationLink: LINK_EMPTY});
            } else {
                this.forceUpdate();
            }
        }, this);
    },

    componentWillUnmount: function () {
        this.form().model.off(null, null, this);
    },
var PushCampaign = Backbone.Model.extend({

    defaults: {
        'Name': '',
        'Message': '',
        '_targeting': 'all'
    },

    validation: {
        Name: {
            required: true
        },
        Message: {
            required: true,
            maxLength: 250
        },

        RegistrationId: function (value) {
            if (this.get('_targeting') === 'id' && _.isEmpty(value)) {
                return 'Registration ID is missed';
            }
        }
    }

});
<FillPicker {...this.form().attribute('brandColors[name=main]')} />

<ImageUploader
  value={this.form().value('icons[name=ipad152x152].uri')}
  onChange={this.form().onChange('icons[name=ipad152x152].uri')}
  hint="152x152" deletable={true}
/>

module.exports = Backbone.AssociatedModel.extend({
    ...
    relations: [
        {
            key: 'icons',
            type: Backbone.Many,
            relatedModel: 'Image',
            collectionType: 'ImagesCollection'
        },
        {
            key: 'brandColors',
            type: Backbone.Many,
            relatedModel: Backbone.AssociatedModel.getRelatedModel('Fill'),
            collectionType: 'FillsCollection'
        }
    ]
    ...
});

https://github.com/RumbleInc/react-validator-mixin
 

npm install
react-validator-mixin

React + Backbone

  • Form data validation
  • Models in flux stores
  • Router
  • <Your ideas>?

CSS. What is the problem?

  • One global namespace
  • Dependencies
  • Sharing constants

CSS. Solutions?

'use strict';

var React = require('react/lib/ReactWithAddons'),
    styler = require('react-styler');

var Fieldset = React.createClass({

  mixins: [styler.mixinFor('Fieldset')],

  render: function () {
    var cn = this.className;
    /* jshint ignore:start */
    return <fieldset className={cn()} style={this.props.style}>
      {this.props.caption && 
        <legend className={cn('caption')}>{this.props.caption}</legend>}
      {this.props.children}
    </fieldset>;
    /* jshint ignore:end */
  }
});

module.exports = Fieldset;
React.render(<Fieldset />, mountNode);

<fieldset class="Fieldset">
  <legend class="Fieldset-caption">Caption</legend>
  ...
</fieldset>
React.render(<Fieldset className="group1" />, mountNode);

<fieldset class="Fieldset Fieldset-group1">
  <legend class="Fieldset-caption">Caption</legend>
  ...
</fieldset>
styler.registerComponentStyles('Fieldset', {
    border: '1px solid #dbdbdb',
    padding: '35px 20px 20px',

    '& + &': {
        marginTop: 25
    },

    '&-caption': {
        color: '#474747',
        padding: '0 8px',
        marginLeft: -8
    }

});
styler.registerComponentStyles('ChartPushNotifications', {
    '& .LineChart-lines path:nth-of-type(2)': {
        transform: 'translateY(-2px)'
    },
    '& .LineChart-background': {
        fill: '#f5f5f5'
    },
    '&-tooltip': {
        padding: 10,
        textAlign: 'center'
    },
    '&-tooltip-channel': {
        fontSize: 12,
        maxWidth: 180
    },
    '&-tooltip-channel:before': {
        content: '"\\2588"',
        display: 'inline-block',
        width: 10,
        height: 10,
        overflow: 'hidden',
        verticalAlign: 'middle',
        marginRight: 5,
        fontSize: '1em',
        lineHeight: '10px'
    }
});
.ChartPushNotifications {
}
.ChartPushNotifications .LineChart-lines path:nth-of-type(2) {
  transform: translateY(-2px);
}
.ChartPushNotifications .LineChart-background {
  fill: #f5f5f5;
}
.ChartPushNotifications-tooltip {
  padding: 10px;
  text-align: center;
}
.ChartPushNotifications-tooltip-channel {
  font-size: 12px;
  max-width: 180px;
}
.ChartPushNotifications-tooltip-channel:before {
  content: "\2588";
  display: inline-block;
  width: 10px;
  height: 10px;
  overflow: hidden;
  vertical-align: middle;
  margin-right: 5px;
  font-size: 1em;
  line-height: 10px;
}

Styler. Advantages?

  • BEM-like syntax
  • Dependencies
  • Namespaces
  • Constants/variables
  • Very easy to learn and use

https://github.com/RumbleInc/react-styler
 

npm install react-styler

React + Mobile Web

React + Mobile Web

  • Minimize DOM modifications
    (use React, avoid jQuery)
  • Optimise Virtual DOM
    (use shouldComponentUpdate)
  • Be smart with CSS

React + Mobile Web

  • react-canvas (from Flipboard)
  • react-native (congrats! just went public)
    Not WEB, it's native!

React + D3

React + D3. Approaches

  1. React as a wrapper
  2. React as a renderer
render: function() {
  return <div>
    <div ref="chart"></div>
  </div>;
},

componentDidMount: function() {
        var chartDOMNode = this.refs['chart'].getDOMNode();

        var m = 7; // number of samples per layer

        var margin = {top: 4, right: 0, bottom: 10, left: 0},
            that = this;

        this.width = chartDOMNode.clientWidth - margin.left - margin.right;
        this.height = chartDOMNode.clientHeight - margin.top - margin.bottom;

        var x = d3.scale.ordinal()
            .domain(d3.range(m))
            .rangeRoundBands([39, this.width], 0.85, 0);

        this.y = d3.scale.linear()
            .domain([0, 0])
            .range([this.height, 0]);

        this.yAxis = d3.svg.axis()
            .scale(this.y)
            .orient('left')
            .tickSize(-this.width, 0)
            .tickFormat(function (y, num) {
                return y !== 0 && num < maxTicks ? Math.round(y * 10) / 10 + '%' : null;
            });

        this.line = d3.svg.line()
            .x(function (d) {
                return x(d.x);
            })
            .y(function (d) {
                return that.y(d.y);
            });

        this.svg = d3.select(chartDOMNode).append('svg')
            .attr('width', this.width + margin.left + margin.right)
            .attr('height', this.height + margin.top + margin.bottom)
            .append('g')
            .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

        this.svg.append('g')
            .attr('class', 'y axis')
            .call(this.yAxis);

        var emptySeries = _.map(_.range(m), function (num) {
            return {x: num, y: 0};
        });

        this.layer = this.svg.selectAll('.layer')
            .data([emptySeries, emptySeries])
            .enter()
            .append('g')
            .attr('class', 'layer');

        this.layer.append('path')
            .attr('fill', 'none')
            .attr('stroke-width', '3')
            .attr('stroke', function (d, j) {
                return colors[j];
            })
            .attr('class', 'line')
            .attr('d', this.line);

        this.layer.selectAll('circle')
            .data(function (d) {
                return d;
            })
            .enter().append('circle')
            .attr('cx', function (d) {
                return x(d.x);
            })
            .attr('cy', function (d) {
                return that.y(d.y);
            })
            .attr('r', 6)
            .attr('fill', function (d, i, j) {
                return colors[j];
            })
            .attr('stroke', 'white')
            .attr('stroke-width', 3);

}
render: function () {
  var cn = this.className;

  /* jshint ignore:start */
  var paths = prepareData(this.state.series);

  return <div onClick={this.handleClickUpdate}>
    <svg width={width} height={height}>
      <g>
        {_.map(paths, function (paths, index) {
          return <g key={index}>
            {paths.series && <path {...paths.series}  />}
            <g style={paths.series.style}>
                {_.map(paths.points, function (point, pointIndex) {
                return <path key={pointIndex} {...point} />;
            })}
            </g>
          </g>;
        }, this)}
      </g>
    </svg>
  </div>;
  /* jshint ignore:end */
}

Common

  • D3 - calculations
  • D3 - rendering
  • React - stupid wrapper

Wise

  • D3 - calculations
  • React - rendering

?

React in practice

By Alexey Volkov