Thursday, August 11, 2016

React.js - A guide for Rails developers_part 3 (end)


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:
  1.  # app/controllers/records_controller.rb
  2.   class RecordsController < ApplicationController
  3.     ...
  4.     def destroy
  5.       @record = Record.find(params[:id])
  6.       @record.destroy
  7.       head :no_content
  8.     end
  9.     ...
  10.   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:
  1.   # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     render: ->
  5.       ...
  6.       # almost at the bottom of the render method
  7.       React.DOM.table
  8.         React.DOM.thead null,
  9.           React.DOM.tr null,
  10.             React.DOM.th null, 'Date'
  11.             React.DOM.th null, 'Title'
  12.             React.DOM.th null, 'Amount'
  13.             React.DOM.th null, 'Actions'
  14.         React.DOM.tbody null,
  15.           for record in @state.records
  16.             React.createElement Record, key: record.id, record: record
And finally, open the Record component and add an extra column with a Delete link:
  1.   # app/assets/javascripts/components/record.js.coffee
  2.   @Record = React.createClass
  3.     render: ->
  4.       React.DOM.tr null,
  5.         React.DOM.td null, @props.record.date
  6.         React.DOM.td null, @props.record.title
  7.         React.DOM.td null, amountFormat(@props.record.amount)
  8.         React.DOM.td null,
  9.           React.DOM.a
  10.             className: 'btn btn-danger'
  11.             '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:
  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:
  1.  # app/assets/javascripts/components/record.js.coffee
  2.   @Record = React.createClass
  3.     handleDelete: (e) ->
  4.       e.preventDefault()
  5.       # yeah... jQuery doesn't have a $.delete shortcut method
  6.       $.ajax
  7.         method: 'DELETE'
  8.         url: "/records/#{ @props.record.id }"
  9.         dataType: 'JSON'
  10.         success: () =>
  11.           @props.handleDeleteRecord @props.record
  12.     render: ->
  13.       React.DOM.tr null,
  14.         React.DOM.td null, @props.record.date
  15.         React.DOM.td null, @props.record.title
  16.         React.DOM.td null, amountFormat(@props.record.amount)
  17.         React.DOM.td null,
  18.           React.DOM.a
  19.             className: 'btn btn-danger'
  20.             onClick: @handleDelete
  21.             '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:
  1.  # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     deleteRecord: (record) ->
  5.       records = @state.records.slice()
  6.       index = records.indexOf record
  7.       records.splice index, 1
  8.       @replaceState records: records
  9.     render: ->
  10.       ...
  11.       # almost at the bottom of the render method
  12.       React.DOM.table
  13.         React.DOM.thead null,
  14.           React.DOM.tr null,
  15.             React.DOM.th null, 'Date'
  16.             React.DOM.th null, 'Title'
  17.             React.DOM.th null, 'Amount'
  18.             React.DOM.th null, 'Actions'
  19.         React.DOM.tbody null,
  20.           for record in @state.records
  21.             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 rreplaceState 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:

  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.

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:
  1.   # config/application.rb
  2.   ...
  3.   module Accounts
  4.     class Application < Rails::Application
  5.       ...
  6.       config.react.addons = true
  7.     end
  8.   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 in array on the target.
  • {$splice: array of arrays} for each item in arrays call splice() on the target with the parameters provided by the item.
We're about to simplify addRecord and deleteRecord from the Record component using these helpers, as follows:
  1.   # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     addRecord: (record) ->
  5.       records = React.addons.update(@state.records, { $push: [record] })
  6.       @setState records: records
  7.     deleteRecord: (record) ->
  8.       index = @state.records.indexOf record
  9.       records = React.addons.update(@state.records, { $splice: [[index, 1]] })
  10.       @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:
  1.   # app/assets/javascripts/components/record.js.coffee
  2.   @Record = React.createClass
  3.     getInitialState: ->
  4.       edit: false
  5.     handleToggle: (e) ->
  6.       e.preventDefault()
  7.       @setState edit: !@state.edit
  8.     ...
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 rrecordRow 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:
  1.   # app/assets/javascripts/components/record.js.coffee
  2.   @Record = React.createClass
  3.     ...
  4.     recordRow: ->
  5.       React.DOM.tr null,
  6.         React.DOM.td null, @props.record.date
  7.         React.DOM.td null, @props.record.title
  8.         React.DOM.td null, amountFormat(@props.record.amount)
  9.         React.DOM.td null,
  10.           React.DOM.a
  11.             className: 'btn btn-default'
  12.             onClick: @handleToggle
  13.             'Edit'
  14.           React.DOM.a
  15.             className: 'btn btn-danger'
  16.             onClick: @handleDelete
  17.             'Delete'
  18.     ...
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:
  1.  # app/assets/javascripts/components/record.js.coffee
  2.   @Record = React.createClass
  3.     ...
  4.     recordForm: ->
  5.       React.DOM.tr null,
  6.         React.DOM.td null,
  7.           React.DOM.input
  8.             className: 'form-control'
  9.             type: 'text'
  10.             defaultValue: @props.record.date
  11.             ref: 'date'
  12.         React.DOM.td null,
  13.           React.DOM.input
  14.             className: 'form-control'
  15.             type: 'text'
  16.             defaultValue: @props.record.title
  17.             ref: 'title'
  18.         React.DOM.td null,
  19.           React.DOM.input
  20.             className: 'form-control'
  21.             type: 'number'
  22.             defaultValue: @props.record.amount
  23.             ref: 'amount'
  24.         React.DOM.td null,
  25.           React.DOM.a
  26.             className: 'btn btn-default'
  27.             onClick: @handleEdit
  28.             'Update'
  29.           React.DOM.a
  30.             className: 'btn btn-danger'
  31.             onClick: @handleToggle
  32.             'Cancel'
  33.     ...
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 are being created? We are using defaultValue instead of value to set the initial input values, this is because using jusvalue without onChange will end up creating read-only inputs.

Finally, the render method boils down to the following code:
  1.   # app/assets/javascripts/components/record.js.coffee
  2.   @Record = React.createClass
  3.     ...
  4.     render: ->
  5.       if @state.edit
  6.         @recordForm()
  7.       else
  8.         @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:
  1.   # app/controllers/records_controller.rb
  2.   class RecordsController < ApplicationController
  3.     ...
  4.     def update
  5.       @record = Record.find(params[:id])
  6.       if @record.update(record_params)
  7.         render json: @record
  8.       else
  9.         render json: @record.errors, status: :unprocessable_entity
  10.       end
  11.     end
  12.     ...
  13.   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:
  1.  # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     updateRecord: (record, data) ->
  5.       index = @state.records.indexOf record
  6.       records = React.addons.update(@state.records, { $splice: [[index, 1, data]] })
  7.       @replaceState records: records
  8.     ...
  9.     render: ->
  10.       ...
  11.       # almost at the bottom of the render method
  12.       React.DOM.table
  13.         React.DOM.thead null,
  14.           React.DOM.tr null,
  15.             React.DOM.th null, 'Date'
  16.             React.DOM.th null, 'Title'
  17.             React.DOM.th null, 'Amount'
  18.             React.DOM.th null, 'Actions'
  19.         React.DOM.tbody null,
  20.           for record in @state.records
  21.             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 Records 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.
Written by Fernando Villalobos

If you found this post interesting, follow and support us.
Suggest for you:



No comments:

Post a Comment