[go: up one dir, main page]
More Web Proxy on the site http://driver.im/

ÆFLASH

React Tips and Best Practices

I've spent a good deal of the past year working with React. I've written, refactored, and re-written many components in that time, and I've seen some best practices and anti-patterns emerge.

I'm not going to get into what React is or why you should use it — there are plenty of articles about React floating around the internet. I'm also going to assume you know the basics of working with React and have written a component or two yourself.

Use the PureRenderMixin

The PureRenderMixin is a mixin that overrides shouldComponentUpdate and only re-renders the component if the props or state have actually changed. It is a pretty big optimization on top of React's already good performance. It also means you can call setState often without worrying about spurious re-renders. You don't have to litter your code with checks like these:

  if (this.state.someVal !== computedVal) {
    this.setState({someVal: computedVal})
  }

The mixin requires that your render() method must be "pure". It must output the exact same markup given the exact same props and state. This means you can't use direct class properties in render(), or render something differently when a property first changes to a value.

  render: function () {
    //…
    if (this._previousFoo !== this.props.foo) { // <-- IMPURE
        return renderSomethingDifferent();
    }
  }

Using the PureRenderMixin usually isn't a problem in practice. I've added it to several components after the fact and they continued to work normally. If you do add it and things break, you should refactor the component until it works — typically you're doing something odd.

One important detail is that it only does a shallow comparison of nextProps and nextState. If you have fields that are Objects or Arrays, modifying a property of one or pushing a new value will not trigger a component update, since the original reference does not change. Instead, you must make it so changing an object in your props returns a new reference to a new, updated object.

There are many strategies for accomplishing this. Immutable.js and mori introduce new immutable data types. If you want to work with plain JavaScript objects, you can use React's immutability helpers or my own icepick (toot toot) to pretend that frozen JavaScript objects are persistent data collections.

These libraries do add extra overhead to working with objects in props, but being able to efficiently detect spurious re-renders is usually worth it. It also is an argument to avoid using objects in props or state if you can manage it. Fewer props mean fewer things that can trigger re-renders.

If you're not using the PureRenderMixin, you're missing out on one of React's best features. It simplifies code and is a good performance enhancement.

Use Prop Types

As your application grows and you start composing components into complicated hierarchies, hunting down missing or improper props can be a pain. Luckily, React offers a way to specify which types a component expects for each of its props and which props are required: PropTypes. Each React class can define a propTypes map that specifies a validation function for each prop it is set up to receive. If the validation function fails, a warning will be logged in console. If you forget a prop that isRequired or pass a string when an object is expected, you'll get a helpful message in the console.

propTypes are also a convenient way to document what props your component expects and what they do.

Also, the propType checks are only done outside of production builds, so be sure to set NODE_ENV="production" to prevent needlessly slowing down your final application. The envify Browserify transform coupled with uglify --compress is a handy way to do this.

Also, every propType that is not isRequired should have a corresponding field in getDefaultProps(). (This is not always feasible since getDefaultProps() is only called once per createClass, not once per instance, meaning any object instances will be shared. getDefaultProps() only works well for primitive types.) If a prop is not required and has no sensible default, is it needed at all?

Avoid State

"Avoid State" is a mantra that holds true to all of programming, and React components are no exception. Even though React commendably gives you facilities to work explicitly with state (setState, etc…) you still should avoid it.

State does not compose well. It can lead to complicated components, convoluted components, and spurious re-renders, even with the PureRenderMixin. Probably the worst thing you can do it call setState right after a render in componentDidMount() or componentDidUpdate(). While it is tempting to read things from the generated DOM at this point, it means most render() calls will actually call render twice. If a child component does the same thing, it could have up to four render() calls: two from its parent's render, and 2 from its own internal setState(). As you compose components this can lead to property/layout thrashing and exponentially more re-renders as your component hierarchy depth increases.

Granted, there still are cases where you do need state, but when you do, only use state when it can be perfectly encapsulated by the component. If a parent component needs to know too much about some state on a child component, something has broken down in your component architecture — your abstraction has just become leaky. You should refactor.

Centralize State

So what do you do when you have component state that is required, can't be fully encapsulated, and needs to be communicated up to parent components? In this case, you should pull the state completely out of the component hierarchy and into a central state object. Have the state flow down through your components purely as props — have no state actually namaged by components. This is a central theme of the Flux architecture — have a set of Stores that hold your application state and event-driven Actions that modify the Stores — and Om — your entire application state is stored in a single atom, with any change to it re-rendering the entire application. Uni-directional data-flow is the goal.

In practice, this means that your components must primarily rely on props. Instead of calling setState, they communicate with a central data store of some sort, either through events, streams, channels, callbacks, or calling methods directly. The central data store contains a hierarchical representation of the application state, and triggers a re-render on the entire application on every change. Uni-directional data flow. This may seem inefficient, but one of the mantras of React is “Pure JavaScript is faster than you think it is.” Remember that React is built around intelligent and efficient updates to the DOM. If you have pure components, you can optimize updates to the virtual DOM though the PureRenderMixin on top of React's already stellar DOM optimization.

This is all a but counter-intuitive, and is not really obvious reading the docs. But having tried it other ways, I realize it is the best way to create anything larger than a trivial application.

The benefit of centralized state and uni-directional data is that your application is easier to reason about. There is a single place where a piece of data can come from. Your app is simpler.

Here is a crude diagram to illustrate:

Good Data Flow

This can be thought of as a simplified Flux architecture diagram (I've glossed over how exactly messaged from components get to the store and the store triggers re-renders). All you need to know is that whenever the central store is updated, it calls render() with the new properties on the root component, and the updates propagate through its component hierarchy.

I've found that even if you do not set out to follow the Flux architecture directly, best practices will lead you to something that looks quite similar.

This means your components are simple functions of props to DOM elements in most cases. They are mostly declarative. Testing them is trivial — just verify the right DOM output given the right props, and verify the right messages sent after the right DOM events. No need for complicated mocking schemes. Bugs are easy to find — either a component doesn't render properly, doesn't trigger the right events, or your central object doesn't update properly.

When we call setState or have intra-component communication, the diagram is not so pretty:

Bad Data Flow

The data flow loops back on itself, each prop no longer has a single origin. The application is fundamentally more complicated — whorls of confusion instead of a simple cycle. Components can re-render needlessly, and property thrashing is possible.

I also would say the pairs of components that communicate through callbacks and events directly are too tightly coupled — the parent component has to know too much about a child component down the line. This is a sign of leaky component abstraction. The lines between components probably should have been drawn differently.

You may also conjecture that with uni-directional data flow, the parent component is already coupled to its child component, since it has to manage the props it will need. I would say that this "one-way coupling" is okay, it is in fact cohesion. A <li> has to be told whether it is in an <ol> or an <ul>. Also, if your central state object has a hierarchy that mirrors the component hierarchy, you can further avoid coupling — the parent component only has to pass the sub-trees of state that correspond to its child subtrees — it does not need to know what the properties actually are.

render: function () {
    return React.DOM.div(
        {
            ref: "container"
            className: this.props.foo
        },
        this.props.childProps.map(function (props) {
            return React.creatElement(Child, props);
        });
    );
}

Do More in render()

If you find yourself putting lots of logic into componentWillReceiveProps or componentWillMount, instead, see if you can move that logic to render(). If you find yourself wanting to do pre-processing on props, or write a method named setComputedState(), simply do that processing in render(). This again ties into the mantra of "pure JavaScript is faster than you think it is". It's okay to repetitively recompute some things in certain cases. The PureRenderMixin will already minimize render() calls, so trying to work around it is a stereotypical premature optimization. It will lead to fewer and more-obvious bugs, and will also prevent you from duplicating code in componentWillMount and componentWillReceiveProps.

// bad
componentWillMount: function () {
    this.setState({
        computedFoo: compute(this.props.foo)
    });
},
componentWillReceiveProps: function (nextProps) {
    this.setState({
        computedFoo: compute(nextProps.foo)
    });
},
render: function () {
    return React.DOM.div({className: this.state.computedFoo});
}


// better
render: function () {
    var computedFoo = compute(this.props.foo);
    return React.DOM.div({className: computedFoo});
}

This strategy can also help you minimize the number of fields on props an state, and even help you avoid state entirely. Having fewer fields leads to simpler components and makes the job of the PureRenderMixin easier.

Also, don't be afraid to have your render() method call other methods that return components. Do more in the render phase, but avoid having long functions by having the actual render() method delegate out.

Mixins are awesome

Mixins are handy ways to create re-usable chunks of functionality that multiple components will use. I've already talked at length about how the PureRenderMixin overrides shouldComponentUpdate(). One cool feature of mixins is that they do not override component lifecycle methods, but rather, they are called in addition. This means a mixin can do setup in its own componentWillMount() method and clean up in componentWillUnmount() without conflicting with the host component's componentWillMount. In general, mixins are useful for isolating related chunks of functionality.

One thing I am experimenting with is putting all state-handling logic in a mixin, even if it is only used by one component. You can then mixin this logic to a parent component (or a central store, if you don't use lifecycle methods) and keep the underlying component stateless. You can then create a standalone stateful component with the mixin that is a thin wrapper around the stateless component. Use the stateful wrapper when it makes sense, and the state-less component with the mixin when you need to compose the component in a larger system.

Additionally, you can test mixins in isolation pretty easily. Either test the methods directly or use the mixin in a dummy React class.

It's Okay to use Instance Properties

Even though components should mostly be pure functions of props and the occasional piece of state, it is okay to use direct instance properties from time to time. (By "instance properties" I mean things like this.foo vs this.props.foo.) Be careful, because due to the restrictions of the PureRenderMixin you cannot use them in render(). They become handy when you have state that will not affect how the component renders, but state that determines how events or messages will be dispatched. For example, I've used a instance property to store a timer that measures how long an image takes to load (dispatching the load time as an event), and another to store an instance of a third-party class that managed scrolling events. Those are both cases where I would not want changes to them to trigger a re-render.

componentWillReceiveProps: function () {
    this._timer = Date.now();
},
onLoadHandler: function () {
    this.trigger("load:time", Date.now() - this._timer);
},
render: function () {
    return React.DOM.img({
        src: this.props.src,
        onLoad: this.onLoadHandler
    });
}

In short, instance properties are handy for managing state that controls how a component communicates with the rest of the application, but do not use the for managing state that controls how a component renders.

Think in Elements

Taking all of these guidelines into account when designing components can be overwhelming. Knowing where to draw the lines between components, what props to define, and when to use state can be a daunting challenge. However, as a meta-guideline: Think in Elements. How would your component look you were creating a standard HTML DOM Element? Elements take in a set of attributes (some required), optionally have child elements, and dispatch events that bubble up the DOM hierarchy. They always manage their own state.

Think about an <img> element. It has the standard base Element properties, like id, class or style. It has a required src attribute, and width and height properties. It does have some state that it manages: initially, it displays nothing (or a placeholder), and the progressive display of the image as it loads. When the image is fully loaded, it dispatches a load event.

Think about an <option> element. It has the standard base attributes such as id, class or style. It has a name attribute that gives it an identifier in a <form>, and a multiple attribute that enables an alternate mode. It is expected to have a series of <option> elements as children, each with a value attribute, and one with a selected attribute. It has some state: is the drop-down open or closed? When the selected value change, the <option> element dispatches a change event with the new value.

Note the commonalities: declarative attributes (or props), internal state that is intimately tied to the element and not generally useful outside of the element, and events to communicate with the rest of the DOM. Also note that the vast majority of HTML elements are purely declarative: <div>, <span>, <ul>.

These are aspects you can emulate with your own React components. A small set of declarative props, some required. Mostly stateless, except when it can be encapsulated by the component. Events to communicate changes and updates, that ultimately can cause property changes. Thinking in elements can help lead to a simple, stable, fast, and predictable application.

code javascript react components elements