This post serves as a Rails / React primer for when you've got an existing rails application that you'd like to add React to. For this exercise I'm going to use a Rails form as an example of what can be done in a React component.
Now, Rails forms (especially when paired with simple-form
and cocoon
) are incredibly powerful, and typically don't need any JavaScript interference. In fact, the particular example I'm using would be ridiculous to write in the real world, and only a crazy person would use React for something so easy in Rails.
However, this example reveals something pretty damn powerful when pairing the two together. Often times complex UIs require you to manipulate Rails forms in very dirty ways, resulting in thousands of lines of JavaScript — I'll list some real-world examples towards below of how React can prevent this.
What I will be covering
- Installing
react-rails
with the existing asset pipeline - Creating our first React component
- Converting a Rails form into a React component
What I will not be covering
- Creating a single page app with React
- Using Rails as an API for a React front end
- AJAX requests
- Managing state
Creating a Rails form
You can follow along with my Rails app here.
Let's assume you've already got a Rails app going. I'm on v5.1.4, but most Rails versions 4.x.x and up should work the same. First we're going to generate a scaffold to get our form going:
rails generate scaffold Post title:string content:text
After this we'll have our migration, model, controller, and views all set up. You know how scaffolds work... Here's the view I'm going to focus on:
<!-- app/views/posts/new.html.erb -->
<%= form_with(model: post, local: true) do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :content %>
<%= f.text_area :content %>
<%= f.submit %>
<% end %>
Installing react-rails
We're going to use react-rails
with the asset pipeline, but there are other options such as Webpacker
if you'd like a more custom install. With the asset pipeline, there's hardly any extra legwork required when compiling these components.
# Gemfile
gem 'react-rails'
bundle && rails g react:install
react-rails
sets up most everything for you automatically.
modified:
Gemfile
Gemfile.lock
app/assets/javascripts/application.js
created:
app/assets/javascripts/components.js
app/assets/javascripts/components/
app/assets/javascripts/server_rendering.js
config/initializers/react_server_rendering.rb
Creating the React component
Your component will live in a .jsx
file in the newly created components
directory.
// app/assets/javascripts/components/_post_form.jsx
class PostForm extends React.Component { }
To render this component, simply call react_component
in your Rails view. Note that this is replacing the old form code.
<!-- app/views/posts/new.html.erb -->
<%= form_with(model: @post, local: true) do |form| %>
<%= react_component 'PostForm', { post: @post } %>
<% end %>
Now let's populate our component with everything our Rails form had. Below is the whole file, but I'll explain each block after.
// app/assets/javascripts/components/_post_form.jsx
class PostForm extends React.Component {
constructor(props) {
super(props);
this.state = {
title: props.post.title,
content: props.post.content
};
this.handleTitleChange = this.handleTitleChange.bind(this);
this.handleContentChange = this.handleContentChange.bind(this);
}
handleTitleChange(e) {
this.setState({ title: e.target.value });
}
handleContentChange(e) {
this.setState({ content: e.target.value });
}
render() {
return (
<div>
<label>Title</label>
<input
type="text"
name="post[title]"
value={this.state.title}
onChange={this.handleTitleChange}
/>
<label>Content</label>
<input
type="text"
name="post[content]"
value={this.state.content}
onChange={this.handleContentChange}
/>
<input type="submit" value="Update Post" />
</div>
);
}
}
Initialization
Constructor
Every React component has a constructor
, and every constructor must have props
passed into it and super(props)
called first. This props argument is the ruby hash passed when calling <%= react_component 'PostForm', { post: @post } %>
. So props.post
is our Rails post record.
State
A react component's state is the local object that gets updated from inside the component. Keeping with the "data down, actions up" philosophy, props
will be sent down, and the component will send its state
back up — we never want to edit props
directly. As such, we build our empty state object with the props
sent in from Rails.
Read more about React state here
Binding actions
When initializing a component, we need to bind our actions to this
in the beginning so they have a reference to the current scope later.
constructor(props) {
super(props);
this.state = {
title: props.post.title,
content: props.post.content
};
this.handleTitleChange = this.handleTitleChange.bind(this);
this.handleContentChange = this.handleContentChange.bind(this);
}
handleTitleChange(e) {
this.setState({ title: e.target.value });
}
handleContentChange(e) {
this.setState({ content: e.target.value });
}
Rendering the component
Last but not least, is the render()
method. Here we return the Reactified HTML that shows up on our page.
render() {
return (
<div></div>
);
}
Form inputs
For our post params, we need a text field with a specific name
attr. In our case, the params
that rails expects in the controller looks like this:
def post_params
params.require(:post).permit(:title, :content)
end
<label>Title</label>
<input
type="text"
name="post[title]"
value={this.state.title}
onChange={this.handleTitleChange}
/>
<input type="submit" value="Update Post" />
Here we have a text input who's initial value
was passed down from the props
to the state
, which came from Rails. Each time we type in the input, onChange
is called, and we update the component's local state
object from the handleTitleChange
action.
Afterwards, when we click the submit
button, the original Rails form is posted to its resource url (something like PUT /posts/1
). Inside that POST
request, will be our form inputs post[title]
, and post[content]
.
Voila!
Our new form submits just as the old rails one did, but with about 5x the amount of code! JavaScript is the future!
Jokes aside, let's consider a few possibilities with our fancy React forms.
Dynamic Material form chips
Creating these inside a Rails form would be far from trivial, but with React, we could render a Chip component inline with the rest of the form. This could be a fantastic replacement for the usual checkboxes for has_many
or HABTM relationships.
Material modal forms
Rendering a modal for a particular record on an index page in Rails can also be difficult. You run the risk of having 100+ items on a page and a modal for each, and if you attempt to fix that, you end up requiring some complex JavaScript to ensure you're dealing with the correct records. In React, this becomes very simple.
Fancy Dropdown Inputs
HTML Select menus don't always serve as the best representation of your form values. Often times, UI elements such as GitHub's are far better for UX but wouldn't be as easy in Rails and JavaScript alone. And in GitHub's case, where labels and other inputs are saved before the form is even posted, a React component would help pave the way for sending AJAX requests prior to saving the record.
Fin
I'd like to keep this a living document, so if you've got more examples of rich UI inline with standard CRUD that can benefit from Rails & React, send them my way! Thanks for reading.