React Tips & Tricks

React Tips & Tricks

Some things, I learned during an React migration, we are doing at ProductHunt.

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

May 20, 2015
Tweet

Transcript

  1. React Tips & Tricks Radoslav Stankov 20/05/2015

  2. Radoslav Stankov @rstankov http://rstankov.com http://github.com/rstankov

  3. None
  4. None
  5. <div data-component="Graph" data-series="<% data.to_json %>"> <div class="legend"></div> <div class="graph"></div> </div>

  6. app.component('Graph', function() { elements: { legend: '.legend', graph: '.graph' },

    wasInitialized: function() { # ... using some js graph lib } }
  7. <div data-component="InfiniteScrolling"> <!—- code —-> <div data-scroll-marker="true">Hunting down more products...</div>

    </div>
  8. app.component('InfiniteScrolling', function() { elements: { marker: '[data-scroll-marker]' }, events: {

    'scroll:bottom': 'loadMore' }, wasInitialized: function() { // ... code code }, wasRemoved: function() { // ... be good and clean after your self } });
  9. None
  10. None
  11. None
  12. Search Field Location Search Bar Search Model Search Result: Products

    Search Result: Collections Search Result: Users Search Index: Users Search Index: Products Search Index: Collections
  13. None
  14. • I don’t want to develop my framework • I

    want module system • I want better "View" layer • I want structure and rules • I want a stable framework • I want to get rid of html mess
  15. None
  16. None
  17. None
  18. None
  19. None
  20. None
  21. None
  22. • Close to what already had • Really easy server

    side rendering • Plays great with ES6 • Virtual DOM • JSX • Data direction • Flux
  23. None
  24. None
  25. None
  26. None
  27. <div className="post-list"> <article className="post-item"> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div>

    <button className="comment-button">{post.commentsCount}</button> </article> <article className="post-item"> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> <!—- more posts —-> </div>
  28. var PostList = React.createClass({ render: function() { var posts =

    this.props.posts.map(function(post) { return ( React.createElement("article", {className: "post-item"}, React.createElement("button", {className: "vote-button"}, post.votesCount), React.createElement("div", null, React.createElement("h1", null, post.name), React.createElement("p", null, post.headline) ), React.createElement("button", {className: "comment-button"}, post.commentsCount) ) ); }); return ( React.createElement("div", {className: "post-list"}, posts) ) } });
  29. Can’t tell if it is true or just trolling

  30. var PostList = React.createClass({ render: function() { var posts =

    this.props.posts.map(function(post) { return ( <article className="post-item"> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> ); }); return ( <div className="post-list"> {posts} </div> ) } });
  31. STILL can’t tell if it is true or just trolling

  32. JSX <article className="post-item"> <VoteButton /> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <CommentButton

    /> </article>
  33. http://webpack.github.io

  34. var PostList = React.createClass({ render: function() { var posts =

    this.props.posts.map(function(post) { return ( <article className="post-item"> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> ); }); return ( <div className="post-list"> {posts} </div> ) } });
  35. Render

  36. var PostList = React.createClass({ render: function() { var posts =

    this.props.posts.map(function(post) { return ( <article className="post-item"> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> ); }); return ( <div className="post-list"> {posts} </div> ) } });
  37. var PostList = React.createClass({ render: function() { return ( <div

    className="post-list"> {this.renderItems()} </div> ) }, renderItems: function () { return this.props.posts.map(function(post) { return ( <article className="post-item" key={'post-' + post.id}> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> ); }); } });
  38. Code conventions renderItem
 renderEmpty renderUploadProgress 
 render[Something]


  39. var PostList = React.createClass({ render: function() { return ( <div

    className="post-list"> {this.renderItems()} </div> ) }, renderItems: function () { return this.props.posts.map(function(post) { return ( <article className="post-item" key={'post-' + post.id}> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> ); }); } });
  40. var PostList = React.createClass({ propTypes: { posts: React.PropTypes.array.isRequired }, getDefaultProps:

    function() { return { posts: [] }; }, render: function() { return ( <div className="post-list"> {this.renderItems()} </div> ) }, renderItems: function () { return this.props.posts.map(function(post) { return ( <article className="post-item" key={'post-' + post.id}> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> ); }); } });
  41. Prop validation React.createClass({ propTypes: { optionalArray: React.PropTypes.array, optionalBool: React.PropTypes.bool, optionalFunc:

    React.PropTypes.func, optionalNumber: React.PropTypes.number, optionalObject: React.PropTypes.object, optionalString: React.PropTypes.string, requiredString: React.PropTypes.string.isRequired, requiredObject: React.PropTypes.shape({ optionalString: React.PropTypes.string, requiredString: React.PropTypes.string.isRequired, }) // more information: // https://facebook.github.io/react/docs/reusable-components.html });
  42. var PostList = React.createClass({ render: function() { return ( <div

    className="post-list"> {this.renderItems()} </div> ) }, renderItems: function () { return this.props.posts.map(function(post) { return ( <article className="post-item" key={'post-' + post.id}> <button className="vote-button">{post.votesCount}</button> <div> <h1>{post.name}</h1> <p>{post.headline}</p> </div> <button className="comment-button">{post.commentsCount}</button> </article> ); }); } });
  43. None
  44. None
  45. Components React is all about building reusable components. In fact,

    with React the only thing you do is build components. Since they're so encapsulated, components make code reuse, testing, and separation of concerns easy.
  46. var PostList = React.createClass({ render: function() { return ( <div

    className="post-list"> {this.renderItems()} </div> ) }, renderItems: function () { return this.props.posts.map(function(post) { return <PostItem key={'post-' + post.id} {...post} /> }); } });
  47. var PostItem = React.createClass({ render: function() { return ( <article

    className="post-item"> <button className="vote-button">{this.props.votesCount}</button> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <button className="comment-button">{this.props.commentsCount}</button> </article> ); } });
  48. var PostItem = React.createClass({ propTypes: { votesCount: React.PropTypes.number.isRequired, commentsCount: React.PropTypes.number.isRequired,

    name: React.PropTypes.string.isRequired, headline: React.PropTypes.string.isRequired }, // code ... });
  49. None
  50. var PostItem = React.createClass({ getInitialState: function() { return { hasVoted:

    Votes.hasVoted(this.props.id) }; }, render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return ( <article className="post-item"> <button className={className}>{this.props.votesCount}</button> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <button className="comment-button">{this.props.commentsCount}</button> </article> ); } });
  51. Props vs State

  52. None
  53. var PostItem = React.createClass({ getInitialState: function() { return { hasVoted:

    Votes.hasVoted(this.props.id), votesCount: this.props.votesCount, }; }, render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return ( <article className="post-item"> <button className={className} onClick={this.handleVoteClick}>{this.state.votesCount}</button> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <button className="comment-button">{this.props.commentsCount}</button> </article> ); }, handleVoteClick: function () { var voted = Votes.toggleVote(this.props.id); this.setState({ hasVoted: voted, votesCount: this.state.votesCount + (voted ? 1 : -1) }); } });
  54. Code conventions handlePostChange
 handleVoteToggle handleClick 
 handle[Something]


  55. var PostItem = React.createClass({ getInitialState: function() { return { hasVoted:

    Votes.hasVoted(this.props.id), votesCount: this.props.votesCount }; }, componentDidMount: function() { Votes.onVoteChange(this.props.id, this.handleVotesUpdate); }, componentWillUnmount: function () { Votes.offVoteChange(this.props.id, this.handleVotesUpdate); }, render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return ( <article className="post-item"> <button className={className} onClick={this.handleVoteClick}>{this.state.votesCount}</button> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <button className="comment-button">{this.state.commentsCount}</button> </article> ); }, handleVotesUpdate: function(count) { this.setState({ hasVoted: Votes.hasVoted(this.props.id), votesCount: count });
  56. Votes.onVoteChange(this.props.id, this.handleVotesUpdate); }, componentWillUnmount: function () { Votes.offVoteChange(this.props.id, this.handleVotesUpdate); },

    render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return ( <article className="post-item"> <button className={className} onClick={this.handleVoteClick}>{this.state.votesCount}</button> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <button className="comment-button">{this.state.commentsCount}</button> </article> ); }, handleVotesUpdate: function(count) { this.setState({ hasVoted: Votes.hasVoted(this.props.id), votesCount: count }); }, handleVoteClick: function () { Votes.toggleVote(this.props.id); } });
  57. var PostItem = React.createClass({ getInitialState: function() { return { hasVoted:

    Votes.hasVoted(this.props.id), votesCount: this.props.votesCount, commentsCount: this.props.commentsCount }; }, componentDidMount: function() { Comment.observe(this.props.id, this.handleCommentsUpdate); Votes.onVoteChange(this.props.id, this.handleVotesUpdate); }, componentWillUnmount: function () { Comment.stopObserving(this.props.id, this.handleCommentsUpdate); Votes.offVoteChange(this.props.id, this.handleVotesUpdate); }, render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return ( <article className="post-item"> <button className={className} onClick={this.handleVoteClick}>{this.state.votesCount}</button> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <button className="comment-button">{this.state.commentsCount}</button> </article> ); }, handleCommentsUpdate: function (count) { this.setState({
  58. render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return

    ( <article className="post-item"> <button className={className} onClick={this.handleVoteClick}>{this.state.votesCount}</button> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <button className="comment-button">{this.state.commentsCount}</button> </article> ); }, handleCommentsUpdate: function (count) { this.setState({ commentsCount: count }); }, handleVotesUpdate: function(count) { this.setState({ hasVoted: Votes.hasVoted(this.props.id), votesCount: count }); }, handleVoteClick: function () { var voted = Votes.toggleVote(this.props.id); } });
  59. Virtual DOM

  60. Virtual Dom React Element React Element React Element React Element

    React Element
  61. Virtual Dom DOM Element DOM Element DOM Element DOM Element

    DOM Element
  62. Virtual Dom React Element React Element React Element React Element

    React Element
  63. Virtual Dom React Element React Element React Element React Element

    React Element
  64. Virtual Dom React Element React Element React Element React Element

    React Element
  65. Virtual Dom React Element React Element React Element React Element

    React Element React Element React Element React Element React Element React Element
  66. Virtual Dom React Element React Element React Element React Element

    React Element React Element React Element React Element React Element React Element DOM Element DOM Element DOM Element DOM Element DOM Element
  67. Virtual Dom React Element React Element React Element React Element

    React Element React Element React Element React Element React Element React Element DOM Element DOM Element DOM Element DOM Element DOM Element DOM Element DOM Element DOM Element DOM Element DOM Element
  68. Virtual Dom React Element React Element React Element React Element

    React Element
  69. Virtual Dom div article article button div button h1 p

  70. Virtual Dom PostList PostItem PostItem button div button h1 p

  71. Virtual Dom PostList PostItem PostItem button div button h1 p

  72. Virtual Dom PostList PostItem PostItem button div button h1 p

  73. Virtual Dom PostList PostItem PostItem Vote Button div Comment Button

    h1 p
  74. Virtual Dom PostList PostItem PostItem Vote Button div Comment Button

    h1 p
  75. None
  76. None
  77. var PostItem = React.createClass({ render: function() { return ( <article

    className="post-item"> <VoteButton {...this.props} /> <div> <h1>{this.props.name}</h1> <p>{this.props.headline}</p> </div> <CommentButton {...this.props} /> </article> ); } });
  78. var CommentButton = React.createClass({ propTypes: { id: React.PropTypes.number.isRequired, commentsCount: React.PropTypes.number.isRequired,

    }, getInitialState: function() { return { commentsCount: this.props.commentsCount }; }, componentDidMount: function() { Comment.observe(this.props.id, this.handleUpdate); }, componentWillUnmount: function () { Comment.stopObserving(this.props.id, this.handleUpdate); }, render: function() { return <button className="comment-button">{this.state.commentsCount}</button>; }, handleUpdate: function (count) { this.setState({ commentsCount: count }); } });
  79. var VoteButton = React.createClass({ getInitialState: function() { return { hasVoted:

    Votes.hasVoted(this.props.id), votesCount: this.props.votesCount, }; }, componentDidMount: function() { Votes.onVoteChange(this.props.id, this.handleUpdate); }, componentWillUnmount: function () { Votes.offVoteChange(this.props.id, this.handleUpdate); }, render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return <button className={className} onClick={this.handleClick}>{this.state.votesCount}</button> }, handleUpdate: function(count) { this.setState({ hasVoted: Votes.hasVoted(this.props.id), votesCount: count }); }, handleClick: function () { var voted = Votes.toggleVote(this.props.id); } });
  80. components/PostList.js components/PostItem.js components/VoteButton.js components/CommentButton.js

  81. components/PostList/index.js (List.js) components/PostList/Item.js components/PostList/VoteButton.js components/PostList/CommentButton.js

  82. // components/PostsList/index.js import Item from './Item'; export default React.createClass({ //

    ... code }); // components/PostsList/Item.js
 // ... other imports import VoteButton from './VoteButton'; import CommentButton from './CommentButton'; export default React.createClass({ // ... code }); // components/PostsList/VoteButton.js // ... other imports export default React.createClass({ // ... code }); // components/PostsList/CommentButton.js // ... other imports export default React.createClass({ // ... code });
  83. // components/index.js import PostList from 'PostList/';

  84. FLUX

  85. import React from 'react'; import classNames from 'classnames'; import Votes

    from '../../lib/Votes.js'; export default React.createClass({ getInitialState: function() { return this.getStateFromStore(); }, getStateFromStore: function() { return { hasVoted: VotesStore.hasVoted(this.props.id), votesCount: VotesStore.voteCount(this.props.id, this.props.votesCount) } }, componentDidMount: function() { VotesStore.addChangeListener(this.handleStoreChange); }, componentWillUnmount: function () { VotesStore.removeChangeListener(this.handleStoreChange); }, handleStoreChange: function() { this.setState(this.getStateFromStore()); }, render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return <button className={className} onClick={this.handleClick}>{this.state.votesCount}</button> }, handleClick: function () {
  86. import Votes from '../../lib/Votes.js'; export default React.createClass({ getInitialState: function() {

    return this.getStateFromStore(); }, getStateFromStore: function() { return { hasVoted: VotesStore.hasVoted(this.props.id), votesCount: VotesStore.voteCount(this.props.id, this.props.votesCount) } }, componentDidMount: function() { VotesStore.addChangeListener(this.handleStoreChange); }, componentWillUnmount: function () { VotesStore.removeChangeListener(this.handleStoreChange); }, handleStoreChange: function() { this.setState(this.getStateFromStore()); }, render: function() { var className = classNames('vote-button', {active: this.state.hasVoted}); return <button className={className} onClick={this.handleClick}>{this.state.votesCount}</button> }, handleClick: function () { VoteActionCreators.toggleVote(this.props.id, this.state.votesCount); } });
  87. import React from 'react';
 import classNames from 'classnames'; import VotesStore

    from '../../stores/VotesStore'; import VoteActionCreators from '../../actions/VoteActionCreators'; export default React.createClass({ propTypes: { id: React.PropTypes.number.isRequired, votesCount: React.PropTypes.number.isRequired, }, getInitialState() { return this.getStateFromStore(); }, getStateFromStore() { return { hasVoted: VotesStore.hasVoted(this.props.id), votesCount: VotesStore.voteCount(this.props.id, this.props.votesCount) } }, componentDidMount() { VotesStore.addChangeListener(this.handleStoreChange); }, componentWillUnmount() { VotesStore.removeChangeListener(this.handleStoreChange); }, handleStoreChange() { this.setState(this.getStateFromStore()); }, render() {
  88. votesCount: React.PropTypes.number.isRequired, }, getInitialState() { return this.getStateFromStore(); }, getStateFromStore() {

    return { hasVoted: VotesStore.hasVoted(this.props.id), votesCount: VotesStore.voteCount(this.props.id, this.props.votesCount) } }, componentDidMount() { VotesStore.addChangeListener(this.handleStoreChange); }, componentWillUnmount() { VotesStore.removeChangeListener(this.handleStoreChange); }, handleStoreChange() { this.setState(this.getStateFromStore()); }, render() { const className = classNames('vote-button', {active: this.state.hasVoted}); return <button className={className} onClick={this.handleClick}>{this.state.votesCount}</button> }, handleClick() { VoteActionCreators.toggleVote(this.props.id, this.state.votesCount); } });
  89. // Sample Votes Store import {EventEmitter} from 'events'; import Dispatcher

    from '../dispatcher'; import {VOTE_ADDED, VOTE_REMOVED} from '../constants/VotesStoreConstants'; var state = {}; var emitter = new EventEmitter(); Dispatcher.register((payload) => { switch(payload.action) { case VOTE_ADDED: // update state emitter.emit(CHANGE_EVENT); break; case VOTE_REMOVED: // update state emitter.emit(CHANGE_EVENT); break; } } }); const CHANGE_EVENT = Symbol('CHANGE_EVENT'); default export { hasVoted(postId) { /* return state */ } voteCount(postId) { /* return state */ } addChangeListener(cb) { emitter.addListener(CHANGE_EVENT, cb); } removeChangeListener(cb) { emitter.removeListener(CHANGE_EVENT, cb); } };
  90. // Sample Votes Store import Store from './Store'; import {VOTE_ADDED,

    VOTE_REMOVED} from '../constants/VotesStoreConstants'; var state = {}; default export Store({ hasVoted(postId) { /* return state */ } voteCount(postId) { /* return state */ } handleDispatch(payload, emit) { switch(payload.action) { case VOTE_ADDED: // update state emit(); break; case VOTE_REMOVED: // update state emit(); break; } } });
  91. // constants/VotesStoreConstants.js export const VOTE_ADDED = Symbol('VOTE_ADDED'); export const VOTE_REMOVED

    = Symbol('VOTE_REMOVED');
  92. // Sample Votes Actions
 import AppDispatcher from '../dispatcher'; import {VOTE_ADDED,

    VOTE_REMOVED} from '../constants/VotesStoreConstants'; import VoteAPIUtils from '../utils/VoteAPIUtils'; import VotesStore from '../stores/VotesStore'; export default { toggleVote: function(postId, currentCount) { if (VotesStore.hasVoted(postId)) { VoteAPIUtils.remove(postId); AppDispatcher.dispatch({ action: VOTE_REMOVED, postId: postId, count: currentCount - 1 }); } else { VoteAPIUtils.remove(postId); AppDispatcher.dispatch({ action: VOTE_ADDED, postId: postId, count: currentCount + 1 }); } } };
  93. None
  94. Overview

  95. • ES6 • Naming conventions • Property validation • File

    Organization • Small components • Generic components • Flux • Controlled experiments
  96. https://speakerdeck.com/rstankov/react-tips-and-tricks

  97. https://github.com/rstankov/talks-code

  98. None
  99. None
  100. @rstankov Thanks :)

  101. Questions?