Skip to content

Instantly share code, notes, and snippets.

@DanCouper
Last active March 4, 2016 11:00
Show Gist options
  • Save DanCouper/b6953544a34606617eb5 to your computer and use it in GitHub Desktop.
Save DanCouper/b6953544a34606617eb5 to your computer and use it in GitHub Desktop.

This is an update to https://www.airpair.com/reactjs/posts/reactjs-a-guide-for-rails-developers, with the coffeescript syntax replaced with current advised best-practice ES6 idioms. I've tried not to remove anything, only add explanation where necessary.

For ES6 reference as it relates to React, Babel has a good overview: http://babeljs.io/blog/2015/06/07/react-on-es6-plus

For ES6 reference, Exploring ES6 by Axel Rauschmayer is excellent, & does a much better job than I have attempted here. In particular, the sections on let/const, replacing concat with ..., object destructuring, arrow functions, new object literal features, and classes.

Introduction to React.js

React.js is the new popular guy around the "JavaScript Frameworks" block, and it shines for its simplicity. Where other frameworks implement a complete MVC framework, we could say React only implements the V (in fact, some people replace their framework's V with React). React applications are built over 2 main principles: Components and States. Components can be made of other smaller components, built-in or custom; the State drives what the guys at Facebook call one-way reactive data flow, meaning that our UI will react to every change of state.

One of the good things about React is that it doesn't require any additional dependencies, making it pluggable with virtually any other JS library. Taking advantage of this feature, we are going to include it into our Rails stack to build a frontend-powered application, or you might say, a Rails view on steroids.


A mock expense tracking app

For this guide, we are building a small application from scratch to keep track of our expenses; each record will consist of a date, a title and an amount. A record will be treated as Credit if its amount is greater than zero, otherwise it will be treated as Debit. Here is a mockup of the project:

Summarizing, the application will behave as follows:

  1. When the user creates a new record through the horizontal form, it will be appended to the records table
  2. The user will be able to inline-edit any existing record
  3. Clicking on any Delete button will remove the associated record from the table
  4. Adding, editing or removing an existing record will update the amount boxes at the top of the page

Initializing our React.js on Rails project

rails new accounts

For this project's UI, we'll be using Bootstrap 4, and we'll need the react-rails gem. Add to the Gemfile:

gem 'bootstrap', '~> 4.0.0.alpha3'
gem 'react-rails'

Then, (kindly) tell Rails to install the new gems:

bundle install

Import Bootstrap right at the top of application.scss:

@import "bootstrap";

react-rails comes with an installation script, which will create a components.js file and a components directory under app/assets/javascripts where our React components will live.

rails g react:install

If you take a look at your application.js file after running the installer, you will notice three new lines:

//= require react
//= require react_ujs
//= require components

Basically, it includes the actual react library, the components manifest file and a kind of familiar file ended in ujs. As you might have guessed for the file's name, react-rails includes an unobtrusive JavaScript driver which will help us to mount our React components and will also handle Turbolinks events.

Activating Stage-0 features

React has been significantly updated since the original tutorial was published, and this update aims to use current React best practices. To allow these to be used, a few lines need to be added to application.rb:*

config.react.addons = true
config.react.jsx_transform_options = {
  stage: 0
}

react.addons is not strictly necessary, but is good practise to include, as we then gain access to propType validations and the CSSTransitionGroup component wrapper that allows for CSS animations.

The jsx_transform_options set to stage: 0 is the important line: it allows properties to be added directly to JavaScript classes, rather than attaching them to the constructor.

Creating the Resource

We are going to build a Record resource, which will include a date, a title, and an amount. Instead of using the scaffold generator, we are going to use the resource generator, as we are not going to be using all of the files and methods created by the scaffoldgenerator. Another option might be running the scaffold generator and then proceed to delete the unused files/methods, but our project can turn a little messy after this. Inside your project, run the following command:

rails g resource Record title date:date amount:float

After some magic, we will end up with a new Record model, controller, and routes. We just need to create our database and run pending migrations.

rake db:create db:migrate

As a plus, you can create a couple of records through rails console:

Record.create title: 'Record 1', date: Date.today, amount: 500
Record.create title: 'Record 2', date: Date.today, amount: -100

Don't forget to start your server with rails s.

Done! We're ready to write some code.

Nesting Components: Listing Records

For our first task, we need to render any existing record inside a table. First of all, we need to create an index action inside of our RecordsController:

# app/controllers/records_controller.rb

class RecordsController < ApplicationController
  def index
    @records = Record.all
  end
end

Next, we need to create a new file index.haml under apps/views/records/. This file will act as a bridge between our Rails app and our React Components. To achieve this task, we will use the helper method react_component, which receives the name of the React component we want to render along with the data we want to pass into it.

-# app/views/records/index.haml

= react_component 'Records', data: @records

It is worth mentioning this helper is provided by the react-rails gem, if you decide to use other React integration method, this helper will not be available.

You can now navigate to localhost:3000/records. Obviously, this won't work yet because of the lack of a Records React component, but if we take a look at the generated HTML inside the browser window, we can spot something like the following code:

<div data-react-class="Records" data-react-props="{...}">
</div>

With this markup present, react_ujs will detect we are trying to render a React component and will instantiate it, including the properties we sent through react_component, in our case, the contents of @records.

The time has come for us to build our First React component, inside the javascripts/components directory, create a new file called records.jsx, this file will contain our Records component.

Note the use of JavaScript classes. The original React.CreateClass syntax is still valid, but React is moving toward using class for components with state. This does simplify things somewhat, syntax-wise.

// app/assets/javascripts/components/records.jsx

class Records extends React.Component {
  render() {
    return (
      <div className='records'>
        <h2 className='title'>Records</h2>
      </div>
    )
  }
}

Each component requires a render method, which will be in charge of rendering the component itself. The render method should return an instance of React.Component, this way, when React executes a re-render, it will be performed in an optimal way (as React detects the existence of new nodes through building a virtual DOM in memory). In the snippet above we created an instance of h2, a built-in ReactComponent.

You can refresh your browser now.

Records component

Perfect! We have rendered our first React Component. Now, it's time to display our records.

Besides the render method, React components rely on the use of properties to communicate with other components and states to detect whether a re-render is required or not. We need to initialize our component's state and properties with the desired values.

Here is a blog post from React where they give basic examples of the use of ES6 classes and ES7 class properties

Note: I built this using Rails 5, which (via Sprockets) seems to have slightly different behaviour re Babel transpiling. Try the first syntax, but on Rails 4, there may be errors. If that is the case (the error will say something like leading decorators must be attached to a class description, which doesn't make sense as there are no decorators used, but anyway), use the second style.

Style 1

This uses the static class properties proposal, which is currently being reccommended by React (it looks highly likely to be adopted, as Typescript already has this, and Angular + Ember are both pushing for it). JavaScript classes, as of the current ES2015 spec, can only have methods declared: any properties must be put onto the constructor(). The [ES7] proposal allows properties to be declared directly on the class, which simplifies things somewhat. As regards React components, it means there isn't any need to explicitly specify a constructor() method.

// app/assets/javascripts/components/records.jsx

class Records extends React.Component {
  static defaultProps = {
    records: []
  }

  state = {
    records: @props.data
  }

  render() {
    ...
  }
}

Style 2

If style 1 causes Rails to error, then revert the code to the current ES2015 class spec. With this, the constructor() is declared. It takes props as an argument, and the defaultProps and state are declared in the constructor. NOTE the call to super(props) must be the first thing in the constructor. The component extends React's Component class, and we need to access the generic methods declared there. Here is a good article covering the Class syntax, which goes into detail about extending and super().

// app/assets/javascripts/components/records.jsx

class Records extends React.Component {
  constructor(props) {
    super(props)
    this.defaultProps = {
      records: []
    }
    this.state = {
      records: this.props.data
    }
  }

  render() {
    ...
  }
}

The defaultProps will initialize our component's properties in case we forget to send any data when instantiating it, and the values described instate will generate the initial state of our component. Now we need to actually display the records provided by our Rails view.

We need to create a new Record component to display each individual record, create a new file record.jsx under the javascripts/components directory and insert the following contents:

// app/assets/javascripts/components/record.jsx

function Record({ record }) {
  const amountFormat = (amount) => `£${Number(amount).toLocaleString()}`
  return (
    <tr>
      <td>{ record.date }</td>
      <td>{ record.title }</td>
      <td>{ amountFormat(record.amount) }</td>
    </tr>
  )
}

At the moment, this is a what React terms a 'dumb' component. Unlike the Records parent component, it has no state. All it does is take the properties passed to it and render them out. This concept is quite important: as much as is possible, a React app should be made up of dumb components. These components are referentially transparent pure functions: given a specific set of data as arguments (via props), they will always return the same result. This will allow optimisations in future versions of React, but at a basic level, it makes the components very easy to reason about - they always do the same thing, never anything unexpected - and make them very, very easy to test in isolation.

Note that many tutorials will use the following style, using a function expression rather than a function declaration:**

const Record = ({ record }) => {
  const amountFormat = (amount) => `£${Number(amount).toLocaleString('en-GB', { style: 'currency', currency: 'GBP'})}`
  return (
    <tr>
      <td>{ record.date }</td>
      <td>{ record.title }</td>
      <td>{ amountFormat(record.amount) }</td>
    </tr>
  )
}

This is effectively a matter of choice, but be aware that the above creates an unnamed function, whereas function Record(args) {} creates a named function. Functions have a name property, which can be extremely helpful whilst debugging.

Note the argument passed to the function, it is an example of:

Destructuring

ES6 Javascript allows pattern matching via destructuring: what this means is that you can do this:

// Take an object:
const user = {
  name: 'Dan',
  job: 'developer'
}

// pre-ES6 you would do this:
function printUserES5(user) {
  return user.name + ' is a ' + user.job
}

// But in ES6, you can grab just the items you want.
// instead of passing the whole object, you can pass in just the keys:
function printUserES6({ name, job }) {
  return `${name} is a ${job}`
}

> printUserES5(user)
Dan is a developer
> printUserES6(user)
Dan is a developer

// This isn't enormously useful on its own, but for React, where we
// pass `props` down to components, those props are, naturally,
// object properties - `{ propName1: prop1, propName2: prop2, ... }`.
// So a parent component could have something like this:

// users.jsx
// =========
class Users extends React.Component {
  static defaultProps = {
    users: []
  }
  state = {
    users: this.state.users
  }
  render() {
    return (
      <ul className="users">
      // The `User` component is given the `user` props...
      { this.state.users.map(user => (<User user={ user } />)) }
      </ul>
    )
  }
}

// user.jsx
// ========
// ...and the `user` props are destructured here,
// where `name` and `job` are extracted:
function User({ name, job }) {
  return (
    <li>{ `${name} is a ${job}` }</li>
  )
}

So, going through Record line-by-line:

The entire component is just a function that returns the JS objects described by the JSX. It takes the props objects as an argument, using destructuring to extract the record property:

function Record({ record }) {

We use a helper to format the amount, using a function called amountFormat. amountFormat takes an amount as an argument. It then converts this to a number, and formats it correctly (for example the dot in 10.50 is a UK/US convention, in the rest of Europe it might be formatted as 10,50). This is interpolated into a string.

  const amountFormat = (amount) => `£${Number(amount).toLocaleString('en-GB', { style: 'currency', currency: 'GBP'})}`

Rails provides the number_to_currency helper. If it were used as an anonymous function (not that it would, this is just for comparison) like above:

amount_format = lambda { |amount| number_to_currency(amount, locale: :gb, unit: '£') }

Finally, the component's JSX markup is returned:

  return (
    <tr>
      <td>{ record.date }</td>
      <td>{ record.title }</td>
      <td>{ amountFormat(record.amount) }</td>
    </tr>
  )
}

The Record component will display a table row containing table cells for each record attribute. Now update the render method inside the Records component with the following code:

// app/assets/javascripts/components/records.jsx

class Records extends React.Component {
  ...
  render() {
    return (
      <div className="records">
        <h2 className="title">Records</h2>
        <table className="table table-bordered">
          <thead>
            <tr>
              <th>Date</th>
              <th>Title</th>
              <th>Amount</th>
            </tr>
          </thead>
          <tbody>
            { this.state.records.map(record => (<Record key={ record.id } record={ record } />)
            }
          </tbody>
      </div>
    )
  }
}

Did you see what just happened? We created a table with a header row, and inside of the body table we are creating a Record element for each existing record. In other words, we are nesting built-in/custom React components. Pretty cool, huh?

NOTE: the for...in loop used in the original tutorial has been replaced with a map. It could also have been a for...of loop, for example:

          <tbody>{
            for (let record of this.state.records) {
              return (<Record key={ record.id } record={ record } />)
            }
          }</tbody>

Using for...in is discouraged at the best of times: it has a specific usecase (iterating through Object properties: they are not enumerable any other way). It definitely should not be used to iterate over an array; for one thing, it is impossible for any existing JS engine to optimise it.

When we handle dynamic children (in this case, records) we need to provide a key property to the dynamically generated elements so React won't have a hard time refreshing our UI, that's why we send key={ record.id } along with the actual record when creating Record elements. If we don't do so, we will receive a warning message in the browser's JS console (and probably some headaches in the near future).

You can take a look at the resulting code of this section here, or just the changes introduced by this section here.

Parent-Child communication: Creating Records

Now that we are displaying all the existing records, it would be nice to include a form to create new records, let's add this new feature to our React/Rails application.

First, we need to add the create method to our Rails controller (don't forget to use _strongparams):

class RecordsController < ApplicationController
  ...

  def create
    @record = Record.new(record_params)

    if @record.save
      render json: @record
    else
      render json: @record.errors, status: :unprocessable_entity
    end
  end

  private

    def record_params
      params.require(:record).permit(:title, :amount, :date)
    end
end

Next, we need to build a React component to handle the creation of new records. The component will have its own state to store date, title and amount. Create a new record_form.jsx file under javascripts/components with the following code:

class RecordForm extends React.Component {
    state: {
      title: '',
      date: '',
      amount: ''
    }

    render() {
      return (
        <form className="form-inline">
          <div className="form-group">
            <input type="text" className="form-control" placeholder="Date" name="date" value={ this.state.date } onChange={ this.handleChange } />
          </div>
          <div className="form-group">
            <input type="text" className="form-control" placeholder="Title" name="title" value={ this.state.title } onChange={ this.handleChange } />
          </div>
          <div className="form-group">
            <input type="number" className="form-control" placeholder="Amount" name="amount" value={ this.state.amount } onChange={ this.handleChange } />
          </div>
          <button type="submit" className="btn btn-primary" disabled={ !this.valid() }>Create record</button>
        </form>
      )
    }
}

Nothing too fancy, just a regular Bootstrap inline form. Notice how we are defining the value attribute to set the input's value and the onChange attribute to attach a handler method which will be called on every keystroke; the handleChange handler method will use the name attribute to detect which input triggered the event and update the related state value:

class RecordForm extends React.Component {
  ...
  handleChange = (e) => {
    const name = e.target.name
    return this.setState({
      [`${name}`]: e.target.value
    })
  }

  ...
}

Going through line-by-line:

The handleChange method is an anonymous functions set as a property directly on the RecordForm class:

  handleChange = (e) => {

Note the use of const; const can and should be used insead of the (effectively obselete) var as much as possible. const cannot be rebound, so you cannot do something like const a = 'foo', then later on a = bar. If this behaviour is needed (for example, in a loop), use let instead - let a = 'foo'; a = 'bar':

    const name = e.target.name
    return this.setState({

Note the use of a computed property name. ES6 allows object keys to be dynamically created using { [keyName]: value }. Note also that the key name in this case is being converted to a string (using the string interpolation syntax): this is just for safety - object keys have to be strings in JavaScript, so this avoids any errors:

      [`${name}`]: e.target.value
    })
  }

  ...
}

Digression [that will be slightly confusing if you don't fully get how this works]

Arrow functions are used for the event handlers. If the handler method above were written like so:

class RecordForm extends React.Component {
  ...

  handleChange(e) {
    const name = e.target.name
    return this.setState({
      [name]: e.target.value
    })
  }

  ...
}

Then, when the handler method was called, this would not refer to the class: it would refer instead to the global window objectby default, and the call would need to be bound manually, like onChange={ this.handleChange.bind(this) }. But arrow functions pick up this from their surroundings: if handleChange(e) { is changed to handleChange = (e) => {, in the arrow function this refers to the outer scope, the component class.

For reference, arrow functions lexically bind this (as well as super, arguments and new.target), and allow the event handler to operate as expected (rather than grabbing the this value from the environment the handler was called from).


Why do we have to use the this.setState() method? Why can't we just set the desired value of this.state as we usually do in regular JS Objects? Because this.setState will perform two actions:

  1. Updates the component's state
  2. Schedules a UI verification/refresh based on the new state

It is very important to have this information in mind every time we use state inside our components.

Lets take a look at the submit button, just at the very end of the render method:

# app/assets/javascripts/components/record_form.jsx

class RecordForm extends React.Component {
  ...
  render() {
    ...
    <form>
      ...
      <button type="submit" className="btn btn-primary" disabled={ !this.valid() }>'Create record'</button>

We defined a disabled attribute with the value of !this.valid(), meaning that we are going to implement a valid method to evaluate if the data provided by the user is correct.

class RecordForm extends React.Component {
  ...
  isValid() {
    return this.state.title && this.state.date && this.state.amount
  }

For the sake of simplicity we are only validating this.state attributes against empty strings. This way, every time the state gets updated, the Create record button is enabled/disabled depending on the validity of the data.

Now that we have our controller and form in place, it's time to submit our new record to the server. We need to handle the form's submit event. To achieve this task, we need to add an onSubmit attribute to our form and a new handleSubmit method (the same way we handled onChange events before):

# app/assets/javascripts/components/record_form.jsx

class RecordForm extends React.Component {
  ...
  handleSubmit = (e) => {
    e.preventDefault()
    return $.post('', {
      record: this.state
    }, (data) => {
        this.props.handleNewRecord(data)
        return this.setState(this.getInitialState())
    }, 'JSON')
  }

  render() {
    <form className="form-inline" onSubmit={ this.handleSubmit } >
    ...

Let's review the new method line by line:

  1. Prevent the form's HTTP submit
  2. POST the new record information to the current URL
  3. Success callback

The success callback is the key of this process, after successfully creating the new record someone will be notified about this action and the state is restored to its initial value. Do you remember early in the post when I mentioned that components communicate with other components through properties (or this.props)? Well, this is it. Our current component sends data back to the parent component through this.props.handleNewRecord to notify it about the existence of a new record.

As you might have guessed, wherever we create our RecordForm element, we need to pass a handleNewRecord property with a method reference into it, something like <RecordForm handleNewRecord={ this.addRecord } />. Well, the parent Records component is the "wherever", as it has a state with all of the existing records, we need to update its state with the newly created record.

Add the new addRecord method inside records.jsx and create the new RecordForm element, just after the h2 title (inside the render method):

# app/assets/javascripts/components/records.jsx

class Records extends React.Component {
  ...
  addRecord = (record) => {
    return this.setState({ records: this.state.records.concat(record) })
  }
  ...
  render()
    <div className="records">
      <h2 className="title">Records</h2>
      <RecordForm handleNewRecord={ this.addRecord } />
    ...

NOTE the use of concat. this.state.records is an array; in the original tutorial, a copy of the records array was created via slice(), the new record was added to the copy using push(record), then this.state.records was set as this new array. Just using concat to create a new array with the extra record added on get rid of the intermediate steps. ES6' magic rest/spread parameter can also acheive this, is a generally better approach:

class Records extends React.Component {
  ...
  addRecord = (record) => {
    return this.setState({ records: [...this.state.records, record] })
  }

The above grabs all the existing this.state.record array values, and puts them into a new array with the extra record. This approach is more flexible, extremely explicit, and advised - for example, if there was a situation where records needed to be added to the front of the array, doing [record, ....this.state.records] works just fine. If there were a situation where a few items needed to be added to an array in different places, [thing1, thing2, ...this.state.records, record, thing3] works just fine as well.

Quite important note related to above: do not mutate the state, instead create a new version when anything changes. Return a new version of it, always.

Refresh your browser, fill in the form with a new record, click Create record... No suspense this time, the record was added almost immediately and the form gets cleaned after submit, refresh again just to make sure the backend has stored the new data.

If you have used other JS frameworks along with Rails (for example, AngularJS) to build similar features, you might have run into problems because your POST requests don't include the CSRF token required by Rails, so, why didn't we run into this same issue? Easy, because we are using jQuery to interact with our backend, and Rails' jquery_ujs unobtrusive driver will include the CSRF token on every AJAX request for us. Cool!

Reusable Components: Amount Indicators

What would an application be without some (nice) indicators? Let's add some boxes at the top of our window with some useful information. We goal for this section is to show 3 values: Total credit amount, total debit amount and Balance. This looks like a job for 3 components, or maybe just one with properties?

We can build a very simple AmountBox component which will receive three properties: amount, text and type. Create a new file called amount_box.jsx under javascripts/components/ and paste the following code:

function AmountBox({ amount, text, type }) {
  return (
    <div className={ `card card-inverse card-${ type }` }>
      <div className='card-block'>
        <h4 className='card-title'>{ text }</h4>
        <p className='card-text'>£{ amount.toLocaleString('en-GB', { style: 'currency', currency: 'GBP'}) }</p>
      </div>
    </div>
  )
}

We are just using Bootstrap's card element to display the information in a "blocky" way, and setting the color through the type property. We have also included a really simple amount formatter method called amountFormat which reads the amount property and displays it in currency format.

In order to have a complete solution, we need to create this element (3 times) inside of our main component, sending the required properties depending on the data we want to display. Let's build the calculator methods first, open the Records component and add the following methods:

class Records extends React.Component {
  ...
  credits() {
    return this.state
               .records
               .filter(val => val.amount >= 0)
               .reduce((prev, curr) => prev + parseFloat(curr.amount), 0)
  }

  debits() {
    return this.state
               .records
               .filter(val => val.amount < 0)
               .reduce((prev, curr) => prev + parseFloat(curr.amount), 0)
  }

  balance() {
    return this.debits() + this.credits()
  }

credits sums all the records with an amount greater than 0, debits sums all the records with an amount less than 0 and balance is self-explanatory. Now that we have the calculator methods in place, we just need to create the AmountBox elements inside the render method (just above the RecordForm component):

class Records extends React.Component {
  ...
  render() {
    return (
      <div className='records'>
        ...
        <div className='card-group m-b-2'>
          <AmountBox type='success' amount={ this.credits() } text='Credit' />
          <AmountBox type='danger' amount={ this.debits() } text='Debit' />
          <AmountBox type='info' amount={ this.balance() } text='Balance' />
        </div>
        <RecordForm handleNewRecord={ this.addRecord } />

We are done with this feature! Refresh your browser, you should see three boxes displaying the amounts we've calculated earlier. But wait! There's more! Create a new record and see the magic work...

setState/replaceState: Deleting Records

The next feature in our list is the ability to delete records, we need a new Actions column in our records table, this column will have a Delete button for each record, pretty standard UI. As in our previous example, we need to create the destroy method in our Rails controller:

# app/controllers/records_controller.rb

class RecordsController < ApplicationController
  ...

  def destroy
    @record = Record.find(params[:id])
    @record.destroy
    head :no_content
  end

  ...
end

That is all the server-side code we will need for this feature. Now, open your Records React component and add the Actions column at the rightmost position of the table header:

class Records extends React.Component {
  render() {
    return (
      <div className='records'>
        ...
        <table className='table'>
          <caption>Records (oldest-newest)</caption>
          <thead>
            <tr>
              <th>Date</th>
              <th>Title</th>
              <th>Amount</th>
              <th className='text-xs-right'>Actions</th>
            </tr>
          </thead>
          ...

And finally, we open the Record component and add an extra column with a Delete link. However, currently the component is a function, and has no state. The entire state can be passed down from the parent Records component. For deleting that's no problem, but we will want to edit as well as delete, and in that case it would be useful to have state held while the component was actually being edited. So we refactor it to a class, and add the delete handler:

class Record extends React.Component {
  render() {
    return (
      <tr>
        <td><input className='form-control' type='text' defaultValue={ this.props.record.date } ref='date' /></td>
        <td><input className='form-control' type='text' defaultValue={ this.props.record.title } ref='title' /></td>
        <td><input className='form-control' type='number' defaultValue={ this.props.record.amount } ref='amount' /></td>
        <td className='text-xs-right'>
          <a className='btn btn-danger'>Delete</a>
          </div>
        </td>
      </tr>
    )
  }

Save your file, refresh your browser and... We have a useless button with no events attached to it!

Let's add some functionality to it. As we learned from our RecordForm component, the way to go here is:

  1. Detect an event inside the child Record component (onClick)
  2. Perform an action (send a DELETE request to the server in this case)
  3. Notify the parent Records component about this action (sending/receiving a handler method through props)
  4. Update the Record component's state

To implement step 1, we can add a handler for onClick to Record the same way we added a handler for onSubmit to RecordForm to create new records. Fortunately for us, React implements most of the common browser events in a normalized way, so we don't have to worry about cross-browser compatibility (you can take a look at the complete events list here).

Re-open the Record component, add a new handleDelete method and an onClick attribute to our "useless" delete button as follows:

class Record extends React.Component {
  handleDelete = (e) => {
    e.preventDefault()
    return $.ajax({
      method: 'DELETE',
      url: `records/${ this.props.record.id }`,
      dataType: 'JSON',
      success: () => {
        return this.props.handleDeleteRecord(this.props.record)
      }
    })
  }

  render() {
    return (
      <tr>
        ...
        <td className='text-xs-right'>
          <a className='btn btn-danger' onClick={ this.handleDelete }>Delete</a>
          </div>
        </td>
      </tr>
    )
  }
}

When the delete button gets clicked, handleDelete sends an AJAX request to the server to delete the record in the backend and, after this, it notifies the parent component about this action through the handleDeleteRecord handler available through props, this means we need to adjust the creation of Record elements in the parent component to include the extra property handleDeleteRecord, and also implement the actual handler method in the parent:

class Records extends React.Component {
  ...
  deleteRecord = (record) => {
    const records = this.state.records
    return this.setState({ records: records.filter(curr => curr.id !== record.id )})
  }

  ...
   render() {
    return (
        ...
          <tbody>
            { this.state.records.map(record => (
                <Record
                  key={ record.i }
                  record={ record }
                  handleDeleteRecord={ this.deleteRecord }
                />
              ))
            }
          </tbody>
        </table>
      </div>
    )
  }
}

Basically, our deleteRecord method copies the current component's records state, performs an index search of the record to be deleted, filters it out of the array, and updates the component's state, pretty standard JavaScript operations.

NOTE the original tutorial introduced a new way of interacting with the state, replaceState; the main difference between setState and replaceState being that the first one will only update one key of the state object, the second one will completely override the current state of the component with whatever new object we send. replaceState cannot be used with ES6 classes, and the react documentation states that it may be removed in a future version of React.

After updating this last bit of code, refresh your browser window and try to delete a record, a couple of things should happen:

  1. The records should disappear from the table and...
  2. The indicators should update the amounts instantly, no additional code is required

We are almost done with our application, but before implementing our last feature, we can apply a small refactor and, at the same time, introduce a new React feature.

You can take a look at the resulting code of this section here, or just the changes introduced by this section here.

Reactive Data Flow: Editing Records

For the final feature, we are adding an extra Edit button, next to each Delete button in our records table. When this Edit button gets clicked, it will toggle the entire row from a read-only state (wink wink) to an editable state, revealing an inline form where the user can update the record's content. After submitting the updated content or canceling the action, the record's row will return to its original read-only state.

As you might have guessed from the previous paragraph, we need to handle mutable data to toggle each record's state inside of our Record component. This is a use case of what React calls reactive data flow. Let's add an edit flag and a handleToggle method to record.jsx:

class Record extends React.Component {
  state = {
    edit: false
  }

  handleToggle = (e) => {
    e.preventDefault()
    return this.setState({ edit: !this.state.edit })
  }
  ...

The edit flag will default to false, and handleToggle will change edit from false to true and vice versa, we just need to trigger handleToggle from a user onClick event.

Now, we need to handle two row versions (read-only and form) and display them conditionally depending on edit. Luckily for us, as long as our render method returns a React element, we are free to perform any actions in it; we can define a couple of helper methods recordRow and recordForm and call them conditionally inside of render() depending on the contents of this.state.edit.

We already have an initial version of recordRow, it's our current render method. Let's move the contents of render to our brand new recordRow method and add some additional code to it:

class Record extends React.Component {
  ...
  recordRow() {
    return (
      <tr>
        <td>{ this.props.record.date }</td>
        <td>{ this.props.record.title }</td>
        <td>£{ this.props.record.amount.toLocaleString() }</td>
        <td className='text-xs-right'>
          <div className='btn-group' role='group' aria-label='Options'>
            <a className='btn btn-warning' onClick={ this.handleToggle }>Edit</a>
            <a className='btn btn-danger' onClick={ this.handleDelete }>Delete</a>
          </div>
        </td>
      </tr>
    )
  }

We only added an additional React.DOM.a element which listens to onClick events to call handleToggle.

Moving forward, the implementation of recordForm will follow a similar structure, but with input fields in each cell. We are going to use a new ref attribute for our inputs to make them accessible. This new attribute will let our component read the data provided by the user through this.refs. NOTE: refs are only available to components created via the class (or React.create.Class) syntax: in dumb, stateless components created with functions, refs will always return null.

class Record extends React.Component {
  ...
  recordForm() {
    return (
      <tr>
        <td><input className='form-control' type='text' defaultValue={ this.props.record.date } ref='date' /></td>
        <td><input className='form-control' type='text' defaultValue={ this.props.record.title } ref='title' /></td>
        <td><input className='form-control' type='number' defaultValue={ this.props.record.amount } ref='amount' /></td>
        <td className='text-xs-right'>
          <div className='btn-group' role='group' aria-label='Options'>
            <a className='btn btn-success' onClick={ this.handleEdit }>Update</a>
            <a className='btn btn-danger' onClick={ this.handleToggle }>Cancel</a>
          </div>
        </td>
      </tr>
    )
  }

Notice we are calling this.handleEdit when the user clicks on the Update button, we are about to use a similar flow as the one implemented to delete records.

Do you notice something different on how React.DOM.inputs are being created? We are using defaultValue instead of value to set the initial input values, this is because using just value without onChange will end up creating read-only inputs.

Finally, the render method boils down to the following code:

class Record extends React.Component {
  ...
  render() {
    return this.state.edit ? this.recordForm() : this.recordRow()
  }
}

You can refresh your browser to play around with the new toggle behavior, but don't submit any changes yet as we haven't implemented the actual updatefunctionality.

To handle record updates, we need to add the update method to our Rails controller:

# app/controllers/records_controller.rb

class RecordsController < ApplicationController
  ...
  def update
    @record = Record.find(params[:id])
    if @record.update(record_params)
      render json: @record
    else
      render json: @record.errors, status: :unprocessable_entity
    end
  end
  ...
end

Back to our Record component, we need to implement the handleEdit method which will send an AJAX request to the server with the updated record information, then it will notify the parent component by sending the updated version of the record via the handleEditRecord method, this method will be received through this.props, the same way we did it before when deleting records:

class Record extends React.Component {
  ...
  handleEdit = (e) => {
    e.preventDefault()
    const data = {
      title: ReactDOM.findDOMNode(this.refs.title).value,
      date: ReactDOM.findDOMNode(this.refs.date).value,
      amount: ReactDOM.findDOMNode(this.refs.amount).value
    }
    return $.ajax({
      method: 'PUT',
      url: `records/${ this.props.record.id }`,
      dataType: 'JSON',
      data: { record: data },
      success: (data) => {
        this.setState({ edit: false })
        return this.props.handleEditRecord(this.props.record, data)
      }
    })
  }

For the sake of simplicity, we are not validating user data, we just read it through ReactDOM.findDOMNode(this.refs.fieldName).value and sending it verbatim to the backend. Updating the state to toggle edit mode on success is not mandatory, but the user will definitely thank us for that.

FIXME: avoid ReactDOM.findDOMNode; it's a last-resort getout; explicitly passing the values is far, far better.

Last, but not least, we just need to update the state on the Records component to overwrite the former record with the newer version of the child record and let React perform its magic. The implementation might look like this:

class Records extends React.Component {
  ...
  updateRecord = (record, data) => {
    return this.setState({
      records: this.state.records.map(curr => {
        return (curr.id === record.id) ? Object.assign(curr, data) : curr
      })
    })
  }

  render() {
    return (
        ...
        <table className='table'>
          <caption>Records (oldest-newest)</caption>
          <thead>
            <tr>
              <th>Date</th>
              <th>Title</th>
              <th>Amount</th>
              <th className='text-xs-right'>Actions</th>
            </tr>
          </thead>
          <tbody>
            { this.state.records.map(record => (
                <Record
                  key={ record.i }
                  record={ record }
                  handleDeleteRecord={ this.deleteRecord }
                  handleEditRecord={ this.updateRecord }
                />
              ))
            }
          </tbody>
        </table>
      </div>
    )
  }
}

The final link between Records and Record is the method this.updateRecord sent through the handleEditRecord property. Note the Object.assign in the setState. What updateRecord is doing is returning a brand new records array - as with the addRecord and deleteRecord methods. But the updateRecord method has to modify the individual record. Each individual record is an object, and only the properties that have been updated should change. Object.assign is an ES6 addition to JS that works the same as jQuery's $.extend or underscore/lodash's _.extend: it returns a new object that takes [in this case] two objects, and merges the second into the first, overwriting any identically-keyed properties. eg:

const obj1 = { prop1: 'foo', prop2: 1 }
const obj2 = { prop1: 'bar', prop2: 1, prop3: 'an extra prop' }

> Object.assign(obj1, obj2)
{ prop1: 'bar', prop2: 1, prop3: 'an extra prop' }

Refresh your browser for the last time and try updating some existing records, notice how the amount boxes at the top of the page keep track of every record you change.

We are done! Smile, we have just built a small Rails + React application from scratch!

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