Part 3 of 3: Handling requests between React and Rails
Recap
In parts 1 & 2 of this series we covered:
All of the code for this series resides at: https://github.com/oddballio/rails-with-react
Introduction
A traditional Rails app has the following general life cycle when rendering a page:
- User visits a URL
- An HTTP request is made to this URL
- The path is identified in
routes.rb
, and calls the associated controller action - The controller action executes its logic
- The controller action renders a view, passing any relevant return data to the view
In this tutorial we'll cover how to recreate this pattern with a React view layer that interacts with the Rails backend.
GET request
We'll represent the HTTP GET
request process through the Posts.js
component. This component will call the posts_controller#index
Rails action in order to render a list of posts.
Rails controller action
This will be a typical Rails controller action, with a few adjustments to make it behave like an API.
1. Create an api
folder underneath app/controllers/
This namespacing of our controllers and routes mitigates against any potential collisions between a React route and a Rails route.
2. In config/initializers/inflections.rb
implement an inflection rule to allow the api
namespace to be referenced as a capitalized API
module during routing
The Rails guide explains this as:
specifying any word that needs to maintain a non-standard capitalization
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
3. Create a app/controllers/api/posts_controller.rb
with an index
action
For simplicity, we'll simulate that the index
action is returning a collection of Post
records from the database through a stubbed out array of posts.
# app/controllers/api/posts_controller.rb
module API
class PostsController < ApplicationController
def index
posts = ['Post 1', 'Post 2']
render json: { posts: posts }
end
end
end
Rails posts#index
route
In keeping with our controller namespacing, we'll namespace all of our routes within an :api
namespace.
# config/routes.rb
Rails.application.routes.draw do
root 'pages#index'
namespace :api, defaults: { format: 'json' } do
resources :posts, only: :index
end
# IMPORTANT #
# This `match` must be the *last* route in routes.rb
match '*path', to: 'pages#index', via: :all
end
Calling posts#index
from React
Our Posts.js
component will need to make an HTTP GET
request to our new posts#index
endpoint. To do this, we will use the Axios HTTP client.
1. Install Axios
$ yarn add axios
2. Import Axios into Posts.js
// app/javascript/components/Posts.js
import axios from 'axios'
...
3. Call posts#index
with Axios
We’ll make the Axios call within a componentDidMount()
React lifecycle method. You can read more about lifecycle methods in this Guide to React Component Lifecycle Methods.
Note that the Rails route for the posts#index
endpoint is /api/posts
.
// app/javascript/components/Posts.js
...
class Posts extends React.Component {
state = {
posts: []
};
componentDidMount() {
axios
.get('/api/posts')
.then(response => {
this.setState({ posts: response.data.posts });
})
}
...
4. Display the posts returned from the posts#index
call
// app/javascript/components/Posts.js
...
renderAllPosts = () => {
return(
<ul>
{this.state.posts.map(post => (
<li key={post}>{post}</li>
))}
</ul>
)
}
render() {
return (
<div>
{this.renderAllPosts()}
</div>
)
}
...
5. Posts.js
should end up looking like this:
// app/javascript/components/Posts.js
import React from 'react'
import axios from 'axios'
class Posts extends React.Component {
state = {
posts: []
};
componentDidMount() {
axios
.get('/api/posts')
.then(response => {
this.setState({ posts: response.data.posts });
})
}
renderAllPosts = () => {
return(
<ul>
{this.state.posts.map(post => (
<li key={post}>{post}</li>
))}
</ul>
)
}
render() {
return (
<div>
{this.renderAllPosts()}
</div>
)
}
}
export default Posts
6. Start the rails s
in one tab, and run bin/webpack-dev-server
in another tab
7. Visit http://localhost:3000/posts
You should see:
• Post 1
• Post 2
POST request
We'll represent the HTTP POST
request process through the NewPost.js
component. This component will call the posts_controller#create
Rails action in order to create a new post.
Rails controller action and route
1. Add a create
action to the posts_controller
We will simulate that the post_controller#create
action was successfully hit by rendering the params
that the endpoint was called with:
# app/controllers/api/posts_controller.rb
def create
render json: { params: params }
end
2. Add a :create
route to the :posts
routes
# config/routes.rb
namespace :api, defaults: { format: 'json' } do
resources :posts, only: [:index, :create]
end
Form to create a post
1. In NewPost.js
create a form to submit a new post
// app/javascript/components/NewPost.js
render() {
return (
<div>
<h1>New Post</h1>
<form>
<input
name="title"
placeholder="title"
type="text"
/>
<input
name="body"
placeholder="body"
type="text"
/>
<button>Create Post</button>
</form>
</div>
)
}
2. Capture the form data
We'll go about this by setState
through each input's onChange
:
// app/javascript/components/NewPost.js
import React from 'react'
class NewPost extends React.Component {
state = {
title: '',
body: ''
}
handleChange = event => {
this.setState({ [event.target.name]: event.target.value });
}
render() {
return (
<div>
<h1>New Post</h1>
<form>
<input
name="title"
onChange={this.handleChange}
placeholder="title"
type="text"
/>
<input
name="body"
onChange={this.handleChange}
placeholder="body"
type="text"
/>
<button>Create Post</button>
</form>
</div>
)
}
}
export default NewPost
Calling posts#create
from React
Our NewPost.js
component will need to make an HTTP POST
request to our new posts#create
endpoint. To do this, we will use the Axios HTTP client we installed in the last section.
1. Import Axios into NewPost.js
// app/javascript/components/NewPost.js
import axios from 'axios'
...
2. Create a function to handle the form's submission
// app/javascript/components/NewPost.js
...
handleSubmit = event => {
event.preventDefault();
}
render() {
return (
<div>
<h1>New Post</h1>
<form onSubmit={e => this.handleSubmit(e)}>
...
3. POST
the form data to the posts#create
endpoint
The Rails route for the posts#create
endpoint is /api/posts
. We will console.log
the response.
// app/javascript/components/NewPost.js
handleSubmit = event => {
event.preventDefault();
const post = {
title: this.state.title,
body: this.state.body
}
axios
.post('/api/posts', post)
.then(response => {
console.log(response);
console.log(response.data);
})
}
4. Start the rails s
in one tab, and run bin/webpack-dev-server
in another tab
5. Visit http://localhost:3000/new_post
, fill out and submit the form
At this point the form should not work. If you look in the Rails server logs, you should see:
ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken)
This is a result of Rails' Cross-Site Request Forgery (CSRF) countermeasures.
Resolve CSRF issues
To resolve this issue, we need to pass Rails' CSRF token in our Axios headers, as part of our HTTP POST
request to the Rails server-side endpoint.
Since this functionality will be required in any other future non-GET
requests, we will extract it into a util/helpers.js
file.
1. Create a app/javascript/util/helpers.js
file
2. In helpers.js
add functions to pass the CSRF token
// app/javascript/util/helpers.js
function csrfToken(document) {
return document.querySelector('[name="csrf-token"]').content;
}
export function passCsrfToken(document, axios) {
axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken(document);
}
3. Import the passCsrfToken
function into NewPost.js
and call it
// app/javascript/components/NewPost.js
...
import { passCsrfToken } from '../util/helpers'
class NewPost extends React.Component {
state = {
title: '',
body: ''
}
componentDidMount() {
passCsrfToken(document, axios)
}
...
4. Visit http://localhost:3000/new_post
, fill out and submit the form
In the console you should see:
params: {title: "some title", body: "some body", format: "json", controller: "api/posts", action: "create", …}
🎉
This tutorial series was inspired by "React + Rails" by zayne.io
Top comments (2)
I Finished your Tutorial! 3+ hours but finally finished and I'm so happy with the final result, I'm going to have to add some more things but it's going to look really nice!
Would you have any suggestions best routes for implementing CRUD I can't find a good helper that goes well with your tutorial unfortunately
Thanks for helping me bridge the gap between Rails and React! Very clear and useful!