Plática: React.js - Una guía para desarrolladores Rails
Escrito el 9 de Enero de 2016
Introducción a React.js
React.js es el nuevo chico popular de la cuadra de los “Frameworks Javascript”, y resalta por su simplicidad. Donde otros frameworks implementan un framework MVC completo, podríamos decir que React sólo implementa la V (de hecho, algunos reemplazan la V de su framework con React). Las aplicaciones React son construidas sobre dos principios importantes: Componentes y Estados. Los compomentes se pueden formar de otros componentes más pequeños, empotrados o personalizados; el Estado {dirige} lo que los chicos en Facebook llaman one-way reactive data flow, que significa que nuestro UI reaccionará a cada cambio de estado.
Una de las cosas buenas sobre React es que no requiere dependencias adicionales, por lo que puede trabajar prácticamente con cualquier otra biblioteca de JavaScript. Aprovechando esta característica, vamos a incluirlo en nuestro stack de Rails para construir una aplicación enfocada a frontend, o, como se podría decir, Rails con esteroides.
Una aplicación de seguimiento de gastos
Para esta guía, vamos a construir una pequeña aplicación desde cero para hacer un seguimiento de nuestros gastos; cada registro consistirá en una fecha, un título y una cantidad. Un registro se tratará como crédito si su cantidad es mayor que cero, en caso contrario se tratará como débito. Aquí una maqueta del proyecto:
Resumiendo, la aplicación se comportará como sigue:
- Cuando el usuario cree un nuevo registro a través del formulario, éste se añadirá a la tabla de registros
- El usuario podrá editar {en línea} cualquier registro existente
- Al hacer clic en cualquier botón “Borrar” se eliminará el registro asociado de la tabla
- Al agregar, editar o eliminar un registro existente, se actualizarán las cantidades en la parte superior de la página
Inicializando nuestro proyecto React.js en Rails
Primero lo primero: tenemos que crear nuestro nuevo proyecto Rails; le llamaremos Accounts
:
rails new accounts
Para la interfaz de usuario de este proyecto, vamos a usar Twitter Bootstrap. El proceso de instalación está fuera del alcance de este post, pero se puede instalar la gema oficial de bootstrap-sass
siguiendo las instrucciones desde el repositorio oficial de GitHub.
Una vez que hayamos creado nuestro proyecto, se procede a incluir React. Para este post decidí incluirlo mediante la gema oficial react-rails porque vamos a aprovechar algunas características interesantes de la gema, pero hay otras formas, como Rails assests o incluso descargar el paquete desde la página oficial y pegarlo en nuestra carpeta javascripts
.
Si has desarrollado aplicaciones Rails antes, sabes lo fácil que es instalar una gema: Añade rails-react
a tu Gemfile.
rails new accounts
Entonces, hay que decirle (amablemente) a Rails que instale las nuevas gemas:
bundle install
react-rails
viene con un script de instalación, lo que creará un archivo components.js
y un directorio componentes
bajo app/assests/javascripts
donde vivirán nuestros componentes React.
rails g react:install
Si ves el archivo application.js
después de ejecutar el instalador, te darás cuenta de que hay tres nuevas líneas:
//= require react
//= require react_ujs
//= require components
Básicamente, incluye la biblioteca de React, el archivo manifiesto componentes
y un archivo que luce familiar terminado en ujs. Como ya habrás adivinado por el nombre del archivo, react-rails incluye un driver JavaScript no instrusivo que nos ayudará a montar nuestros componentes React y también se encargará de eventos Turbolinks.
Creación del recurso
Vamos a construir un recurso Record
, que incluirá una fecha, un título y una cantidad. En lugar de utilizar el generador de scaffold
, vamos a utilizar el generador de resource
, ya que no vamos a utilizar todos los archivos y métodos creados por el scaffold. Otra opción podría ser ejecutar el scaffold
y luego eliminar los archivos/métodos no utilizados, pero nuestro proyecto podría quedar muy revuelto después de esto. Dentro de tu proyecto, ejecuta el siguiente comando:
rails g resource Record title date:date amount:float
Después de un poco de magia, terminamos con un nuevo modelo Record
, con su controlador y rutas. Sólo tenemos que crear nuestra base de datos y ejecutar migraciones pendientes.
rake db:create db:migrate
Como agrergado, puedes crear un par de registros desde la consola de Rails:
Record.create title: 'Record 1', date: Date.today, amount: 500
Record.create title: 'Record 2', date: Date.today, amount: -100
No se olvide de iniciar el servidor con rails server
.
¡Listo! Estamos listos para escribir algo de código.
Anidando componentes: Listado de registros {records}
Para nuestra primera tarea, tenemos que renderear todos los registro existentes dentro de una tabla. Primero, tenemos que crear una acción index
dentro de nuestro RecordsController
:
# app/controllers/records_controller.rb
class RecordsController < ApplicationController
def index
@records = Record.all
end
end
Después, tenemos que crear un nuevo archivo index.html.erb
en apps/views/records/
, este archivo servirá como un puente entre nuestra aplicación Rails y nuestros componentes React. Para lograr esto, vamos a utilizar el método helper, react_component
, que recibe el nombre del componente React que queramos renderear, junto con los datos que queremos pasar.
<%# app/views/records/index.html.erb %>
<%= react_component 'Records', { data: @records } %>
Vale la pena mencionar que este helper es proporcionado por la gema rails-react
, si decides utilizar otro tipo de integración con React, este helper no estará disponible.
Ahora puede navegar hasta localhost:3000/records
. Obviamente, esto todavía no va a funcionar, debido a la falta de un componente React Records
, pero si vemos el HTML generado dentro de la ventana del navegador, podemos ver algo como el siguiente código
<div data-react-class="Records" data-react-props="{...}">
</div>
Con este markado HTML presente, react_ujs
detectará que queremos renderear un componente React y creará una instancia del mismo, incluyendo las propiedades que enviamos a través del método react_component
, en nuestro caso, el contenido de @records
.
El momento para crear nuestro primer componente React ha llegado, dentro del directorio javascripts/components
, crea un nuevo archivo llamado records.js.coffee
, este archivo contendrá nuestro componente Records
.
# app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
render: ->
React.DOM.div
className: 'records'
React.DOM.h2
className: 'title'
'Records
Cada componente requiere un método render
, que será el encargado de renderear el propio componente. El método render
debe devolver una instancia de ReactComponent
, de esta manera, cuando React ejecuta un re-render, se comportará de forma óptima (ya que React detecta la existencia de nuevos nodos construyendo un DOM virtual en la memoria). En el fragmento anterior creamos una instancia de h2
, un ReactComponent
ya incluido en React.
NOTA: Otra forma de crear una instancia ReactComponents
dentro del método render
es a través de la sintaxis JSX
. El siguiente fragmento es equivalente al anterior:
render: ->
`<div className="records">
<h2 className="title"> Records </h2>
</div>`
Personalmente, cuando trabajo con CoffeeScript, prefiero usar la sintaxis React.DOM
sobre JSX
porque el código se estructura jerárquicamente por sí sólo, como en HAML. Por otro lado, si estás tratando de integrar React en una aplicación existente construida con erb, tienes la opción de reutilizar el código erb existente y convertirlo en JSX.
Ahora puedes refrescar tu navegador.
¡Perfecto! Hemos rendereado nuestro primer componente React. Ahora, es tiempo de mostrar nuestros registros.
Además del método render
, los componentes React hacen uso de properties
para comunicarse con otros componentes y states
para detectar si se requiere o no re-renderear. Necesitamos inicializar el estado y las propiedades de nuestro componente con los valores deseados:
# app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
getInitialState: ->
records: @props.data
getDefaultProps: ->
records: []
render: ->
...
El método getDefaultProps
inicializa las propiedades de nuestros componentes en caso de que olvidemos enviar los datos al instanciarlos, y el método getInitialState
genera el estado inicial de nuestro componente. Ahora tenemos que mostrar los registros provistos realmente por nuestra vista Rails.
Parece que vamos a necesitar un método helper para formatear cadenas de cantidad. Podemos implementar un sencillo formateador de cadenas y hacerlo accesible para todos nuestros archivos coffeescript
. Crea un nuevo archivo utils.js.coffee
en javascripts/
con el siguiente contenido:
# app/assets/javascripts/utils.js.coffee
@amountFormat = (amount) ->
'$ ' + Number(amount).toLocaleString()
Tenemos que crear un nuevo componente Record
para mostrar cada registro individual. Crea un nuevo archivo record.js.coffee
bajo el directorio javascripts/components
e inserta el siguiente contenido:
# 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)
El componente Record
mostrará una fila que contiene la información de cada atributo del registro. No te preocupes por esos valores null
s en las llamadas de React.DOM.*
, significan que no estamos enviando atributos a los componentes. Ahora actualiza el método render
dentro del componente Records
con el siguiente código:
# 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
¿Viste qué pasó? Hemos creado una tabla con una fila como encabezado, y en el cuerpo de la tabla creamos un elemento Record
por cada registro existente. En otras palabras, estamos anidando compomentes React ya existentes y personalizados. Bien, ¿cierto?
Cuando trabajamos con contenido dinámico (en este caso, registros), necesitamos proporcionar una propiedad key
a los elementos generados dinámicamente para que React no tenga problemas al refrescar nuestra UI, es por eso que enviamos key: record.id
junto con el propio registro al crear elementos Record
. Si no hacemos esto, recibiremos un mensaje de advertencia en la consola JS del navegador (y probablemente algunos dolores de cabeza en un futuro cercano).
Puedes echar un vistazo al código resultante de esta sección aquí, o sólo los cambios introducidos por esta sección aquí.
Comunicación padre-hijo: Creación de registros
Ahora que estamos mostrando todos los registros existentes, sería bueno incluir un formulario para crear nuevos registros. Agrerguemos esta nueva característica a nuestra aplicación React/Rails.
Primero, tenemos que añadir el método de create
a nuestros controlador Rails (no te olvides de utilizar 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
Luego, tenemos que construir un componente React para manejar la creación de nuevos registros. El componente tendrá su propio estado para almacenar la fecha, título y la cantidad. Crear un nuevo archivo record_form.js.coffee
en javascripts/components
con el siguiente código:
# 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'
Nada pretencioso, sólo un sencillo formulario Bootstrap in line. Observa cómo estamos definiendo el atributo value
para setear el valor de entrada y el atributo onChange
para agregar un método que será llamado cada que se presione una tecla; el método handleChange
utilizará el atributo nombre
para detectar qué entrada disparó el evento y actualizar su valor relacionado en el estado state
:
# app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
...
handleChange: (e) ->
name = e.target.name
@setState "#{ name }": e.target.value
...
Sólo estamos usando interpolación de cadenas para definir dinámicamente las llaves de nuestro objeto, equivalente a @setState title: e.target.value
cuando name
es igual a title. Pero, ¿por qué tenemos que utilizar @setState
? ¿Por qué no podemos simplemente setear el valor deseado de @state
como solemos hacer en objetos normales en JS? Porque @setState
realizará 2 acciones:
- Actualizar el estado del componente
- Agendar una verificación/repintado de la UI basado en el nuevo estado
Es muy importante tener en cuenta esta información cada vez que utilizamos state
dentro de nuestros componentes.
Veamos el botón de enviar, justo al final del método render
:
@RecordForm = React.createClass
...
render: ->
...
React.DOM.form
...
React.DOM.button
type: 'submit'
className: 'btn btn-primary'
disabled: !@valid()
'Create record'
Definimos un atributo disabled
con el valor de !valid()
, lo que significa que vamos a implementar un método valid
para evaluar si los datos ingrersados por el usuario son correctos.
# app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
...
valid: ->
@state.title && @state.date && @state.amount
Por simplicidad sólo vamos a validar cadenas vacías para atributos dentro del state
. De esta manera, cada vez que el estado se actualice, el botón “Crear registro” se activará/desactivará dependiendo de la validez de los datos.
Ahora que tenemos nuestro controlador y formulario en su lugar, es momento de submitear nuestro nuevo registro al servidor. Tenemos que manejar el evento submit
del formulario. Para lograr esto, tenemos que añadir un atributo onSubmit
a nuestro formulario y un nuevo método handleSubmit
(de la misma forma que manejamos los eventos onChange
antes):
# 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
...
Revisemos el nuevo método línea por línea:
- Evitar el submit HTTP del formulario
- Hacer POST de la nueva información de
record
al URL actual - Callback de éxito
El callback success
es la clave de este proceso. Después de crear el nuevo registro exitosamente, alguien será notificado sobre esta acción y state
se restaurará a su valor inicial. ¿Recuerdas cuando mencioné que los componentes se comunican con otros componentes a través de propiedades (@props
)? Bueno, es esto. Nuestro componente actual envía datos al componente padre a través de @props.handleNewRecord
para notificar de la existencia de un nuevo registro.
Como ya habrás adivinado, donde quiera que creemos nuestro elemento RecordForm
, tenemos que pasarle una propiedad handleNewRecord
con la referencia de un método, algo así como React.createElement RecordForm, handleNewRecord:addRecord
. Bueno, el componente padre Records
es el “donde quiera”. Dado que tiene un estado con todos los registros existentes, necesitamos actualizar su estado con el registro recién creado.
Añade el nuevo método addRecord
dentro records.js.coffee
y crea el nuevo elemento RecordForm
, justo después h2
(dentro del método render
).
# 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
...
Refresca tu navegador, rellena el formulario con un nuevo registro, haz clic en “Create record”… Sin suspenso esta vez, se añadió el registro casi de inmediato y el formulario se limpia después del submit. Refrezca de nuevo sólo para asegurarte que el servidor ha almacenado la nueva información.
Si has utilizado otros frameworks JS junto con Rails (por ejemplo, AngularJS) para construir cosas similares, es posible que hayas tenido problemas porque sus peticiones POST no incluyen el token CSRF que Rails necesita, así que, ¿por qué no tuvimos el mismo problema? Fácil, porque estamos usando jQuery
para interactuar con nuestro backend y el driver jquery_ujs
de rails incluye automáticamente el token CSRF
en cada petición AJAX por nosotros. ¡Bien!
Puedes revisar el código resultante de esta sección aquí, o sólo los cambios introducidos por esta sección aquí.
Componentes reutilizables: Indicadores de Cantidad
¿Qué sería de una aplicación sin algunos (bonitos) indicadores? Añadamos algunas recuadros en la parte superior de la ventana con un poco de información útil. Nuestro objetivo de esta sección es mostrar tres valores: cantidad total de crédito, cantidad total de débito y el balance. Esto parece un trabajo para tres componentes, ¿o tal vez sólo uno con propiedades?
Podemos construir un nuevo componente AmountBox
que recibirá tres propiedades: amount
, text
y type
. Crea un nuevo archivo llamado amount_box.js.coffee
en javascripts/componentes/
y pega el siguiente código:
# 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)
Estamos usando elemento de panel de Bootstrap para mostrar la información en forma de bloques, y seteamos el color mediante la propiedad type
. También hemos incluido un método muy simple para formatear la cantidad llamado amountFormat
que lee la propiedad amount
y la muestra en formato de moneda.
Para tener una solución completa, necesitamos crear este elemento (tres veces) dentro de nuestro componente principal, mandando las propiedades requeridas dependiendo de los datos que queremos mostrar. Construyamos primero los métodos para calcular. Abre el componente Records
y añade los siguientes métodos:
# 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
suma todos los registros con una cantidad mayor a 0, debits
suma todos los registros con una cantidad menor que 0 y balance no necesita explicación. Ahora que tenemos los métodos para hacer cálculos en su lugar, sólo tenemos que crear los elementos AmountBox
dentro del método render
(justo arriba del componente RecordForm
):
# 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
...
¡Hemos terminado con esta funcionalidad! Refresca el navegador, deberías ver tres recuadros que muestran las cantidades que hemos calculado anteriormente. ¡Pero espera! ¡Hay más! Crea un nuevo registro y ve la magia…
Puedes ver el código resultante de esta sección aquí, o sólo los cambios introducidos por esta sección aquí.
setState
/replaceState
: Eliminación de registros
La siguiente funcionalidad en nuestra lista es poder eliminar registros. Necesitamos una nueva columna Actions
en nuestra tabla de registros, esta columna tendrá un botón “Delete” para cada registro, con una UI bastante estándar. Al igual que en el ejemplo anterior, tenemos que crear el método destroy
en nuestro controlador Rails:
# app/controllers/records_controller.rb
class RecordsController < ApplicationController
...
def destroy
@record = Record.find(params[:id])
@record.destroy
head :no_content
end
...
end
Ese es todo el código del lado del servidor que necesitaremos para esta funcionalidad. Ahora, abre tu componente React Records
y añade la columna Actions en la posición más a la derecha de la cabecera de la tabla:
# 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
Y, por último, abre el componente Record
y añade una columna adicional con un link a Delete:
# 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'
Guarda el archivo, refreca tu navegador y … ¡Tenemos un botón inútil sin eventos asociados a él!
Añadamos funcionalidad. Como hemos aprendido de nuestro componente RecordForm
, el camino a seguir es:
- Detectar un evento dentro del componente
Record
hijo (onClick) - Realizar una acción (enviar una solicitud DELETE al servidor en este caso)
- Notificar al componente padre
Records
acerca de esta acción (envío/recepción de un método a través de props) - Actualizar el estado del componente
Record
Para implementar el paso 1, podemos agregar un manejador para onClick
a Registro
de la misma manera que añadimos un manejador para onSubmit
a RecordForm
para crear nuevos registros. Afortunadamente para nosotros, React implementa la mayoría de los eventos comunes de navegadores de una forma normalizada, por lo que no tenemos que preocuparnos por la compatibilidad entre navegadores (puedes vera la lista de eventos completos aquí).
Re-abre el componente Record
, añade un nuevo método handleDelete
y un atributo onClick
a nuestro botón “inútil”, como se ve a continuación:
# 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'
Cuando el botón de borrado recibe el clic, handleDelete
envía una petición AJAX al servidor para borrar el registro en el backend y, después de esto, se notifica al componente padre de esta acción a través del manejador handleDeleteRecord
disponible mediante props. Esto significa que tenemos que ajustar la creación de elementos Record
en el componente padre para incluir la propiedad extra handleDeleteRecord
, y propiamente implementar el método en el padre:
# 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
Básicamente, nuestro método deleteRecord
copia el estado del componente records
actual, realiza una búsqueda del registro que desea borrar, lo elimina de la colección y actualiza el estado del componente, operaciones estándar de JavaScript.
Hemos introducido una nueva forma de interactuar con el estado, replaceState
; la principal diferencia entre setstate
y replaceState
es que el primero sólo actualizará un atributo del objeto state, el segundo sobreescribe completamente el estado actual del componente con cualquier nuevo objeto que le enviemos.
Después de la actualización de esta última pieza de código, refrescamos la ventana del navegador y tratamos de eliminar un registro. Deben de suceder un par de cosas:
- Los registros deben desaparecer de la tabla y …
- Los indicadores deben actualizar las cantidades de inmediato, no se requiere ningún código adicional
Casi terminamos con nuestra aplicación, pero antes implementar nuestra última funcionalidad, podemos aplicar un pequeño refactor y, al mismo tiempo, introducir una nueva funcionaldad de React.
Puedes ver el código resultante de esta sección aquí, o sólo los cambios introducidos por esta sección aquí.
Refactor: Helpers de estado
Hasta aquí, tenemos un par de métodos donde el estado se actualiza sin dificultad dado que nuestros datos no son precisamente “complejos”, pero imagina una aplicación más compleja con un estado JSON multi-nivel. Puedes imaginarte realizando copias profundas y malabares con tus datos de estado. React incluye algunos bonitos helpers para el estado que ayudan con parte del trabajo pesado, sin importar qué tan profundo es el estado, estos helpers te permitirán manipularlo con más libertad utilizando un lenguaje de consulta de tipo MongoDB (o al menos eso es de lo que dice la documentación de React).
Antes de utilizar estos helpers, primero tenemos que configurar nuestra aplicación Rails para incluirlos. Abre el archivo de configuración appication.rb
de tu proyecto y añade config.react.addons = true
en la parte inferior del bloque Aplicación
:
# config/application.rb
...
module Accounts
class Application < Rails::Application
...
config.react.addons = true
end
end
Para que los cambios surtan efecto, reinicia el servidor de Rails. Repito, reinicia el servidor de Rails. Ahora tenemos acceso a los helpers de estado mediante React.addons.update
, que procesarán nuestro objeto de estado (o cualquier otro objeto que enviemos a él) y aplicará los comandos proporcionados. Los dos comandos que usaremos son $push
y $splice
(tomo prestada la explicación de estos comandos de la documentación oficial de React):
* `{$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.
Estamos a punto de simplificar addRecord
y deleteRecord
del componente Record
usando estos helpers, como sigue:
# 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
Más corto, más elegante y con los mismos resultados. Puedes refrescar el navegador y asegurarte que nada se rompió.
Puedes ver el código resultante de esta sección aquí, o sólo los cambios introducidos por esta sección aquí.
Flujo reactivo de datos: Edición de registros
Para la funcionalidad final, añadiremos un botón extra de edición, al lado del botón Borrar en nuestra tabla de registros. Cuando se haga clic en este botón Editar, se cambiará toda la fila de un estado de sólo lectura a un estado editable, mostrando un formulario en línea en el que el usuario pueda actualizar el contenido del registro. Después de enviar el contenido actualizado o cancelar la acción, la fila del registro volverá a su estado inicial de sólo lectura.
Como ya habrán adivinado por el párrafo anterior, tenemos que manejar datos mutables para alternar el estado de cada registro dentro de nuestro componente Record
. Este es un caso de uso de lo que React llama flujo reactivo de datos. Añadamos una bandera de edición y un método handleToggle
a record.js.coffee
:
# app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
getInitialState: ->
edit: false
handleToggle: (e) ->
e.preventDefault()
@setState edit: !@state.edit
...
La bandera edit
tendrá un valor por defecto de false
y handleToggle
cambiará edit
de falso a verdadero y viceversa, sólo tenemos que disparar handleToggle
a partir de un evento onClick
del usuario.
Ahora, tenemos que manejar dos versiones de filas (de formulario y de sólo lectura) y mostrarlas de forma condicional dependiendo de edit
. Por suerte para nosotros, siempre y cuando nuestro método render
devuelva un elemento React, podemos llevar a cabo cualquier acción en él; podemos definir un par de métodos auxiliares recordRow
y recordForm
y llamarlos condicionalmente dentro de render
en función del contenido de @state.edit
.
Ya tenemos una primera versión de recordRow
, es nuestro método render
actual. Pasemos el contenido de render
a nuestro nuevo método recordRow
y añadamos algo de código adicional:
# 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'
...
Sólo agregamos un elemento React.DOM.a
adicional que escucha eventos onClick
para llamar a handleToggle
.
Continuemos, la implementación de recordForm
tendría una estructura similar, pero con inputs de entrada en cada celda. Utilicemos un nuevo atributo ref
para hacer accesibles nuestras entradas. Ya que este componente no maneja un estado, este nuevo atributo permitirá que nuestro componente lea los datos provistos por el usuario por medio de @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'
...
No tengas miedo, este método puede parecer grande, pero es sólo nuestra sintaxis tipo HAML. Nota que llamamos a @handleEdit
cuando el usuario hace clic en el botón “Update”. Utilicemos un flujo similar a la implementación para eliminar registros.
¿Notas algo diferente en cómo se están creando React.DOM.inputs
? Estamos utilizando defaultValue
en lugar de value
para asignar los valores de entrada iniciales, esto es porque el usar value
sin onChange
acabará creando entradas de sólo lectura.
Por último, el método render
se reduce al siguiente código:
# app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
...
render: ->
if @state.edit
@recordForm()
else
@recordRow()
Puedes refescar tu navegador para jugar con el nuevo comportamiento, pero no presentará ningún cambio todavía ya que no hemos implementado la funcionalidad real.
Para manejar las actualizaciones de registros, tenemos que añadir el método update
a nuestro controlador Rails:
# 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
De regreso a nuestro componente Record
, tenemos que implementar el método handleEdit
que enviará una petición AJAX al servidor con la información actualizada de record
, después notificará al componente padre enviando la versión actualizada del registro a través del método handleEditRecord
, este método se recibirá a través de @props
, de la misma manera que lo hicimos antes, al eliminar de registros:
# 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
...
Para simplificar, no vamos a validar los datos del usuario, sólo los leeremos por medio de React.findDOMNode(@refs.fieldName).value
y los enviaremos tal cual al backend. Actualizar el estado para cambiar al modo de edición en success
no es obligatorio, pero el usuario nos lo agradecerá.
Por último, pero no menos importante, sólo tenemos que actualizar el estado del componente Records
para sobrescribir el registro anterior con la nueva versión del registro hijo y dejar que React realice su magia. La implementación podría verse así:
# 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
Como hemos aprendido en la sección anterior, usar React.addons.update
para cambiar nuestro estado podría resultar en métodos más concretos. El enlace final entre Records
y Record
es el método updateRecord
enviado a través de la propiedad handleEditRecord
.
Refresca el navegador por última vez y trata de actualizar algunos registros existentes, observa cómo las recuadros de cantidad en la parte superior de la página mantienen un seguimiento de cada cambio en los registros.
¡Hemos terminado! Sonríe, ¡acabamos de construir una pequeña aplicación Rails + React desde cero!
Puedes ver el código resultante de esta sección aquí, o sólo los cambios introducidos por esta sección aquí.
Comentarios finales: React.js - simplicidad y flexibilidad
Hemos examinado algunas de las funcionalidades de React y hemos aprendido que apenas si introduce nuevos conceptos. He escuchado comentarios de gente diciendo X o Y framewrork JavaScript tiene una curva de aprendizaje empinada debido a todos los nuevos conceptos que introduce, pero no es el caso React, que implementa conceptos básicos de JavaScript, como manejadores de eventos y bindings, por lo que es fácil de adoptar y aprender. Una vez más, uno de sus puntos fuertes es su sencillez.
También aprendimos cómo integrarlo en el pipeline de Rails y lo bien que se entiende con CoffeeScript, jQuery, Turbolinks, y el resto de la orquesta Rails. Pero esta no es la única forma de lograr los resultados deseados. Por ejemplo, si no usas Turbolinks (por lo tanto, no es necesario react_ujs
) puedes utilizar Rails Assets en lugar de la gema react-rails
; podrías utilizar JBuilder para construir respuestas JSON más complejas en lugar de la prestación de los objetos JSON, y cosas por el estilo, y seguir obteniendo los mismos maravillosos resultados.
React definitivamente aumentará tus habilidades de frontend, lo que la hace una gran biblioteca para tener entre tus herramientas Rails.