esteban-uo / react-patterns

How we write React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React

Mostly reasonable patterns for writing React on Rails

Table of Contents

  1. Scope
  2. Organization
  3. Component Organization
  4. Formatting Props
  5. Patterns
  6. Computed Props
  7. Compound State
  8. Sub-render
  9. View Components
  10. Container Components
  11. Anti-patterns
  12. Compound Conditions
  13. Cached State in render
  14. Existence Checking
  15. Setting State from Props
  16. Practices
  17. Naming Handle Methods
  18. Naming Events
  19. Using PropTypes
  20. Using Entities
  21. Gotchas
  22. Tables
  23. Add-ons
  24. ClassSet
  25. Other
  26. JSX
  27. ES6 Harmony
  28. react-rails
  29. rails-assets
  30. flux

Scope

This is how we write React.js on Rails. We've struggled to find the happy path. Recommendations here represent a good number of failed attempts. If something seems out of place, it probably is; let us know what you've found.

⬆ back to top


Component Organization

Group methods into logical groups.

  • propTypes
  • get methods
  • state methods
  • lifecycle events
  • event handlers
  • "private" methods
  • render
var Person = React.createClass({
  propTypes: {
    name: React.PropTypes.string
  },

  getInitialState() {
    return {
      smiling: false
    };
  },

  getDefaultProps() {
    return {
      name: 'Guest'
    };
  },

  componentWillMount() {
    // add event listeners (Flux Store, WebSocket, document, etc.)
  },

  componentDidMount() {
    // React.getDOMNode()
  },

  componentWillUnmount() {
    // remove event listeners (Flux Store, WebSocket, document, etc.)
  },

  handleClick() {
    this.setState({smiling: !this.state.smiling});
  },

  _doSomethingGross() {
    // These really aren't private but it's a sign the method could stand
    // improvement or has unideal implementation.
  },

  render() {
    return (
      <div
       className="Person"
       onClick={this.handleClick}>
        {this.props.name} {this.state.smiling ? "is smiling" : ""}
      </div>
    );
  },

});

Place get methods (computed props) after React's getInitialState and getDefaultProps.

Place has/is/can methods (compound state) after that, respectively.

var Person = React.createClass({
  getInitialState() {},
  getDefaultProps() {},
  getFormattedBirthDate() {},
  hasHighExpectations() {},
  isLikelyToBeDissapointedWithSurprisePartyEfforts() {}
});

⬆ back to top

Formatting Props

Wrap props on newlines for exactly 2 or more.

(Hint: Don't separate props with commas)

// bad
<Person
 firstName="Michael" />

// good
<Person firstName="Michael" />
// bad
<Person firstName="Michael" lastName="Chan" occupation="Designer" favoriteFood="Drunken Noodles" />

// good
<Person
 firstName="Michael"
 lastName="Chan"
 occupation="Designer"
 favoriteFood="Drunken Noodles" />

⬆ back to top


Computed Props

Name computed prop methods with the get prefix.

  // bad
  firstAndLastName() {
    return `${this.props.firstName} ${this.props.lastname}`;
  }

  // good
  getFullName() {
    return `${this.props.firstName} ${this.props.lastname}`;
  }

See: Cached State in render anti-pattern

⬆ back to top


Compound State

Name compound state methods with the is, has or can prefix.

// bad
happyAndKnowsIt() {
  return this.state.happy && this.state.knowsIt;
}
// good
isWillingSongParticipant() {
  return this.state.happy && this.state.knowsIt;
},

hasWorrysomeBehavior() {
  return !this.isWillingSongParticipant() && this.props.punchesKittens;
},

canPetKittens() {
  return this.hasHands() && this.props.kittens.length;
}

These methods should return a boolean value.

See: Compound Conditions anti-pattern

⬆ back to top

Sub-render

Use sub-render methods to isolate logical chunks of component UI.

// bad
render() {
  return <div>{this.props.name} {this.state.smiling ? "is smiling" : ""}</div>;
}
// good
renderSmilingStatement() {
  if (this.state.isSmiling) {
    return "is smiling";
  }

  return "";
},

render() {
  return <div>{this.props.name} {this.renderSmilingStatement()}</div>;
}

⬆ back to top

View Components

Compose components into views. Don't create one-off components that merge layout and domain components.

// bad
var PeopleWrappedInBSRow = React.createClass({
  render() {
    return (
      <div className="row">
        <People people={this.state.people} />
      </div>
    );
  }
});
// good
var BSRow = React.createClass({
  render() {
    return <div className="row">{this.props.children}</div>;
  }
});

var SomeView = React.createClass({
  render() {
    return (
      <BSRow>
        <People people={this.state.people} />
      </BSRow>
    );
  }
});

This works nicely for complex components—like Tabs or Tables—where you you might need to iterate over children and place them within a complex layout.

⬆ back to top

Container Components

A container does data fetching and then renders its corresponding sub-component. That's it. — Jason Bonta

Bad

// CommentList.js

var CommentList = React.createClass({
  getInitialState() {
    return { comments: [] }
  },

  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  },

  render() {
    return <ul> {this.state.comments.map(renderComment)} </ul>;
  },

  renderComment({body, author}) {
    return <li>{body}{author}</li>;
  }
});

Good

// CommentList.js

var CommentList = React.createClass({
  render() {
    return <ul> {this.props.comments.map(renderComment)} </ul>;
  },

  renderComment({body, author}) {
    return <li>{body}{author}</li>;
  }
});
// CommentListContainer.js

var CommentListContainer = React.createClass({
  getInitialState() {
    return { comments: [] }
  },

  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: function(comments) {
        this.setState({comments: comments});
      }.bind(this)
    });
  },

  render() {
    return <CommentList comments={this.state.comments} />;
  }
});

Read more
Watch more

⬆ back to top


Cached State in render

Do not keep state in render

// bad
render() {
  var name = 'Mrs. ' + this.props.name;
  return <div>{name}</div>;
}

// good
render() {
  return <div>{'Mrs. ' + this.props.name}</div>;
}
// best
getFancyName() {
  return `Mrs. ${this.props.name}`;
},

render() {
  return <div>{this.getFancyName()}</div>;
}

See: Computed Props pattern

⬆ back to top

Compound Conditions

Do not put compound conditions in render.

// bad
render() {
  return <div>{if (this.state.happy && this.state.knowsIt) { return "Clapping
hands" }</div>;
}
// better
isTotallyHappy() {
  return this.state.happy && this.state.knowsIt;
},

render() {
  return <div>{if (this.isTotallyHappy() { return "Clapping hands" }}</div>;
}
// betterer
getHappinessMessage() {
  if (this.isTotallyHappy()) {
    return "Clapping hands";
  }
},

isTotallyHappy() {
  return this.state.happy && this.state.knowsIt;
},

render() {
  return <div>{this.getHappinessMessage()}</div>;
}

The best solution for this would use a container component to manage state and pass new state down as props.

See: Compound State pattern

⬆ back to top

Existence Checking

Do not check existence of prop objects.

// bad
render() {
  if (this.props.person) {
    return <div>{this.props.person.firstName}</div>;
  } else {
    return null;
  }
}
// good
getDefaultProps() {
  return {
    person: {
      firstName: 'Guest'
    }
  };
},

render() {
  return <div>{this.props.person.firstName}</div>;
}

This is only where objects or arrays are used. Use PropTypes.shape to clarify the types of nested data expected by the component.

⬆ back to top

Setting State from Props

Do not set state from props without obvious intent.

// bad
propTypes: {
  items: React.PropTypes.array
},

getInitialState() {
  return {
    items: this.props.items
  };
}
// good
propTypes: {
  initialItems: React.PropTypes.array
},

getInitialState() {
  return {
    items: this.props.initialItems
  };
}

Read: "Props is getInitialState Is an Anti-Pattern"

⬆ back to top


Naming Handler Methods

Name the handler methods after their triggering event.

// bad
punchABadger() {},

render() {
  return <div onClick={this.punchABadger}> ... </div>;
}
// good
handleClick() {},

render() {
  return <div onClick={this.handleClick}> ... </div>;
}

Handler names should:

  • begin with handle
  • end with the name of the event they handle (eg, Click, Change)
  • be present-tense

If you need to disambiguate handlers, add additional information between handle and the event name. For example, you can distinguish between onChange handlers: handleNameChange and handleAgeChange. If you do this, ask yourself if you should create another component class.

⬆ back to top

Naming Events

Use custom event names for components Parent-Child event listeners.

var Parent = React.createClass({
  handleCry() {
    // handle Child's cry
  },

  render() {
    return (
      <div className="Parent">
        <Child onCry={this.handleCry} />
      </div>
    );
  }
});

var Child = React.createClass({
  render() {
    return (
      <div
       className="Child"
       onChange={this.props.onCry}>
        ...
      </div>
    );
  }
});

⬆ back to top

Using PropTypes

Use PropTypes to communicate expectations and log meaningful warnings.

var MyValidatedComponent = React.createClass({
  propTypes: {
    name: React.PropTypes.string
  },

  ...
});

This component will log a warning if it receives name of a type other than string.

<Person name=1337 />
// Warning: Invalid prop `name` of type `number` supplied to `MyValidatedComponent`, expected `string`.

Components may require props

var MyValidatedComponent = React.createClass({
  propTypes: {
    name: React.PropTypes.string.isRequired
  },

  ...
});

This component will now validate the presence of name.

<Person />
// Warning: Required prop `name` was not specified in `Person`

Read: Prop Validation

⬆ back to top

Using Entities

Use React's String.fromCharCode() for special characters.

// bad
<div>PiCO · Mascot</div>

// nope
<div>PiCO &middot; Mascot</div>

// good
<div>{'PiCO ' + String.fromCharCode(183) + ' Mascot'}</div>

// better
<div>{`PiCO ${String.fromCharCode(183)} Mascot`}</div>

Read: JSX Gotchas

⬆ back to top

Tables

The browser thinks you're dumb. But React doesn't. Always use tbody in your table components.

// bad
render() {
  return (
    <table>
      <tr>...</tr>
    </table>
  );
}

// good
render() {
  return (
    <table>
      <tbody>
        <tr>...</tr>
      </tbody>
    </table>
  );
}

The browser is going to insert tbody if you forget. React will continue to insert new trs into the table and confuse the heck out of you. Always use tbody.

⬆ back to top

classSet

NOTE: the classSet addon has been deprecated. Use classNames instead on NPM and Bower.

Use classNames() to manage conditional classes in your app:

// bad
render() {
  var classes = ['MyComponent'];
  if (this.state.active) {
    classes.push('MyComponent--active');
  }

  return <div className={classes.join(' ')} />;
},

getClassName() {
  var classes = ['MyComponent'];
  if (this.state.active) {
    classes.push('MyComponent--active');
  }
  return classes.join(' ');
}

// good
render() {
  var classes = {
    'MyComponent': true,
    'MyComponent--active': this.state.active
  };

  <div className={classNames(classes)} />;
}

Read: Class Name Manipulation

⬆ back to top

JSX

We used to have some hardcore CoffeeScript lovers is the group. The unfortunate thing about writing templates in CoffeeScript is that it leaves you on the hook when certain implementations change that JSX would normally abstract.

We no longer recommend using CoffeeScript to write templates.

For posterity, you can read about how we used CoffeeScript for templates, when using CoffeeScript was non-negotiable: CoffeeScript and JSX.

⬆ back to top

ES6 Harmony

These examples use the harmony option on react-rails for ES6/ES2015 sugar. Examples use the createClass API over React.Component for consistency with the official documentation.

ES6 implementation in jstransform is limited.

⬆ back to top

react-rails

react-rails should be used in all Rails apps that use React. It provides the perfect amount of glue between Rails conventions and React.

⬆ back to top

rails-assets

rails-assets should be considered for bundling js/css assets into your applications. The most popular React-libraries we use are registered on Bower and can be easily added through Bundler and react-assets.

caveats: rails-assets gives you access to bower projects via Sprockets requires. This is a win for the traditionally hand-wavy approach that Rails takes with JavaScript. This approach does buy you modularity or the ability to interop with JS tooling that requires modularity.

⬆ back to top

flux

Use Alt for flux implementation. Alt is true to the flux pattern with the best documentation available.

⬆ back to top

About

How we write React

License:MIT License