Want help from a top reactjs developer?
Fernando Villalobos is available for live reactjs pair programming on AirPair.
>> Video chat with Fernando Villalobos
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:- 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
Initializing our React.js on Rails project
First things first, we need to start our brand new Rails project, we are naming itAccounts
: rails new accounts
For this project's UI, we'll be using Twitter Bootstrap.
The installation process is out of the scope of this post, but you can
install the bootstrap-sass
official gem following the instructions from the official github repo.Once our project has been initialized, we proceed to include React. For this post I decided to include it via the official gem react-rails because we are going to take advantage of some cool features included in this gem, but there are other ways to achieve this task, like using Rails assets or even downloading the source package from the official page and pasting it into our
javascripts
folder.If you have been developing Rails apps, you know how easy it is to install a gem: Add
react-rails
to your Gemfile. gem 'react-rails', '~> 1.0'
Then, (kindly) tell Rails to install the new gems: bundle install
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.Creating the Resource
We are going to build aRecord
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 scaffold
generator. 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
Done! We're ready to write some code.rails s
.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 anindex
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.html.erb
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.html.erb %>
<%= 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.js.coffee
, this file will contain our Records
component. # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
render: ->
React.DOM.div
className: 'records'
React.DOM.h2
className: 'title'
'Records'
Each component requires a render
method, which will be in charge of rendering the component itself. The render method should return an instance of ReactComponent
,
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
.NOTE: Another way to instantiate ReactComponents inside the render method is through
JSX
syntax. The following snippet is equivalent to the previous one: render: ->
`<div className="records">
<h2 className="title"> Records </h2>
</div>`
Personally, when I am working with CoffeeScript, I prefer using the React.DOM
syntax over JSX
because the code will arrange in a hierarchical structure by itself, similar to HAML. On the other hand, if you are trying to integrate React into an existing application built with erb, you have the option to reuse your existing erb code and convert it into JSX.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: # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
getInitialState: ->
records: @props.data
getDefaultProps: ->
records: []
render: ->
...
The method getDefaultProps
will initialize our component's properties in case we forget to send any data when instantiating it, and the getInitialState
method will generate the initial state of our component. Now we need to
actually display the records provided by our Rails view.It looks like we are going to need a helper method to format amount strings, we can implement a simple string formatter and make it accesible to all of our coffee files. Create a new
utils.js.coffee
file under javascripts/
with the following contents: # app/assets/javascripts/utils.js.coffee
@amountFormat = (amount) ->
'$ ' + Number(amount).toLocaleString()
We need to create a new Record
component to display each individual record, create a new file record.js.coffee
under the javascripts/components
directory and insert the following contents: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
render: ->
React.DOM.tr null,
React.DOM.td null, @props.record.date
React.DOM.td null, @props.record.title
React.DOM.td null, amountFormat(@props.record.amount)
The Record
component will display a table row containing table cells for each record attribute. Don't worry about those null
s in the React.DOM.*
calls, it means we are not sending attributes to the components. Now update the render
method inside the Records
component with the following code: # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
render: ->
React.DOM.div
className: 'records'
React.DOM.h2
className: 'title'
'Records'
React.DOM.table
className: 'table table-bordered'
React.DOM.thead null,
React.DOM.tr null,
React.DOM.th null, 'Date'
React.DOM.th null, 'Title'
React.DOM.th null, 'Amount'
React.DOM.tbody null,
for record in @state.records
React.createElement Record, key: record.id, record: record
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?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 strong_params): # app/controllers/records_controller.rb
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.js.coffee
file under javascripts/components
with the following code: # app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
getInitialState: ->
title: ''
date: ''
amount: ''
render: ->
React.DOM.form
className: 'form-inline'
React.DOM.div
className: 'form-group'
React.DOM.input
type: 'text'
className: 'form-control'
placeholder: 'Date'
name: 'date'
value: @state.date
onChange: @handleChange
React.DOM.div
className: 'form-group'
React.DOM.input
type: 'text'
className: 'form-control'
placeholder: 'Title'
name: 'title'
value: @state.title
onChange: @handleChange
React.DOM.div
className: 'form-group'
React.DOM.input
type: 'number'
className: 'form-control'
placeholder: 'Amount'
name: 'amount'
value: @state.amount
onChange: @handleChange
React.DOM.button
type: 'submit'
className: 'btn btn-primary'
disabled: !@valid()
'Create record'
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: # app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
...
handleChange: (e) ->
name = e.target.name
@setState "#{ name }": e.target.value
...
We are just using string interpolation to dynamically define object keys, equivalent to @setState title: e.target.value
when name
equals title
. But why do we have to use @setState
? Why can't we just set the desired value of @state
as we usually do in regular JS Objects? Because @setState
will perform 2 actions, it:- Updates the component's
state
- Schedules a UI verification/refresh based on the new state
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.js.coffee
@RecordForm = React.createClass
...
render: ->
...
React.DOM.form
...
React.DOM.button
type: 'submit'
className: 'btn btn-primary'
disabled: !@valid()
'Create record'
We defined a disabled
attribute with the value of !@valid()
, meaning that we are going to implement a valid
method to evaluate if the data provided by the user is correct. # app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
...
valid: ->
@state.title && @state.date && @state.amount
...
For the sake of simplicity we are only validating @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.js.coffee
@RecordForm = React.createClass
...
handleSubmit: (e) ->
e.preventDefault()
$.post '', { record: @state }, (data) =>
@props.handleNewRecord data
@setState @getInitialState()
, 'JSON'
render: ->
React.DOM.form
className: 'form-inline'
onSubmit: @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
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 @props)? Well, this is it. Our current component sends data back to the parent component through @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 React.createElement RecordForm, handleNewRecord: @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.js.coffee
and create the new RecordForm
element, just after the h2
title (inside the render
method). # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
addRecord: (record) ->
records = @state.records.slice()
records.push record
@setState records: records
render: ->
React.DOM.div
className: 'records'
React.DOM.h2
className: 'title'
'Records'
React.createElement RecordForm, handleNewRecord: @addRecord
React.DOM.hr null
...
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!You can take a look at the resulting code of this section here, or just the changes introduced by this section here.
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 new
AmountBox
component which will receive three properties: amount
, text
and type
. Create a new file called amount_box.js.coffee
under javascripts/components/
and paste the following code: # app/assets/javascripts/components/amount_box.js.coffee
@AmountBox = React.createClass
render: ->
React.DOM.div
className: 'col-md-4'
React.DOM.div
className: "panel panel-#{ @props.type }"
React.DOM.div
className: 'panel-heading'
@props.text
React.DOM.div
className: 'panel-body'
amountFormat(@props.amount)
We are just using Bootstrap's panel
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: # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
credits: ->
credits = @state.records.filter (val) -> val.amount >= 0
credits.reduce ((prev, curr) ->
prev + parseFloat(curr.amount)
), 0
debits: ->
debits = @state.records.filter (val) -> val.amount < 0
debits.reduce ((prev, curr) ->
prev + parseFloat(curr.amount)
), 0
balance: ->
@debits() + @credits()
...
credits
sums all the records with an amount greater than 0, debits
sums all the records with an amount lesser 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): # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
render: ->
React.DOM.div
className: 'records'
React.DOM.h2
className: 'title'
'Records'
React.DOM.div
className: 'row'
React.createElement AmountBox, type: 'success', amount: @credits(), text: 'Credit'
React.createElement AmountBox, type: 'danger', amount: @debits(), text: 'Debit'
React.createElement AmountBox, type: 'info', amount: @balance(), text: 'Balance'
React.createElement RecordForm, handleNewRecord: @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...You can take a look at the resulting code of this section here, or just the changes introduced by this section here.
setState/replaceState: Deleting Records
The next feature in our list is the ability to delete records, we need a newActions
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: # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
render: ->
...
# almost at the bottom of the render method
React.DOM.table
React.DOM.thead null,
React.DOM.tr null,
React.DOM.th null, 'Date'
React.DOM.th null, 'Title'
React.DOM.th null, 'Amount'
React.DOM.th null, 'Actions'
React.DOM.tbody null,
for record in @state.records
React.createElement Record, key: record.id, record: record
And finally, open the Record
component and add an extra column with a Delete link: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
render: ->
React.DOM.tr null,
React.DOM.td null, @props.record.date
React.DOM.td null, @props.record.title
React.DOM.td null, amountFormat(@props.record.amount)
React.DOM.td null,
React.DOM.a
className: 'btn btn-danger'
'Delete'
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
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: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
handleDelete: (e) ->
e.preventDefault()
# yeah... jQuery doesn't have a $.delete shortcut method
$.ajax
method: 'DELETE'
url: "/records/#{ @props.record.id }"
dataType: 'JSON'
success: () =>
@props.handleDeleteRecord @props.record
render: ->
React.DOM.tr null,
React.DOM.td null, @props.record.date
React.DOM.td null, @props.record.title
React.DOM.td null, amountFormat(@props.record.amount)
React.DOM.td null,
React.DOM.a
className: 'btn btn-danger'
onClick: @handleDelete
'Delete'
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: # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
deleteRecord: (record) ->
records = @state.records.slice()
index = records.indexOf record
records.splice index, 1
@replaceState records: records
render: ->
...
# almost at the bottom of the render method
React.DOM.table
React.DOM.thead null,
React.DOM.tr null,
React.DOM.th null, 'Date'
React.DOM.th null, 'Title'
React.DOM.th null, 'Amount'
React.DOM.th null, 'Actions'
React.DOM.tbody null,
for record in @state.records
React.createElement Record, key: record.id, record: record, handleDeleteRecord: @deleteRecord
Basically, our deleteRecord
method copies the current component's records
state, performs an index search of the record to be deleted, splices it
from the array and updates the component's state, pretty standard
JavaScript operations.We introduced a new way of interacting with the state,
replaceState
; the main difference between setState
and replaceState
is 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.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
You can take a look at the resulting code of this section here, or just the changes introduced by this section here.
Refactor: State Helpers
At this point, we have a couple of methods where the state gets updated without any difficulty as our data is not what you might call "complex", but imagine a more complex application with a multi-level JSON state, you can picture yourself performing deep copies and juggling with your state data. React includes some fancy state helpers to help you with some of the heavy lifting, no matter how deep your state is, these helpers will let you manipulate it with more freedom using a kind-of MongoDB's query language (or at least that's what React's documentation says).Before using these helpers, first we need to configure our Rails application to include them. Open your project's
config/application.rb
file and add config.react.addons = true
at the bottom of the Application block: # config/application.rb
...
module Accounts
class Application < Rails::Application
...
config.react.addons = true
end
end
For the changes to take effect, restart your rails server, I repeat, restart your rails server. Now we have access to the state helpers through React.addons.update
, which will process our state object (or any other object we send to it) and apply the provided commands. The two commands we will be using are $push
and $splice
(I'm borrowing the explanation of these commands from the official React documentation):-
{$push: array}
push()
all the items inarray
on the target. -
{$splice: array of arrays}
for each item inarrays
callsplice()
on the target with the parameters provided by the item.
addRecord
and deleteRecord
from the Record
component using these helpers, as follows: # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
addRecord: (record) ->
records = React.addons.update(@state.records, { $push: [record] })
@setState records: records
deleteRecord: (record) ->
index = @state.records.indexOf record
records = React.addons.update(@state.records, { $splice: [[index, 1]] })
@replaceState records: records
Shorter, more elegant and with the same results, feel free to reload your browser and ensure nothing got broken.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.js.coffee
: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
getInitialState: ->
edit: false
handleToggle: (e) ->
e.preventDefault()
@setState edit: !@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 @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: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
...
recordRow: ->
React.DOM.tr null,
React.DOM.td null, @props.record.date
React.DOM.td null, @props.record.title
React.DOM.td null, amountFormat(@props.record.amount)
React.DOM.td null,
React.DOM.a
className: 'btn btn-default'
onClick: @handleToggle
'Edit'
React.DOM.a
className: 'btn btn-danger'
onClick: @handleDelete
'Delete'
...
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; as this component doesn't handle a state, this new attribute will let our component read the data provided by the user through @refs
: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
...
recordForm: ->
React.DOM.tr null,
React.DOM.td null,
React.DOM.input
className: 'form-control'
type: 'text'
defaultValue: @props.record.date
ref: 'date'
React.DOM.td null,
React.DOM.input
className: 'form-control'
type: 'text'
defaultValue: @props.record.title
ref: 'title'
React.DOM.td null,
React.DOM.input
className: 'form-control'
type: 'number'
defaultValue: @props.record.amount
ref: 'amount'
React.DOM.td null,
React.DOM.a
className: 'btn btn-default'
onClick: @handleEdit
'Update'
React.DOM.a
className: 'btn btn-danger'
onClick: @handleToggle
'Cancel'
...
Do not be afraid, this method might look big, but it is just our HAML-like syntax. Notice we are calling @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.input
s 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: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
...
render: ->
if @state.edit
@recordForm()
else
@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 @props
, the same way we did it before when deleting records: # app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
...
handleEdit: (e) ->
e.preventDefault()
data =
title: React.findDOMNode(@refs.title).value
date: React.findDOMNode(@refs.date).value
amount: React.findDOMNode(@refs.amount).value
# jQuery doesn't have a $.put shortcut method either
$.ajax
method: 'PUT'
url: "/records/#{ @props.record.id }"
dataType: 'JSON'
data:
record: data
success: (data) =>
@setState edit: false
@props.handleEditRecord @props.record, data
...
For the sake of simplicity, we are not validating user data, we just read it through React.findDOMNode(@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.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: # app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
...
updateRecord: (record, data) ->
index = @state.records.indexOf record
records = React.addons.update(@state.records, { $splice: [[index, 1, data]] })
@replaceState records: records
...
render: ->
...
# almost at the bottom of the render method
React.DOM.table
React.DOM.thead null,
React.DOM.tr null,
React.DOM.th null, 'Date'
React.DOM.th null, 'Title'
React.DOM.th null, 'Amount'
React.DOM.th null, 'Actions'
React.DOM.tbody null,
for record in @state.records
React.createElement Record, key: record.id, record: record, handleDeleteRecord: @deleteRecord, handleEditRecord: @updateRecord
As we have learned on the previous section, using React.addons.update
to change our state might result on more concrete methods. The final link between Records
and Record
is the method @updateRecord
sent through the handleEditRecord
property.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!
You can take a look at the resulting code of this section here, or just the changes introduced by this section here.
Closing thoughts: React.js Simplicity & Flexibility
We have examined some of React's functionalities and we
learned that it barely introduces new concepts. I have heard comments of
people saying X or Y JavaScript framework has a steep learning curve
because of all the new concepts introduced, this is not React's case; it
implements core JavaScript concepts such as event handlers and
bindings, making it easy to adopt and learn. Again, one of its strengths
is its simplicity.
We also learned by the example how to integrate it into the
Rails' assets pipeline and how well it plays along with CoffeeScript,
jQuery, Turbolinks, and the rest of the Rails' orchestra. But this is
not the only way of achieving the desired results. For example, if you
don't use Turbolinks (hence, you don't need
react_ujs
) you can use Rails Assets instead of the react-rails
gem, you could use Jbuilder to build more complex JSON responses instead of rendering JSON objects, and so on; you would still be able to get the same wonderful results.
React will definitely boost your frontend abilities, making it a great library to have under your Rails' toobelt.
Post a Comment