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.
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.
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:
- When the user creates a new record through the horizontal form, it will be appended to the records table
- The user will be able to inline-edit any existing record
- Clicking on any Delete button will remove the associated record from the table
- Adding, editing or removing an existing record will update the amount boxes at the top of the page
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.
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.
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.
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.
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.
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.
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() {
...
}
}
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 extend
ing 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:
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.
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
})
}
...
}
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:
- Updates the component's state
- 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:
- Prevent the form's HTTP submit
- POST the new record information to the current URL
- 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!
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...
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:
- Detect an event inside the child Record component (onClick)
- Perform an action (send a DELETE request to the server in this case)
- Notify the parent Records component about this action (sending/receiving a handler method through props)
- 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:
- The records should disappear from the table and...
- 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.
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 update
functionality.
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!