DEV Community

Cover image for Rails with React - One Repo - Part 3
Harry Levine
Harry Levine

Posted on

Rails with React - One Repo - Part 3

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:

  1. User visits a URL
  2. An HTTP request is made to this URL
  3. The path is identified in routes.rb, and calls the associated controller action
  4. The controller action executes its logic
  5. 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)

Collapse
 
cirogolden profile image
Ciro Golden • Edited

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

Collapse
 
2ndplayer profile image
Chris Camp

Thanks for helping me bridge the gap between Rails and React! Very clear and useful!