Crud for Users

Now we have our ReactToRails library in place, adding the Crud for users should be easy.

Here’s the store.

import Store from 'react-to-rails/store'
export const store = new Store('user')

Here’s the User view component.

export function User({user}) {
  return <div className='user'>
    <h2 className='name'>{user.name}</h2>
    <div className='email'>{user.email}</div>
  </div>
}

export default connectView(User, store)

And here’s the list:

export function List({users}) {
  return <ul className='users-list'>
    {
      users.map(user => <li key={user.id}>
        <User user={user}/>
      </li>)
    }
  </ul>
}

export default connectList(List, store)

There’s a bit of boilerplate-like overlap between the views and it’s tempting to try to abstract that away into the library but I actually like what Rails does with scaffolded views where it generates something simple that you can later edit. Maybe I’ll do that. Maybe I’ll do both.

I still need to build the Editor for users but that can wait until I get a bit further along with my understanding of forms in React. I think I want to work on a proper navigation scheme for the app first. It’s all a bit of a mess at the moment. But first we’ll have to learn about how to handle routes in React.

We’ll do that next week.

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.

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.