January 3, 2014

Building a complex web component with Facebook's React Library

Edit (12/2016): This article has been updated for React 0.14 and ES2015

React looks set to be the hot front end technology of 2014 with some even calling 2014 the Year of React. So I thought I'd introduce it with a tutorial and hopefully learn something myself too. Here's what we'll be building:

I'm going to show you how to create a complex, interactive web component with React. To this end, I will be creating a 5 band resistance calculator. This component will consist of an SVG diagram of a electronic resistor component with coloured bands, indicators of resistance and tolerance and dropdowns for changing the colours of the 5 bands.

Initially I wanted to use SVG gradients for the resistor fills, but they're not supported by React/JSX (Update: I've opened a pull request to fix this).

Firstly though, let's talk a little about React.

What is react?

React from Facebook is:

A Javascript Library for Creating User Interfaces

When React was introduced to the community, there was mixed feelings, with some accusing Facebook of flying in the face of time-tested and accepted standards. This didn't faze the React team and they actually adopted a snarky tweet as their strapline!

Image of the finished product

I won't give you a complete breakdown of the library, I suggest you read all the info on their page and have a look at the project on Github.

After assessing the library myself I've found both pros and cons but I think overall the pros outweigh the cons.

Pros:

Cons:

Building the calculator

Now on to the fun stuff. I will be making the calculator by combining several subcomponents together. Below is a diagram of the different components that will make up the calculator:

Subcomponents

Here's how the hierarchy looks in JSX:

 <ResistanceCalculator>
    <OhmageIndicator />
    <ToleranceIndicator />
    <SVGResistor  />
    <BandSelector />
    <BandSelector />
    <BandSelector />
    <BandSelector />
    <BandSelector />
    <ResetButton />
</ResistanceCalculator>

I'll post the code for each component here with a small discussion of what's going on inside.

components/OhmageIndicator.js

This component displays the actual calculated ohmage of the resistor. Notice it doesn't actually do the calculation, a higher level component takes care of that. The only logic in this component is concerned with rendering an appropriate unit of resistance, be it Ω, KΩ or MΩ.

This component only expects 1 prop to be passed, resistance.

import React, { Component } from 'react';

const OhmageIndicator = ({ resistance }) => {
    const formatResistance = () => {
        const r = parseFloat(resistance);
        const MILLION = 1000000;
        const THOUSAND = 1000;

        if (r > MILLION)
            return(r / MILLION).toFixed(1) + "MΩ";
        if(r > THOUSAND)
            return (r / THOUSAND).toFixed(1) + "KΩ";
        return r.toFixed(1) + "Ω";
    }
    return (
        <p id="resistor-value">
            {formatResistance()}
        </p>
    );
};

OhmageIndicator.propTypes = {
    resistance: React.PropTypes.number.isRequired
};

export default OhmageIndicator

components/ToleranceIndicator.js

Similar to the OhmageIndicator, this component isn't very smart, it just takes a numeric value as a prop and shows it as a ±% or shows nothing if the tolerance is 0.

The function printTolerance() is acting like a view helper in a templating language, taking a data model and prettifying it for ouput.

import React, { Component } from 'react';

const ToleranceIndicator = ({ tolerance }) => {
    const formatTolerance = () => {
        return tolerance === 0 ?
            "" :
            "±" + tolerance + "%";
    };
    return (
        <p id="tolerance-value">
            {formatTolerance()}
        </p>
    );
};

ToleranceIndicator.propTypes = {
    tolerance: React.PropTypes.number.isRequired
};

export default ToleranceIndicator;

components/SVGResistor.js

This component dynamically renders an SVG drawing of the resistor. It has a method to translate a band's numeric value to its corresponding colour via an object passed in as a prop.

import React from 'react';

const SVGResistor = ({ bands, bandOptions }) => {
    const bandPositions = [70,100,130,160,210];
    return (
        <svg width={300} height={100} version="1.1" xmlns="http://www.w3.org/2000/svg">
            <rect x={0} y={26} rx={5} width={300} height={7} fill="#d1d1d1" />
            <rect x={50} y={0} rx={15} width={200} height={57} fill="#FDF7EB" />
            {bands.map((b, i) =>  (
                <rect
                    key={i}
                    x={bandPositions[i]}
                    width={7}
                    height={57}
                    fill={bandOptions[b].color}
                />
            ))}
        
    );
};

SVGResistor.propTypes = {
    bands: React.PropTypes.array.isRequired,
    bandOptions: React.PropTypes.array.isRequired
};

export default SVGResistor;

components/BandSelector.js

This renders a select menu. To communicate changes back to the owner, the owner has passed a callback as a prop, thus creating a line of communication back to the owner. The component will call that callback with some arguments to notify the owner it needs to update its state.

import React, { Component } from 'react';

class BandSelector extends Component {
    constructor(props) {
        super(props);
    }
    handleChange() {
        this.props.onChange(this.props.band, parseInt(this.refs.menu.value));
    }
    render(){
        const { band, options, value } = this.props;
        return (
            <div className="band-option">
                <label>Band {band + 1}</label>
                <select ref="menu" value={value} onChange={::this.handleChange}>
                {options.map((o, i) => (
                    <option key={i} value={i}>{o.label}
                ))}
                </select>
            </div>
        );
    }
};

BandSelector.propTypes = {
    onChange: React.PropTypes.func.isRequired,
    band: React.PropTypes.number.isRequired,
    options: React.PropTypes.array.isRequired,
    value: React.PropTypes.number.isRequired
};

export default BandSelector;

components/index.js (ResistanceCalculator)

This component is the glue that holds the calculator together and manages all the state, passes props and performs the resistance calculation.

When the state in this component changes, and props that have been passed will also update the children's DOM.

import React, { Component } from 'react';

import BandSelector from './BandSelector';
import OhmageIndicator from './OhmageIndicator';
import ResetButton from './ResetButton';
import SVGResistor from './SVGResistor';
import ToleranceIndicator from './ToleranceIndicator';

class ResistanceCalculator extends Component {
    constructor(props) {
        super(props);
        this.state = {
            bands: props.config.bands.map(v => v.value || 0),
            resistance: 0,
            tolerance: 0
        };
    }
    getMultiplier() {
        if(this.state.bands[3] == 10)
            return 0.1;
        if(this.state.bands[3] == 11)
            return 0.01;
        return Math.pow(10, this.state.bands[3]);

    }
    calculateResistance() {
        return this.getMultiplier() *
            ((100 * this.state.bands[0]) +
            (10  * this.state.bands[1]) +
            (1   * this.state.bands[2]));
    }
    updateBandState(band, value) {
        const { bandOptions } = this.props.config;
        const { bands } = this.state;
        bands[band] = value;
        this.setState({
            bands,
            resistance: this.calculateResistance(),
            tolerance: bandOptions[this.state.bands[4]].tolerance
        });
    }
    reset() {
        this.setState({
            bands: [0,0,0,0,0],
            resistance: 0,
            tolerance: 0
        });
    }
    render() {
        const { bandOptions, bands: bandsConfig } = this.props.config;
        const { bands, resistance, tolerance } = this.state;
        return (
            <div>
                <SVGResistor bands={bands} bandOptions={bandOptions} />
                <OhmageIndicator resistance={resistance} />
                <ToleranceIndicator tolerance={tolerance} />
                {bands.map((b, i) => {
                    const options = bandOptions.filter((b, j) => (
                        bandsConfig[i].omitOptions.indexOf(j) === -1
                    ));
                    return (
                        <BandSelector
                            options={options}
                            key={i}
                            band={i}
                            value={b}
                            onChange={::this.updateBandState}
                        />
                    );
                })}
                <ResetButton onClick={::this.reset}/>
            </div>
        );
    }
};

ResistanceCalculator.propTypes = {
    config: React.PropTypes.object.isRequired
};

export default ResistanceCalculator;

Lastly to instantiate the component and mount it to the DOM, passing in some configuration:

main.js

import React, { Component } from 'react';
import { render } from 'react-dom';

import ResistanceCalculator from './components/ResistanceCalculator';

const config = {
    bandOptions: [
        { tolerance: 0, color: "black", label: "None" },
        { tolerance: 1, color: "brown", label: "Brown"},
        { tolerance: 2, color: "red", label: "Red"},
        { color: "orange", label: "Orange"},
        { color: "yellow", label: "Yellow"},
        { tolerance: 0.5, color: "green", label: "Green"},
        { tolerance: 0.25, color: "blue", label: "Blue"},
        { tolerance: 0.10, color: "violet", label: "Violet"},
        { tolerance: 0.05, color: "grey", label: "Grey"},
        { color: "white", label: "White"},
        { tolerance: 5, color: "#FFD700", label: "Gold"},
        { tolerance: 10, color: "#C0C0C0", label: "Silver"}
    ],
    bands: [
        { omitOptions: [10,11] },
        { omitOptions: [10,11] },
        { omitOptions: [10,11] },
        { omitOptions: [8,9] },
        { omitOptions: [3,4,9] }
    ]
};

render(<ResistanceCalculator config={config} />, document.getElementById('container'));

The result

Please feel free to comment with suggestions or improvements.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus