Skip to content

Instantly share code, notes, and snippets.

@hpjaj
Last active November 13, 2018 21:09
Show Gist options
  • Select an option

  • Save hpjaj/344827cfbfe83252612628041f9827bc to your computer and use it in GitHub Desktop.

Select an option

Save hpjaj/344827cfbfe83252612628041f9827bc to your computer and use it in GitHub Desktop.
Tutorial Series: Rails with React - One Repo

Rails with React - One Repo

Part 1 of 3: Setting React as the view layer

This tutorial series will walk you through creating a Ruby on Rails web application that uses React as its view layer, all in one repository.

The primary technologies that will be included are:

  • Rails 5.2.1
    • Ruby 2.5.3
    • Postgresql
    • Webpacker
    • RSpec
  • React 16
    • React router
    • Axios

All of the code for this series resides at: https://github.com/oddballio/rails-with-react

Setup

You'll first need to install the current versions of Ruby and Rails. These instructions assume a MacOS with rbenv. They follow this fantastic, well maintained resource for Ruby and Rails environment setup, which also includes instructions for Ubuntu and Windows.

Install Ruby 2.5.3

Using rbenv call:

$ rbenv install 2.5.3

Install Rails 5.2.1

$ gem install rails -v 5.2.1

Create new Rails/React web app

Our web app will be called rails-with-react. Here is the command you'll run in order to create a Rails app that:

$ rails _5.2.1_ new rails-with-react -T -d postgresql --webpack=react

Set the Ruby version

The Ruby version will need to be updated in your application. cd into the new directory, and change the ruby version to 2.5.3 in both of these files:

  • Gemfile
  • .ruby-version

Add RSpec for spec support

Install the rspec-rails gem by adding the below to the Gemfile:

group :development, :test do
  gem 'rspec-rails', '~> 3.8'
end

Run the following commands to complete the installation:

$ gem install bundler
$ bundle install
$ rails generate rspec:install

Complete the setup

Lastly you'll run the bin/setup script to complete the installations:

$ bin/setup

Should you receive this error:

== Command ["bin/rails db:setup"] failed ==

Try:

$ rails db:migrate
$ bin/setup

Set React as the view layer

From the --webpack=react flag in the setup, you'll note the presence of a new app/javascript/ directory. This is where all of our React related code will live.

By default, Rails has included the following boilerplate files and structure:

app/
|
|-- javascript
|   |-- packs
        |-- application.js
        |-- hello_react.jsx

We are going to make a few changes, to set the React app to follow a more traditional, scalable component based structure.

Components

First we'll create our base App.js component:

  1. Underneath the app/javascript/ folder, create a components folder
  2. Within the components folder, create the first component called App.js
  3. Begin with a basic "Hello world" class component structure
// app/javascript/components/App.js

import React from 'react'

class App extends React.Component {
  render() {
    return (
      <div>
        Hello world!
      </div>
    )
  }
}

export default App

Index.js

Next we'll create an index.js file that will be injected into a Rails view file, containing our React app. To accomplish this, we will:

  1. Rename hello_react.jsx to index.js
  2. Remove this boiler plate code:
import PropTypes from 'prop-types'

const Hello = props => (
  <div>Hello {props.name}!</div>
)

Hello.defaultProps = {
  name: 'David'
}

Hello.propTypes = {
  name: PropTypes.string
}
  1. Import and render the new App component

The index.js file should look like this:

// app/javascript/packs/index.js

// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file,
// like app/views/layouts/application.html.erb. All it does is render <div>Hello React</div> at the bottom
// of the page.

import React from 'react'
import ReactDOM from 'react-dom'
import App from '../components/App'

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <App />,
    document.body.appendChild(document.createElement('div')),
  )
})

Root Rails view for the React app

We will now create the single Rails view where the entire React app will be injected into. Following conventional Rails patterns, this will involve a:

  • controller action
  • root route
  • view
  1. Create an app/controllers/pages_controller.rb with an empty index action
# app/controllers/pages_controller.rb

class PagesController < ApplicationController
  def index
  end
end
  1. Set the root route to this index action
# config/routes.rb

Rails.application.routes.draw do
  root 'pages#index'
end
  1. Create an empty app/views/pages/index.html.erb view file for rendering the index action

Inject React into Rails

To bridge the two worlds, React and Rails, you will use the Rails javascript_pack_tag to inject the entire React application into the Rails view.

  1. Add the javascript_pack_tag to the app/views/pages/index.html.erb file, injecting index.js
# app/views/pages/index.html.erb

<%= javascript_pack_tag 'index' %>
  1. Start the rails server
$ rails s
  1. Visit http://localhost:3000/

If you see Hello world!, you have successfully set React as a view layer for your Rails app!

Next Steps

There are two more tutorials in this series:

  • Part 2 of 3: Integrating React Router
  • Part 3 of 3: Handling requests between React and Rails

Rails with React - One Repo

Part 2 of 3: Integrating React Router

Recap

In part 1 of this series we covered setting react as the view layer.

All of the code for this series resides at: https://github.com/oddballio/rails-with-react

Introduction

As we now have the view layer staged, next we will want to be able to visit many different pages in our application, each with their own purpose. For example:

  • a home page
  • a page that displays a list of posts
  • a page with a form to create a new post

In order to create multiple React components with multiple, unique URLs, we will integrate React Router.

Create and import new components

Let's create a class component to represent each of these pages, with some boilerplate content.

  1. Create app/javascript/components/Home.js
// app/javascript/components/Home.js

import React from 'react'

class Home extends React.Component {
  render() {
    return (
      <div>
        Home page
      </div>
    )
  }
}

export default Home
  1. Create app/javascript/components/Posts.js
// app/javascript/components/Posts.js

import React from 'react'

class Posts extends React.Component {
  render() {
    return (
      <div>
        Posts page
      </div>
    )
  }
}

export default Posts
  1. Create app/javascript/components/NewPost.js
// app/javascript/components/NewPost.js

import React from 'react'

class NewPost extends React.Component {
  render() {
    return (
      <div>
        NewPost page
      </div>
    )
  }
}

export default NewPost
  1. Import the components into App.js
// app/javascript/components/App.js

import React from 'react'
import Home from './Home'
import Posts from './Posts'
import NewPost from './NewPost'

...

Install and import dependencies

  1. Install React Router and react-router-dom
$ yarn add react-router
$ yarn add react-router-dom
  1. In index.js import the relevant package components
// app/javascript/packs/index.js

...
import { BrowserRouter as Router, Route } from 'react-router-dom'

Setup the Router and Routes

Let's integrate the components from these new packages.

  1. In index.js, instead of passing in the App component, we'll pass in the package's Router component
// app/javascript/packs/index.js

...
document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <Router>

    </Router>,
    document.body.appendChild(document.createElement('div')),
  )
})
  1. Within the Router component, we'll add a Route component that establishes a root path, with App.js as our root component
// app/javascript/packs/index.js

...
    <Router>
      <Route path="/" component={App} />
    </Router>,

Create the routes in the React app

As App.js is set as the root component for the router, it will contain all of the individual routes for each component.

  1. In App.js, import the Route and Switch components from react-router-dom
// app/javascript/components/App.js

...
import { Route, Switch } from 'react-router-dom'
  1. In App.js, establish all of the unique routes within a Switch component
// app/javascript/components/App.js

...
class App extends React.Component {
  render() {
    return (
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/posts" component={Posts} />
          <Route exact path="/new_post" component={NewPost} />
        </Switch>
      </div>
    )
  }
}

export default App

Umbrella Rails route for all React routes

We need to create a catchall route that matches any of the potential routes that might come from our React app, and funnel them to our pages_controller#index action. This being the action that renders our React application.

Important: This match route must be the last route in routes.rb to ensure that it does not mistakenly absorb any other routes.

  1. In routes.rb create the catchall route
# config/routes.rb

Rails.application.routes.draw do
  root 'pages#index'

  # IMPORTANT #
  # This `match` must be the *last* route in routes.rb
  match '*path', to: 'pages#index', via: :all
end
  1. Start the rails server with rails s
  2. In a separate tab run the bin/webpack-dev-server script
$ bin/webpack-dev-server

This sets up the real time reloading that is standard with a basic React app.

  1. Visit http://localhost:3000/

You should see "Home page"

  1. Visit http://localhost:3000/posts

You should see "Posts page"

  1. Visit http://localhost:3000/new_post

You should see "NewPost page"

Next Steps

There is one more tutorial in this series:

Part 3 of 3: Handling requests between React and Rails

Rails with React - One Repo

Part 3 of 3: Handling requests between React and Rails

Recap

In parts 1 & 2 of this series we covered:

  • setting react as the view layer
  • integrating React router

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 typicall 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.

  1. In config/initializers/inflections.rb implement an inflection rule to allow the api namespace to be referenced as a capitalized API module during routing
# config/initializers/inflections.rb

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym 'API'
end
  1. 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
  1. Import Axios into Posts.js
// app/javascript/components/Posts.js

import axios from 'axios'
...
  1. Call posts#index with Axios

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 });
      })
  }
...
  1. 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>
    )
  }
...
  1. 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
  1. Start the rails s in one tab, and run bin/webpack-dev-server in another tab

  2. 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
  1. 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>
    )
  }
  1. 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'
...
  1. 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)}>
...
  1. 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);
      })
  }
  1. Start the rails s in one tab, and run bin/webpack-dev-server in another tab

  2. 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);
}
  1. 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)
  }
...
  1. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment