An introductory talk to React.js for Rails developers. Covers high-level concepts, implementation, and languages. Includes all speaker notes and audience questions.
lottery being able to talk about JavaScript at a Ruby conference. But there’s actually been a lot of talk about JavaScript at This RailsConf. Talk of pepper backpacks, the zombie apocalypse, and whether or not JavaScript belongs in your zombie apocalypse pepper backpack. I’d like to talk with you about how we tackle client-side code and how React has helped us write better Rails apps.
and a browser client. But we discovered a new host of problems: 2 sets of everything. 2 models, controllers, routers, validations, these efforts are all doubled. And you don’t realize just how good form_for is until you no longer have it.
gave up, and said just search for “hard refresh”. We had come full circle, that the best way for us to deliver reliable data to our users was to refresh their browser when they came to critical pages. This is insanity.
T I L L W R I T I N G A L O T O F J AVA S C R I P T But we were still writing a lot of JavaScript. SRJ is great when you interfaces justify an event that should be handled by a Rails controller. But they’re not so good for transient state or pages that are update (top- down) at a regular clip.
real-time and interface with native apps through a single Rails API. We’d like for these to work consistently on nothing more than the data we are sending those devices. We want something that scales well to both of our needs, small and large.
U L D S H I P A R E A C T C O M P O N E N T I N A N H O U R First, that you could walk out of here and ship a React component in an hour. 5 minutes to setup. 55 minutes to write code.
C E I V E P R O P S • M A I N TA I N S TAT E C O M P O N E N T S D O 3 T H I N G S : * render * receive props (these can be passed from component to component or a rails view) * maintain application state
end %> </ul> • Welcome To New York • Blank Space • Style • Out of the Woods • Shake It Off • I Wish You Would • Bad Blood • Wildest Dreams • How You Get the Girl • This Love • I know Places • Clean These are the tracks from Taylor Swift’s 1989. I may or may not have completed this list from memory.
%> <li><%= song.name %></li> <% end %> </ul> _songs.html.erb This is where locals come in. If we expect a `songs` variable from the view, we can sidestep this coupling.
_songs.html.erb <ul> <% songs.each do |song| %> <li><%= song.name %></li> <% end %> </ul> Now, if we want to use this partial on the index page, we can.
_songs.html.erb <ul> <% songs.each do |song| %> <li><%= song.name %></li> <% end %> </ul> 1989 • Welcome To New York • Blank Space • Style • Out of the Woods • Shake It Off • I Wish You Would • Bad Blood • Wildest Dreams • How You Get the Girl • This Love • I know Places • Clean RED • State Of Grace • Red • Treacherous • I Knew You Were Trouble • All Too Well • 22 • I Almost Do • We Are Never Ever Getting Back Together • Stay Stay Stay • The Last Time • … Speak Now • Mine • Sparks Fly • Back To December • Speak Now • Dear John • Mean • The Story Of Us • Never Grow Up • Enchanted • Better Than Revenge • Innocent • Haunted • Last Kiss Now, if we want to use this partial on the index page, we can.
partial: "songs", locals: { songs: album.songs } %> <% end %> index.html.erb <%= render partial: "songs", locals: { songs: @album.songs } %> This works even though the context is different in each view.
M U TA B L E * kinda In Yehuda’s Rust talk, he described ownership is the right to modify or destroy. In React, a component does not own props. It only has access to see and use that value. JavaScript obviously has now way to protect these values but you should consider them to be immutable.
end %> </ul> var Songs = React.createClass({ render() { return <ul>{this.props.songs.map()}</ul>; } }); and iterates over it. in ruby we can just each over this list. in React, we need the array. so we map().
end %> </ul> var Songs = React.createClass({ render() { var createItem = () => return <ul>{this.props.songs.map(createItem)}</ul>; } }); for each iteration, we run a function to create a new song <li />.
=> ; return <ul>{this.props.songs.map(createItem)}</ul>; } }); <ul> <% songs.each do |song| %> <li><%= song.name %></li> <% end %> </ul> We take each song,
=> <li>{song.name}</li>; return <ul>{this.props.songs.map(createItem)}</ul>; } }); <ul> <% songs.each do |song| %> <li><%= song.name %></li> <% end %> </ul> and return a <li />, which spits out the songs’ name.
I C E ! So, that’s our primer. You learned how to: * render components from rails views * define a component * and the similarities components have to Rails partials
O M M E N T S F O R T H E 1 5 - M I N U T E B L O G I’d like to show you how to build out React components in practice. Let’s use an app that every Rails developer knows, the 15-minute blog.
}); <% @post.comments.each do |comment| %> <%= react_component "Comment", { comment: comment.body } %> <% end %> and use our react_component helper to render the component `Comment`.
}); <% @post.comments.each do |comment| %> <%= react_component "Comment", { comment: comment.body } %> <% end %> We dutifully provide our component a `comment` prop, which comes from the `comment` relationship on `@post`.
comment.body } %> <% end %> comments.js.jsx var Comments = React.createClass({ render() { var createItem = ({body}) => <Comment comment={body} />; return <ul>{this.props.comments.map(createItem)}</ul>; } }); which is a React class,
comment.body } %> <% end %> comments.js.jsx var Comments = React.createClass({ render() { var createItem = ({body}) => <Comment comment={body} />; return <div>{this.props.comments}</div>; } }); and interpolates the value of `comments` from the `props` object.
comment.body } %> <% end %> comments.js.jsx var Comments = React.createClass({ render() { var createItem = ({body}) => <Comment comment={body} />; return <div>{this.props.comments.map()}</div>; } }); We iterate over each `comment` in `comments` with map(),
() => <Comment comment={body} />; return <div>{this.props.comments.map(createItem)}</div>; } }); <% @post.comments.each do |comment| %> <%= react_component "Comment", { comment: comment.body } %> <% end %> and create an element for each comment,
comment.body } %> <% end %> comments.js.jsx var Comments = React.createClass({ render() { var createItem = ({body}) => <Comment comment={body} />; return <div>{this.props.comments.map(createItem)}</div>; } }); We take the comment, restructure out the `body` member (using es6 destructuring).
comment.body } %> <% end %> comments.js.jsx var Comments = React.createClass({ render() { var createItem = ({body}) => <Comment />; return <div>{this.props.comments.map(createItem)}</div>; } }); And render the <Comment /> component for each. NOTE: When we render a component, from within a component, we can use this HTML-like syntax,
comment.body } %> <% end %> comments.js.jsx var Comments = React.createClass({ render() { var createItem = ({body}) => <Comment comment={body} />; return <div>{this.props.comments.map(createItem)}</div>; } }); We’ll give our component the comment boy as the comment.
({body}) => <Comment comment={body} />; return <div>{this.props.comments.map(createItem)}</div>; } }); <%= react_component "Comments", { comments: @post.comments } %> and update our helper to render the <comments /> component. We then send it `@post.comments` as `comments`.
E N T S • W H I C H C O M P O N E N T R E N D E R S T H E M < C o m m e n t s C o n t a i n e r / > C O M P O N E N T This component does two things. * It knows how to fetch new comments * It knows with comment is capable of rendering those comments
fetchComments() { $.getJSON( this.props.commentsPath, (data) => this.setState({comments: data}); ); }, getInitialState() { return { comments: [] }; }, render() { return <Comments comments={this.state.comments} />; } }); And this function simply provides an initial starting value you. When we are doing async operations for data, it’s important to set starting data so you initial render doesn't spit undefined to the other components.
return <Comments comments={this.state.comments} />; } }); We’ll also take advantage of the lifecycle event `getInitialState`. When fetching asynchronous data, it’s important to provide our component with a safe initial-render state. We don’t want to send undefined through to our other components.
=> this.setState({comments: data}); ); }, getInitialState() { return { comments: [] }; }, render() { return <Comments comments={this.state.comments} />; } This is our function for fetching comments from the server.
=> this.setState({comments: data}); ); }, getInitialState() { return { comments: [] }; }, render() { return <Comments comments={this.state.comments} />; } It uses the `commentsPath` that we passed in through `props`.
=> this.setState({comments: data}); ); }, getInitialState() { return { comments: [] }; }, render() { return <Comments comments={this.state.comments} />; } on success, we take the data and call `setState` and apply the new `data` to `comments`.
=> this.setState({comments: data}); ); }, getInitialState() { return { comments: [] }; }, render() { return <Comments comments={this.state.comments} />; } This function, is where the React magic happens. When we call this function, with new data, it’s going to re-render the entire component tree.
{ $.getJSON( this.props.commentsPath, (data) => this.setState({comments: data}); ); }, getInitialState() { return { comments: [] }; }, When the component is ready to go, we want to fetch the current comments. We can use the lifecycle event `componentWillMount` do run this function when our component is instantiated.
new comments through a standard Rails form and the second is polling for comment as they come in. As I mentioned early, calling setState with new data is triggers an re-render of the entire component tree. But that’s not what we’re seeing in the DOM. We’re only see single inserts for new comments. I haven’t told my component how to do this. I just told it what I want that group of comments to look. React only re-renders in memory. It then runs the minimal number of change operations no get the DOM up to date with the in-memory representation of our app.
new comments through a standard Rails form and the second is polling for comment as they come in. As I mentioned early, calling setState with new data is triggers an re-render of the entire component tree. But that’s not what we’re seeing in the DOM. We’re only see single inserts for new comments. I haven’t told my component how to do this. I just told it what I want that group of comments to look. React only re-renders in memory. It then runs the minimal number of change operations no get the DOM up to date with the in-memory representation of our app.
O Y O U N E E D T O K N O W T O B E E F F E C T I V E ? I just ran you through 5 APIs pretty quick. You’re probably wondering how many APIs you need to know to be affective. Let’s look at the ones we’ve seen and a couple we don’t yet know.
send it a number (or any other time), React will log out a warning telling you where the problem lies and how to fix it. This is a great way to communicate design expectations between team members.
send it a number (or any other time), React will log out a warning telling you where the problem lies and how to fix it. This is a great way to communicate design expectations between team members.
only DOM element changing is the innerHTML of the clock. This is because react is ridiculously effective in providing the fewest number of DOM mutations required to bring it up to date.
only DOM element changing is the innerHTML of the clock. This is because react is ridiculously effective in providing the fewest number of DOM mutations required to bring it up to date.
G E M * Provides the `react_component` helper * Uses UJS to correctly mount and unmount components * Turbolinks safe * Created by Facebook * Is now 1.0
gem 'byebug' gem 'web-console', '~> 2.0' gem 'spring' end gem 'react-rails', '~> 1.0' source 'https://rails-assets.org' do gem 'rails-assets-alt' gem 'rails-assets-react-router' gem 'rails-assets-moment' end R A I L S - A S S E T S This is like magic. You * add rails-assets as a source * you prefix the bower libraries with `rails-assets-` and it will make first-class gems from those bower libraries. It’s terrific.
H E A S S E T P I P E L I N E Our mistake was jumping on NPM too early. Adding browserify-rails and NPM can be slow and feels like it sidesteps The Rails Way.
{ return ( <div> <div>Name: {this.props.name}</div> </div> ); } }); J S X So, this is JSX. It’s an XML-like syntax for writing templates that looks like HTML. People like to make a big deal out of it. It’s not a big deal. It works the same way ERB, Haml, and Handlebars do.
{ return React.createElement("div", null, React.createElement("div", null, "Name: " + {this.props.name} ); ); } }); J S X react-rails handles all of the transformations.
<div>Name: {this.props.name}</div> </div> ` C O F F E E S C R I P T We also have to use `this` instead of @ inside JSX * We spent 6 months building React in CoffeeScript and now we no longer do so. * React uses JSX to hide some of the implementation details of React. If you use JSX, version updates are trivial. If you use a third party library, you can be stuck waiting for new features, and find yourself taking the brunt of the. * For this reason, we didn’t really like the alternatives of CJSX or coffee-react. JSX THIS IS WHERE REACT HIDES IT’S IMPLEMENTATION DETAILS. The hardest updates we had were completely the fault of our early decision to sidestep JSX.
class Greeting extends React.Component { render() { return ( <div> <div>{this.props.comment}</div> </div> ); } } Greeting.propTypes = { comment: React.PropTypes.string }; ES6. This is how we write all of our new components and how I’d recommend going forward. More and more react examples are being written in this way.
{ render() { return ( <div> <div>{this.props.comment}</div> </div> ); } } Greeting.propTypes = { comment: React.PropTypes.string.isRequired }; The biggest difference is that you define your components as a class, extending `React.Componont`.
{ render() { return ( <div> <div>{this.props.comment}</div> </div> ); } } Greeting.propTypes = { comment: React.PropTypes.string.isRequired }; propTypes are moved out of the definition, as a constructor property.
class Greeting extends React.Component { getInitialState() { return {foo: "bar"} } render() { return ( <div> <div>{this.props.comment}</div> </div> ); } } Greeting.propTypes = { React’s has terrific errors. You could seriously learn a thing or two about copywriting from this error.
class Greeting extends React.Component { getInitialState() { return {foo: "bar"} } render() { return ( <div> <div>{this.props.comment}</div> </div> ); } } Greeting.propTypes = { React’s has terrific errors. You could seriously learn a thing or two about copywriting from this error.
E T A G A I N S T J S X Our biggest mistake was betting against JSX. The most painful parts of our upgrades have been do to sidestepping JSX. JSX is where React hides its implementation details.
2 0 1 5 + J S X Use es6/es2015 or whatever it’s called this week. Support in react-rails is partial, currently. But it will be improving shortly, as the library moves to using the babel transpiler.
AVA S C R I P T A S Y O U N E E D We’re very happy with React in providing us the ability to write only as much JavaScript as we need. I want to leave you with the shape your apps take with different approaches to JavaScript.
apps use the library Alt. It is incredibly documented, which is great for teams like ours. It’s available an Bower/rails-assets. We try very hard to use the container pattern with for our components. This makes it very easy to use shored components in isolation or inside of a framework like Alt.
like browserify-rails. Practically, browserify-rails dramatically slowed down our apps in development. We weren’t using the interoperability of js modules enough to justify the loss in development speed. We’re basically waitng for something better to come along.
pretty thin layer around Jasmine, which is great if you know Jasmine. The React library ships with testing utilities, which allows you to work with any testing tool you have comfort with.