No more boilerplate!

The next story wants us to let authors edit their own posts but we don’t have an association between posts and users yet.

  • An author can edit a blog post.

We don’t have a way to create users either, except in the Rails console.

Normally, we’d scaffold a UI in Rails, but I want to do this in React to see how the Store that we created for posts generalises to other models and associations.

I’ll start by adding some missing details to the user model.

# rails g migration AddNameToUser name:string
class AddNameToUser < ActiveRecord::Migration[6.1]
  def change
    add_column :users, :name, :string
    add_column :users, :role, :integer
    add_index :users, :name, unique: true
  end
end

# User.rb
class User < ApplicationRecord
  authenticates_with_sorcery!
  enum role: [:guest, :author, :admin]

  validates :name,
            presence: true,
            uniqueness: true,
            length: { minimum: 2, maximum: 20},
            format: /\A[a-z_][\sa-z0-9_.-]+\z/i

  validates :email,
            presence: true,
            uniqueness: true,
            format: /\A[^@]+@[^@]+\.[^@]+\z/i
end

Now I’ll add a UsersController to return some JSON to the client. The tests are on GitHub.

# routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  resources :posts
  resources :sessions
  resources :users

  get :sign_in,  to: 'sessions#new', as: :sign_in
end
class UsersController < ApplicationController
  skip_before_action :require_login, only: :show
  before_action :require_admin, except: :show
  before_action :set_user, except: :index

  helper_method :current_user, :admin?

  def index
    @users = User.by_name
  end

  def show
  end

  protected
  def set_user
    @user = User.find params[:id]
  end

  def admin?
    current_user&.admin?
  end

  def require_admin
    require_login
    raise Errors::AccessError unless admin?
  end
end
# app/lib/errors/access_error.rb
class Errors::AccessError < StandardError
-# views/users/index.json.jbuilder
json.users do
  json.partial! 'users/user', collection: @users, as: :user
end
-# views/users/show.json.jbuilder
json.partial! 'users/user', user: @user
-# views/users/_user.json.jbuilder
if admin?
  json.extract! user, :id, :name, :email, :role, :created_at, :updated_at
else
  json.extract! user, :id, :name
end

Now, after a bit of tidying, we have an AccessControl concern…

# app/controllerrs/concerns/access_control.rb
module AccessControl
  extend ActiveSupport::Concern

  included do
    before_action :require_login
    helper_method :current_user, :admin?
  end

  protected
  def admin?
    current_user&.admin?
  end

  def require_admin
    require_login
    raise Errors::AccessError unless admin?
  end
end


…that gets loaded by the ApplicationController and the UsersController simplifies to this:

class UsersController < ApplicationController
  skip_before_action :require_login, only: :show
  before_action :require_admin, except: :show
  before_action :set_user, except: :index

  layout 'react'
  
  def index
    @users = User.by_name
  end

  def show
  end

  protected
  def set_user
    @user = User.find params[:id]
  end
end

I also extracted a layout file for the react container so I don’t have to repeat it for every view that needs it.

<% content_for :body do %>
  <div id='react' data-user-id='<%= current_user&.id%>' />
<% end %>

<%= render template: 'layouts/application' %>

And now for the bit that I am most interested in. I want to make it as easy to create a React app — for the CRUD actions at least — as it is to do with Rails views.

Let’s look at the Store that we created for posts.

export class Store {
  constructor() {
    this.all = []
    this.by_id = {}
    this.subscribers = []
  }

  async list() {
    const {posts} = await list()
    this.addAndNotify(posts)
    return posts
  }

  async find(id) {
    let post = this.by_id[id]
    if(! post) {
      post = await fetch(id)
      this.addAndNotify(post)
    }
    return post
  }

  async create(post) {
    post = await create(post)
    // todo don't add it to the store unless it is valid
    this.addAndNotify(post)
    return post
  }

  addAndNotify(post_or_posts) {
    if(Array.isArray(post_or_posts))
      post_or_posts.forEach(post => this.add(post))
    else
      this.add(post_or_posts)

    this.all = Object.values(this.by_id).sort(by_created_at)
    this.notify()
  }

  add(post) {
    this.by_id[post.id] = post
  }

  // extract this to a class
  subscribe(fn) {
    this.subscribers.push(fn)
  }

  unsubscribe(fn) {
    this.subscribers = this.subscribers.filter(subscriber => subscriber !== fn)
  }

  notify() {
    //  do a dispatch thing here with a queue
    this.subscribers.forEach(fn => fn())
  }
}

export const store = new Store()

There’s nothing about that that is specific to posts. It should work for any model so let’s extract the beginning of a library for binding the React UI to the Rails API with minimal coding.

I moved the Store to the new directory that will be the start of our library and parameterised the constructor to take a type.

// ReactToRails/store.js
export default class Store {
  constructor(type) {
    this.type = type
    this.plural = pluralize(type)
    this.all = []
    this.by_id = {}
    this.subscribers = []
  }
  // ...

I moved everything into this directory that is not part of the Blogging domain: api.js; sorting.js; server.js. Everything.

I parameterised the api calls to take a type. It uses the simple rule that URLs take the form /plural-of-type.json which covers the majority of URLs. When we get one that doesn’t follow this rule, we’ll do something more sophisticated.

export function fetch(type, id) {
  return server.get(pathToShow(type, id))
}

export function list(type) {
  return server.get(pathToIndex(type))
}

export function create(type, record) {
  return server.post(pathToIndex(type), {[type]: record})
}

function pathToShow(type, id) {
  return `/${pluralize(type)}/${id}.json`
}

function pathToIndex(type) {
  return `/${pluralize(type)}.json`
}

export function pluralize(type) {
  return `${type}s`
}

Let’s look at those ConnectedXXX components next.

// posts/List.jsx
export default class ConnectedList extends React.Component {
  constructor(props) {
    super(props)

    this.store = this.props.store || store
    this.state = {
      posts: null
    }

    this.storeDidUpdate = this.storeDidUpdate.bind(this)
  }

  render() {
    const {posts} = this.state
    if(posts === null) return 'Loading…'
    return <List posts={posts} />
  }

  async componentDidMount() {
    const posts = await this.store.list()
    this.setState({posts})
    this.store.subscribe(this.storeDidUpdate)
  }

  componentWillUnmount() {
    this.store.unsubscribe(this.storeDidUpdate)
  }

  storeDidUpdate() {
    const posts = this.store.all
    this.setState({posts})
  }
}

That’s 98% boilerplate and we can extract that away to the library too. Let’s try a higher-order component (HOC) the way that Redux does for connected components.

According to the React site,

a higher-order component is a function that takes a component and returns a new component.

We are going to use it for two things:

  1. To parameterize the ConnectedList.
  2. To connect the list to the store.

Here it is:

// ReactToRails/ConnectedList.jsx

export function connectList(WrappedList, store) {
  return class extends React.Component {
    constructor(props) {
      super(props)

      this.state = {
        records: null
      }

      this.storeDidUpdate = this.storeDidUpdate.bind(this)
    }

    render() {
      const {records} = this.state
      if (records === null) return 'Loading…'

      const props = {
        [store.plural]: records
      }
      return <WrappedList {...props} />
    }

    async componentDidMount() {
      const records = await store.list()
      this.setState({records})
      store.subscribe(this.storeDidUpdate)
    }

    componentWillUnmount() {
      store.unsubscribe(this.storeDidUpdate)
    }

    storeDidUpdate() {
      const records = store.all
      this.setState({records})
    }
  }
}

It’s functionally equivalent to the ConnectedList we had before, but this one lets us pass in a store object and a List component. and it doesn’t care about the types.

Here’s what it does:

  1. Fetch a list of records from the store.
  2. Subscribe to updates from the store.
  3. Updated the WrappedList when the records are added to the store.

We can call from list.jsx.

// posts/list.jsx
export default connectList(List, store)

We can do the same de-boilerplating for the ConnectedPost component.

// ReactToRails/ConnectedView.jsx
export function connectView(WrappedView, store) {
  return class extends React.Component {
    constructor(props) {
      super(props)

      this.store = this.props.store || store
      this.state = {
        record: {}
      }
    }

    render() {
      const {record} = this.state
      const props = {
        [this.store.type]: record
      }
      return <WrappedView {...props} />
    }

    async componentDidMount() {
      const record = await this.store.find(this.props.id)
      this.setState({record})
    }
  }
}
export default connectView(Post, store)

All the boilerplate is gone from our App now and, with any luck, it should be easy to add the crud pages for users.

Deploy a React/Rails app to Heroku

Now that we are all secure, we can deploy our new blogging platform to the interwebs. Heroku makes it super easy and gives you clear instructions at every step. I’ll just summarize the highlights here.

Start by creating a Heroku account and then go to your Heroku dashboard at https://dashboard.heroku.com/apps and click the NEW button.

Heroku has a free tier that restricts you to a certain number of hours of availability and rows in the database. Even the next level up (Hobby Tier) only costs $7 per month.

Once the app is created, Heroku gives you custom instructions to deploy your app.

For me it was:

heroku login
heroku git:remote -a react-to-blogging
git push heroku master

Heroku does not run the database migrations automatically so I’ll do that now.

heroku run rails db:migrate

I’ll need to create a user and sign in before I can post. I can get to the rails console with…

heroku run rails console

…and I can create a user record with…

User.create! email: 'sally@example.com', password: 'XXXXXXXX'

Now I can sign and post. That’s it. I’m blogging on Heroku.

You can visit the blog here: https://react-to-blogging.herokuapp.com/

One last thing: While I was deploying, I noticed a warning message saying that a Procfile is recommended. I haven’t seen any downsides of not having one but it gives you a place to run migrations automatically so I’ll add one now.

I copied this from somewhere or other.

web: bundle exec puma -C config/puma.rb
release: rake db:migrate

Back to writing code! What’s next, I wonder.

Halt! Who goes there!

If we publish a blogging platform to the internet with no security, bad people will post spam and mess it up for everyone. Let’s add a sign-in form to keep the bad guys out.

I like the Sorcery gem because it just quietly does its thing without imposing its opinions on registration flow like Devise does.

From the Sorcery readme:

// Gemfile
gem 'sorcery'
bundle install
rails generate sorcery:install
rails db:migrate

That will have created a User model for us.

class User < ApplicationRecord
  authenticates_with_sorcery!
end

We’ll need a SessionsController for the sign in method.

rails g controller Sessions

I think it’s interesting to show the tests for this. It can be a mini-tutorial for folks who have never used Sorcery before and need help writing integration tests with it.

require "test_helper"

class SessionsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @sally = User.create! email: 'sally@example.com',
                          password: 'letmein'
  end

  test 'sign in renders a form' do
    get sign_in_url
    assert_response :success

    assert_select 'form' do
      assert_select '#credentials_email'
      assert_select '#credentials_password'
      assert_select 'input.submit'
    end
  end

  test 'redirect to home page after sign in' do
    post sessions_url, params: {
        credentials: {
            email: 'sally@example.com',
            password: 'letmein',
        }
    }
    assert_redirected_to root_url
  end

  test 'rerender the form if sign in fails' do
    post sessions_url, params: {
        credentials: {
            email: 'sally@wrong.com',
            password: 'wrong',
        }
    }
    assert_response :success

    assert_select '.form' do
      assert_select '.error', 'Invalid Login'
      assert_select '#credentials_email' do |elements|
        assert_equal 'sally@wrong.com', elements[0][:value]
      end
    end
  end
end
// routes.rb

  resources :sessions
  get :sign_in,  to: 'sessions#new', as: :sign_in
class SessionsController < ApplicationController
  def new
  end

  def create
    credentials = credential_params

    login(credentials[:email], credentials[:password]) do |user, failure|
      if failure
        @email = credentials[:email]
        @error = failure
        render action: 'new'
      else
        redirect_back_or_to(:root)
      end
    end
  end

  private
  def credential_params
    params.
        require(:credentials).
        permit(:email, :password)
  end
end

The view is pretty simple but it took me a while to remember how to write ERB. I usually use HAML as it’s so much easier to read and write.

-# views/sessions/new.html.erb
<div class='session-page'>
  <div class='session-panel'>
    <h1>Sign in</h1>

    <%= form_for :credentials, url: {action: :create}, html: {class: 'form'} do |form| %>
      <div class='error'>
        <%= t @error %>
      </div>
      <div class='input email'>
        <%= form.label :email %>
        <%= form.email_field :email,
                             value: @email,
                             required: true,
                             autofocus: true,
                             placeholder: 'Enter your email address'
        %>
      </div>
      <div class='input password'>
        <%= form.label :password %>
        <%= form.password_field :password, 
                                required: true, 
                                placeholder: 'Enter your password' %>
      </div>
      <div class='actions'>
        <%= form.submit 'Sign in', class: 'button submit primary' %>
      </div>
    <% end %>
  </div>
</div>

That test passes and a little bit of CSS magic gives me this

I’d like the sign in to be in React someday but it’ll be a distraction for now and I’ll come back to it later. No one said that all my UI must be in React anyway.

If I create a user in the Rails console, I’ll be able to sign in.

User.create! email: 'sally@example.com', password: 'letmein'

Now we can protect the important controller actions. I find it safest to require login on everything and to then make exceptions for the public-facing actions.

class ApplicationController < ActionController::Base
  before_action :require_login

class PostsController < ApplicationController
  skip_before_action :require_login, only: [:index, :show]

class SessionsController < ApplicationController
  skip_before_action :require_login

If I run all the Rails tests now, I see a failure.

PostsControllerTest#test_create_a_blog_post:
Expected response to be a <2XX: success>, 
but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being <a href="http://www.example.com/">redirected</a>.</body></html>

I can update the test to sign in first.

  test 'create a blog post requires a sign in' do
    post posts_url(format: :json), params: {
        post: {
            title: 'A new day',
            body: 'First post!'
        }
    }
    assert_redirected_to root_url
  end

  test 'create a blog post' do
    sign_in @sally
    post posts_url(format: :json), params: {
        post: {
            title: 'A new day',
            body: 'First post!'
        }
    }
    assert_response :success
    # ...
  end 

The sign_in method exists in a test helper:

module TestHelpers
  module AuthenticationTestHelper
    def sign_in user, options={}
      password = options[:password] || 'letmein'
      post sessions_url,
           params: {
               credentials: {
                   email:    user.email,
                   password: password
               },
           }
    end
  end
end

It’s a bit of faffing to set up a test helper that is accessible to all the tests, but it make them more readable if you do.

# test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require_relative "../config/environment"
require "rails/test_help"
Dir['test/test_helpers/*.rb'].each {|file| require file.sub(/^test\//, '') }

class ActiveSupport::TestCase
  parallelize(workers: :number_of_processors)
  fixtures :all
end

class ActionDispatch::IntegrationTest
  include TestHelpers::JsonTestHelper
  include TestHelpers::AuthenticationTestHelper
end

While I am here, I’ll create a test helper for JSON testing too. That’s been bothering me for a while. I just updated the test above to use it.

module TestHelpers
  module JsonTestHelper
    def json_response options={}
      assert_response options[:expected_response] || :success
      JSON.parse response.body, symbolize_names: true
    end
  end
end

The last thing left to do is to hide the post Editor if the user is not signed in. There are a bunch of ways we can send the current user to the React client. The easiest, for now, is to add a data property to the div that contains the React application. We can do something more sophisticated later.

<div id='react' data-user-id='<%= current_user&.id%>' />

We can read this property from Javascript and store it somewhere where it is accessible from any component.

I’m sure this will get more complex later but let’s keep it simple for now.

// users/current_user.js
const current_user = {
  id: null,
}

export function signIn(id=true) {
  current_user.id = id
}

export function signOut() {
  current_user.id = null
}

export function signedIn() {
  return current_user.id
}

We can read the ID and store it when the app loads.

// application/index.jsx
document.addEventListener('DOMContentLoaded', () => {
  const react = document.querySelector('#react')
  signIn(react.getAttribute('data-user-id'))
  // ...

Now we can call the signedIn() method from the editor and render nothing if the user is not signed in.

    if(!signedIn()) return null

We can also call signIn() and signOut() in our tests.

Managing State in a React Application

Just to recap where we left off.

We can:

  1. Fetch a post from the server and display it.
  2. Create a new post and send it to the server.
  3. Fetch a list of posts from the server and display them.

It’s all tested and checked in to GitHub here and all the tests are running in CircleCI here.

We just discovered a problem though. Although you can create a new post, it doesn’t show up in the list of posts unless you refresh the page.

This is the point where I would usually reach for Redux but I have an idea for a state management library that is more Rails-friendly and requires less boilerplate code. Will it work? Who knows! Maybe I’ll just end up rebuilding Redux.

I am going to try though.

I want something that will be familiar to Rails developers and use many of the same API conventions. I’ll try to follow the Rails ideal of convention-over-configuration and, where there is configuration already on the server, I’ll try to use that. Wish me luck!

Let’s start with a Store that can be accessed by all the connected components.

// posts/Store.test.jsx
describe('The post store', () => {
  test('is initially empty', () => {
    const store = new Store()
    expect(store.all).toEqual([])
  })
})
// posts/Store.js
export class Store {
  constructor() {
    this.all = []
  }
}

// There's one store accessible to whoever needs it.
export const store = new Store() 

If someone calls list(), the store should go fetch posts from the server.

describe('The post store', () => {
let store
server.send = jest.fn()
const post = {
id: 1,
title: 'The Title',
body: 'The body.',
}

beforeEach( () => {
store = new Store()
})

test('is initially empty', () => {
expect(store.all).toEqual([])
})

test('fetches the list of posts from the server', async () => {
server.send.mockReturnValue({posts: [post]})

const posts = await store.list()
expect(posts.length).toEqual(1)
expect(posts[0].title).toEqual('The Title')
expect(store.all).toEqual(posts)

expect(server.send).toBeCalledWith('/posts.json')
})
})
export class Store {
  constructor() {
    this.all = []
  }

  async list() {
    const {posts} = await list()
    this.all = posts
    return posts
  }
}

Now we’ll edit ConnectedList to get its posts from the store instead of directly from the server. No need to change the tests; they should still just work.

export default class ConnectedList extends React.Component {
  async componentDidMount() {
    const posts = await store.list()
    this.setState({posts})
  }
}

Our next trick is to notify subscribers when a post is added to the store.

  test('notify subscribers when a post is added to the store', async () => {
    const subscriber = jest.fn()
    store.subscribe(subscriber)

    store.addAndNotify(post)

    expect(subscriber).toBeCalled()
    expect(store.all[0]).toEqual(post)
  })
export class Store {
  constructor() {
    this.all = []
    this.subscribers = []
  }

  async list() {
    const {posts} = await list()
    this.all = posts
    return posts
  }

  addAndNotify(post) {
    this.all = [...this.all, post]
    this.notify()
  }

  // extract this to a class
  subscribe(fn) {
    this.subscribers.push(fn)
  }

  unsubscribe(fn) {
    this.subscribers = this.subscribers.filter(subscriber => subscriber !== fn)
  }

  notify() {
    this.subscribers.forEach(fn => fn())
  }
}

And now we can hook the ConnectedList component up as a subscriber to the store.

// posts/List.test.jsx
  test('updates the list when a post is added to the store', async () => {
    server.send.mockReturnValue({posts: []})

    const component = await displayConnected(<ConnectedList store={store} />)
    assert_select(component, '.post', 0)

    store.addAndNotify(post)
    component.update()
    assert_select(component, '.post', 1)
  })

I did a little tidying here to allow us to pass in the store as a property to make tests easier to isolate from one another.

And, finally, here’s the ConnectedList, now subscribing to changes in the store.

  async componentDidMount() {
    const posts = await this.store.list()
    this.setState({posts})
    this.store.subscribe(this.storeDidUpdate)
  }

  componentWillUnmount() {
    this.store.unsubscribe(this.storeDidUpdate)
  }

  storeDidUpdate() {
    const posts = this.store.all
    this.setState({posts})
  }

I’m going to stop showing all my tests from here on since you probably get the idea already and it’s TDDious to show every little change. I’ll share the test if there is something new and interesting but, otherwise, I’ll stick to the code. I am still TDDing behind the scenes of course and, if you are interested, you can see all the tests in the GitHub repository here.

There are a couple of loose ends though. I should make all the components use the store instead of doing their own thing.

I’ll start with ConnectedPost. I’ll make the store maintain a list of posts by id and return the local copy if it already has one.

// store.js
  async find(id) {
    let post = this.by_id[id]
    if(! post) {
      post = await fetch(id)
      this.addAndNotify(post)
    }
    return post
  }

  async list() {
    const {posts} = await list()
    this.addAndNotify(posts)
    return posts
  }


  addAndNotify(post_or_posts) {
    if(Array.isArray(post_or_posts))
      post_or_posts.forEach(post => this.add(post))
    else
      this.add(post_or_posts)
    
    this.all = Object.values(this.by_id)
    this.notify()
  }

  add(post) {
    this.by_id[post.id] = post
  }

Don’t forget we need to sort the posts by date.

  addAndNotify(post_or_posts) {
    // ...    
    this.all = Object.values(this.by_id).sort(by_created_at)
    // ...
  }


// sorting.js
export function by_created_at(a, b) {
  if(a.created_at === b.created_at) return 0
  if(a.created_at > b.created_at) return -1
  return 1
}

Now to add a create method to the store…

// posts/store.js
  async create(post) {
    post = await create(post)
    this.addAndNotify(post)
    return post
  }

…and finally, we can call it from the post Editor

async handleSubmit(event) {
event.preventDefault()

const {post} = this.state
await this.store.create(post)
}

…and our job here is done.

We’ll reflect on where we are tomorrow because, now, we’ve earned ourself a pint of IPA at The Broken Dock.

What do you want to see?

As of now, we can create a new post and we can show one hard-coded post. It’s almost time to add some structure and navigation to our app. Let’s show a list of posts first, though, and we can think about how we navigate between items later.

Let’s add a list component to show all the posts. We’ll fake it in the front end…

export function Main() {
  return <div id='main'>
    <Editor onSubmit={create} />
    <List />
  </div>
}
// /javascript/posts/List.jsx
export default class List extends React.Component {
  render() {
    return 'list of posts'
  }
}

… until we have the backend in place.

The posts.index action in PostsController will return the React application if you request HTML and a list of posts if you request JSON.

# PostsControllerTest.rb
  
test 'the index page just returns the react application' do
  get posts_url
  assert_select '#react'
end

test 'fetch a list of posts as json' do
  get posts_url(format: :json)
  assert_response :success
  json = JSON.parse response.body, symbolize_names: true

  assert 1, json[:posts].count
  assert 'The title', json[:posts][0][:title]
end
# PostsController.rb

def index
  @posts = Post.all.order(created_at: :desc)
end

# index.json.jbuilder
json.posts do
  json.array! @posts do |post|
    json.extract! post, :id, :title, :body, :created_at, :updated_at
  end
end

And, if I open http://blogging.local:3000/posts.json in the browser, I see my posts. Now to fetch them from the client.

I’ll add an API methods to fetch the list of posts…

// posts/api.js

export function list() {
  return server.get(`/posts.json`)
}

…and update the List component to call it and render the list.

export default class List extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      posts: null
    }
  }

  render() {
    const {posts} = this.state
    if(posts === null) return 'Loading…'

    return <ul className='posts-list'>
      {
        posts.map(post => <li key={post.id}>
          <Post post={post}/>
        </li>)
      }
    </ul>
  }

  async componentDidMount() {
    const response = await list()
    this.setState({posts: response.posts})
  }
}

That’s all straightforward, I think, except: note the key property in <li key={post.id}>.

React performs optimisations by keeping track of changes to the React component tree and only re-rendering components that have changed since the last render. When you render an array of components you should provide a unique key to each component to help React figure out what has changed.

If I run all the Javascript tests now, I see a failure in the top-level Application.test.jsx because it is still expecting a single Post component rather than a list.

Let’s fix that.

// Application.test.jsx

describe('The application', () => {
  server.send = jest.fn()

  const post = {
    id: 1,
    title: 'React on Rails',
    body: 'I can use React with Rails.',
  }

  const posts = [post]

  beforeEach( () => {
    // Return a list of posts instead of a single post
    server.send.mockReturnValue({posts})  
  })

  test('shows a list of blog posts', async () => {
    const component = await display(<Application />)
    component.update()

    assert_select(component, '.site-name',   'Blogging')
    assert_select(component, '.post .title', 'React on Rails')
    assert_select(component, '.post .body',  'I can use React with Rails.')

    expect(server.send).toBeCalledWith('/posts.json')
  })
})

I’m not sure why, but I had to add component.update() to get the list component to render the correct HTML (I think this might be a bug in Enzyme).

I like to think of the top-level Application.test as a kind of smoke test that tests the whole application from end to end, albeit in a rather shallow fashion. It won’t tell us anything profound about the application’s behaviour but it might flag some future regression that would be missed by lower-level unit tests.

As I did with the Post.test, I’ll duplicate this test as a unit test so I can test the List component more thoroughly. But first I am going to split the component into a List that knows how to render a list of posts and a ConnectedList that knows how to fetch a list of posts from the server.

This seems like a lot of faff when I am typing it into WordPress but it’s literally 10 seconds of copy-paste in the IDE and my future self will thank me when I need to write more complex tests and I can do so without loading the whole framework.

Here’s the code.

// posts/List.jsx

export function List({posts}) {
  return <ul className='posts-list'>
    {
      posts.map(post => <li key={post.id}>
        <Post post={post}/>
      </li>)
    }
  </ul>
}

export default class ConnectedList extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      posts: null
    }
  }

  render() {
    const {posts} = this.state
    if(posts === null) return 'Loading…'
    return <List posts={posts} />
  }

  async componentDidMount() {
    const response = await list()
    this.setState({posts: response.posts})
  }
}

And here are the tests: one for the List and one for the ConnectedList.

// posts/List.test.jsx
describe('The post list', () => {
  server.send = jest.fn()

  const post = {
    id: 1,
    title: 'React on Rails',
    body: 'I can use React with Rails.',
  }
  const posts = [post]

  test('shows a list of blog posts', () => {
    const component = display(<List posts={posts}/>)

    assert_select(component, '.posts-list .post', 1)
    assert_select(component, '.post .title', 'React on Rails')
    assert_select(component, '.post .body',  'I can use React with Rails.')
  })

  test('fetches the list of blog posts from the server', async () => {
    server.send.mockReturnValue({posts})

    const component = await display(<ConnectedList />)
    component.update()

    assert_select(component, '.posts-list .post', 1)
    expect(server.send).toBeCalledWith('/posts.json')
  })
})

A quick test in the browser shows that I can show a list of posts from the server and I can create a new one except… Ooops!… the new one doesn’t get added to the list unless I refresh the page.

This is the kind of problem that Redux will solve for us but I have a different approach in mind. We’ll give it a try in the next episode.

Forms are Simple

All the code is refactored and shiny. The tests look nice and we have the beginnings of a framework but we’ve only completed one story so far.

Let’s work on the next one.

  • An author can publish a blog post.

I’ve been using (and loving!) the simple_form gem for a while. It’s one of the things from Rails that I miss most after switching to React.

Let’s see if we can reproduce some of that functionality here. There are a lot of moving parts.

We’ll do the rails bit first because it is easy.

# posts_controller_test.rb

  test 'create a blog post' do
    post posts_url(format: :json), params: {
        post: {
            title: 'A new day',
            body: 'First post!'
        }
    }
    assert_response :success
    json = JSON.parse response.body, symbolize_names: true

    # The post was created
    @post = Post.last
    assert_equal 'A new day', @post.title

    # Return the post as JSON
    assert_equal @post.id, json[:id]
    assert_equal 'A new day', json[:title]
  end
class PostsController < ApplicationController
  def create
    @post = Post.new post_params
    if @post.save
      render :show, status: :created, location: @post
    else
      render json: @post.errors, status: :unprocessable_entity
    end
  end

  # ... existing stuff

  def post_params
    params.require(:post).permit(:title, :body)
  end
end
class PostTest < ActiveSupport::TestCase
  test 'post has a title and a body'
  test 'validate the title and body'
  test 'trim the title and body'
end

We’ll use the strip_attributes gem to trim the fields.

# Gemfile 
gem 'strip_attributes'
bundle install
class Post < ApplicationRecord
  strip_attributes
  validates :title, length: { minimum: 2, maximum: 255}
  validates :body,  length: { minimum: 2}
end

We’ll work on the client side tomorrow.

Wait a minute! It’s a post, man.

We’ll start with the Post model first. I’ll keep the commentary to a minimum because you know this stuff already.

rails generate model Post title:string body:text
rails db:migrate

We’ll need a test framework. I like Minitest.

# Gemfile 
gem 'minitest', group: :test
bundle install

I use TDD but you can do whatever you like. I’m not your dad.

I’ll fast-forward through the details.

require 'test_helper'
class PostTest < ActiveSupport::TestCase
  test 'a post has a title and a body' do
    post = Post.create! title: 'The title', body: 'The body.'
    assert_equal 'The title', post.title
    assert_equal 'The body.', post.body
  end
end
rails test

.E

Error:
PostsControllerTest#test_should_get_index:
NameError: undefined local variable or method `posts_index_url' for #<PostsControllerTest:0x00007f8939850650>
    test/controllers/posts_controller_test.rb:5:in `block in <class:PostsControllerTest>'

PostTest passes already but the auto-generated PostControllerTest is failing because the path is wrong. Let’s fix that.

require 'test_helper'
class PostsControllerTest < ActionDispatch::IntegrationTest
  test 'should get index' do
    get posts_url
    assert_response :success
  end
end
rails test

# Running:

..

Finished in 0.382970s, 5.2223 runs/s, 7.8335 assertions/s.
2 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Our React app is going to need some JSON. Let’s add a show action to PostsController.

require 'test_helper'
class PostsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @post = Post.create! title: 'The title', body: 'The body.'
  end

  test 'fetch a post as json' do
    get post_url(@post, format: :json)
    assert_response :success

    json = JSON.parse response.body, symbolize_names: true
    assert_equal @post.id, json[:id]
    assert_equal 'The title', json[:title]
    assert_equal 'The body.', json[:body]
  end
end
# PostsController.rb
class PostsController < ApplicationController
  before_action :set_post, only: :show
  
  private
  def set_post
    @post = Post.find params[:id]
  end
end

# views/posts/show.json.jbuilder
json.extract! @post, :id, :title, :body, :created_at, :updated_at

Let’s create a post so we can see what the JSON looks like.

# In the rails console
Post.create! title: 'React on Rails', body: 'I can use React with Rails.'
open http://localhost:3000/posts/1.json

That’s the back-end taken care of. We’ll build a React component to show our post in the next instalment.

From zero to one

I have Ruby v2.6.7 installed using rvm and I’ll use Homebrew to install Node and Yarn on my Mac.

Installing software tends to either go swimmingly or you end up drowning in a Pool of Despair. In the Pool of Despair case, extra instructions don’t seem to help very much so I’ll keep mine to a minimum.

gem install rails  # I'm on rails 6.1
brew install node  # javascript runtime
brew install yarn  # javascript package manager

Yarn is the equivalent of rubygems in the Javascript ecosystem. We’ll use yarn to install node packages.

Open a terminal and go to that favourite part of your hard drive where you keep all your future dreams and create a rails project.

rails new blogging --database=postgresql --webpack=react --skip-turbolinks --skip-spring

Go make a cup of tea while that is installing.

I use PostgresSQL because it makes deploying to Heroku easier. Use SQLite or MySQL if you prefer.

I don’t like Turbolinks.

On four Mac Books in a row, Spring always pegs my CPU at 100% and I don’t know why.

–webpack=react will install Webpack and the node packages for React.

Webpack replaces Sprockets for processing Javascript assets but we’ll still use Sprockets for CSS because I haven’t figured out to use Webpack for CSS yet and everyone says it’s complicated.

I’m going to push everything to Github so I can link to the code online. I just created a repository at https://github.com/klawrence/blogging

cd blogging
git init
git add .
git commit -m 'Start blogging!'

git remote add origin https://klawrence@github.com/klawrence/blogging.git
git push -u origin master

If the install gods are on our side we can create the database, start the server and see the happy, happy Welcome to Rails banner.

rails db:create
rails server

# Then in a new terminal
open http://localhost:3000/

When we created our Rails project, Webpacker created a HelloReact app. We can use that to test that React installed correctly. We’ll create a posts controller to initialize HelloReact and we’ll set it as the home page in routes.rb.

rails generate controller Posts index
#routes.rb
Rails.application.routes.draw do
  root to: 'posts#index'
  resources :posts
end

Webpacker compiles javascript modules into packs and the javascript_pack_tag method will include your pack on a page. We’ll call it from the index page.

# views/posts/index.html.erb
<%= javascript_pack_tag 'hello_react' %>

Webpacker ships with a utility that rebuilds your packs and refreshes the page whenever you change any Javascript. We’ll run it in a terminal window.

bin/webpack-dev-server

Now, when I go to http://localhost:3000/, I see the message “Hello, React”. I hope you do too.

Everything is set up now and we’re ready to start coding. In the next episode, we’ll show our first blog post.

React on Rails

When I was first learning React I went through about 17 tutorials before I finally grokked what it was all about. Which is weird because React is quite simple really.

I’m gonna build the tutorial that I wished I’d had when I started learning it.

I’ll assume my reader knows Rails very well but knows just enough Javascript to add sprinkles. I won’t try to teach you React, but I’ll point you in the right direction to learn what you need to know.

Let’s do this.

But “Do what?” you may ask?

We are going to build a blogging platform. It’s going to be Rails on the back end and React on the front end. We’re going to test with Minitest, Jest and Enzyme as we go along. We’ll deploy it on Heroku. We’ll use CircleCI for continuous integration.

We are not going to use Redux. I have an idea for a more object-oriented state management system that leverages the rich metadata that Rails already has and, hopefully, avoids all the acres of boilerplate code that every other React tutorial seems to generate. I don’t have a design for this in mind. I’m hoping one will emerge as we go along.

Let’s start with a quick planning session. What do we want our blogging platform to do?

  • An author can publish a blog post.
  • A reader can read a blog post.
  • The home page shows a list of posts, most recent first.
  • An author can edit a blog post.
  • Readers can comment on a post.
  • Comments are held for moderation.
  • The author can approve comments.
  • Notify the author when there are comments.

At some point we’ll want to upload images and have multiple authors and fancy formatting and RSS feeds and other blogphernalia but that list is enough to get us started.

I’ll tackle the second story first because I like to get something working from end to end as quickly as possible and reading a post has fewer moving parts than creating one.

Join me here to get started.

Episodes

I’ll add links to all the episodes here for folks who want to skip ahead.

  1. Install Rails, Yarn, React & create a project on GitHub.
  2. Create a Post model in Rails and show the JSON.
  3. Introducing React components.
  4. Testing React with Jest & Enzyme. Setup CircleCI.
  5. Introduce class components and load a post from the server.
  6. Mock the API call to the server.
  7. Add a test helper to make testing React components easier.
  8. Create a new post in our PostsController.
  9. Use a controlled form to create a new post.
  10. Connect our new form to the back end to create a post.
  11. Fetch a list of posts from the server.
  12. Testing asynchronous components.
  13. Managing state in a React application.
  14. CHECKPOINT: Review the plan.
  15. Add a user model for registration and sign in.
  16. Deploy to Heroku.
  17. Extract our ReactToRails library to eliminate some boilerplate.
  18. Use the ReactToRails library to build the CRUD for users.