Navegando por el ecosistema React.js
Publicado: 2022-03-11La velocidad de la innovación en JavaScript Land es tan alta que algunas personas incluso piensan que es contraproducente. Una biblioteca puede pasar de ser un juguete de adopción temprana a ser una biblioteca de última generación y volverse obsoleta en el transcurso de unos pocos meses. Ser capaz de identificar una herramienta que seguirá siendo relevante durante al menos otro año se está convirtiendo en un arte en sí mismo.
Cuando se lanzó React.js hace dos años, apenas estaba aprendiendo Angular, y rápidamente descarté React como una oscura biblioteca de plantillas. Durante esos dos años, Angular realmente ganó un punto de apoyo entre los desarrolladores de JavaScript y casi se convirtió en sinónimo del desarrollo JS moderno. Incluso comencé a ver Angular en entornos corporativos muy conservadores, y di por sentado su brillante futuro.
Pero de repente, sucedió algo extraño. Parece que Angular se convirtió en víctima del efecto Osborne, o "muerte por anuncio previo". El equipo anunció que, en primer lugar, Angular 2 será completamente diferente, sin una ruta de migración clara desde Angular 1 y, en segundo lugar, que Angular 2 no estará disponible hasta dentro de un año más o menos. ¿Qué le dice eso a alguien que quiere comenzar un nuevo proyecto web? ¿Quiere escribir su nuevo proyecto en un marco que quedará obsoleto con el lanzamiento de una nueva versión?
Esta ansiedad entre los desarrolladores jugó a favor de React, que buscaba establecerse en la comunidad. Pero React siempre se comercializó como la "V" en "MVC". Esto provocó cierta frustración entre los desarrolladores web, que están acostumbrados a trabajar con marcos completos. ¿Cómo completo las piezas que faltan? ¿Debería escribir el mío? ¿Debería usar una biblioteca existente? ¿Si es así, Cuál?
Efectivamente, Facebook (creadores de React.js) tenía un as más en la manga: el flujo de trabajo Flux, que prometía completar las funciones "M" y "C" que faltaban. Para hacer las cosas aún más interesantes, Facebook declaró que Flux es un "patrón", no un marco, y su implementación de Flux es solo un ejemplo del patrón. Fieles a su palabra, su implementación fue realmente simple e implicó escribir una gran cantidad de texto repetitivo y detallado para que las cosas funcionaran.
La comunidad de código abierto vino al rescate y, un año después, tenemos docenas de bibliotecas de Flux e incluso algunos metaproyectos destinados a compararlas. Ésto es una cosa buena; en lugar de lanzar un marco corporativo listo para usar, Facebook logró despertar el interés en la comunidad y alentó a las personas a encontrar sus propias soluciones.
Hay un efecto secundario interesante en este enfoque: cuando tiene que combinar muchas bibliotecas diferentes para obtener su marco completo, está escapando efectivamente del bloqueo del proveedor, y las innovaciones que surgen durante la construcción de su propio marco se pueden reutilizar fácilmente. en otra parte.
Es por eso que las cosas nuevas sobre React son tan interesantes; la mayor parte se puede reutilizar fácilmente en otros entornos de JavaScript. Incluso si no planea usar React, echar un vistazo a su ecosistema es inspirador. Es posible que desee simplificar su sistema de compilación con el Webpack potente, aunque comparativamente fácil de configurar, paquete de módulos, o comenzar a escribir ECMAScript 6 e incluso ECMAScript 7 hoy con el compilador Babel.
En este artículo, repasaré algunas de las características y bibliotecas interesantes que están disponibles. Entonces, ¡exploremos el ecosistema React!
Sistema de construcción
Podría decirse que el sistema de compilación es lo primero que debe preocuparse al crear una nueva aplicación web. El sistema de compilación no es solo una herramienta para ejecutar scripts, sino que, en el mundo de JavaScript, generalmente da forma a la estructura general de su aplicación. Las tareas más importantes que debe cubrir un sistema de compilación son las siguientes:
- Gestión de dependencias externas e internas
- Ejecución de compiladores y preprocesadores
- Optimización de activos para la producción
- Ejecutar el servidor web de desarrollo, el observador de archivos y el recargador del navegador
En los últimos años, el flujo de trabajo de Yeoman con Bower y Grunt se presentó como la santísima trinidad del desarrollo frontend moderno, resolviendo los problemas de generación repetitiva, gestión de paquetes y ejecución de tareas comunes respectivamente, con la gente más progresista cambiando de Grunt a Gulp recientemente.
En el entorno React, puede olvidarse de estos con seguridad. No es que no puedas usarlos, pero lo más probable es que puedas salirte con la tuya usando Webpack y el viejo NPM. ¿Cómo es eso posible? Webpack es un paquete de módulos que implementa la sintaxis del módulo CommonJS, común en el mundo de Node.js, también en el navegador. En realidad, simplifica las cosas, ya que no necesita aprender otro administrador de paquetes para el front-end; simplemente usa NPM y comparte dependencias entre el servidor y el front-end. Tampoco necesita lidiar con el problema de cargar los archivos JS en el orden correcto porque se deduce de las importaciones de dependencia especificadas en cada archivo, y todo el enjambre se concatena correctamente en un script cargable.
Para hacer las cosas aún más atractivas, Webpack, a diferencia de su primo mayor Browserify, también puede manejar otros tipos de activos. Por ejemplo, con los cargadores, puede transformar cualquier archivo de activos en una función de JavaScript que inserte o cargue el archivo al que se hace referencia. Por lo tanto, olvídese de preprocesar manualmente y hacer referencia a los activos de HTML. Solo require
sus archivos CSS/SASS/LESS de JavaScript, y Webpack se encarga del resto con un archivo de configuración simple. Webpack también incluye un servidor web de desarrollo y un observador de archivos. Además, puede usar la clave "scripts"
en package.json
para definir frases sencillas de shell:
{ "name": "react-example-filmdb", "version": "0.0.1", "description": "Isomorphic React + Flux film database example", "main": "server/index.js", "scripts": { "build": "./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js", "dev": "node --harmony ./webpack/dev-server.js", "prod": "NODE_ENV=production node server/index.js", "test": "./node_modules/.bin/karma start --single-run", "postinstall": "npm run build" } ... }
Y eso es todo lo que necesita para reemplazar a Gulp y Bower. Por supuesto, aún puede usar Yeoman para generar el modelo de aplicación. No se desanime cuando no haya un generador Yeoman para las cosas que desea (las bibliotecas más avanzadas a menudo no tienen uno). Todavía puede simplemente clonar algunos repetitivos de GitHub y piratear.
ECMAScript del mañana, hoy
El ritmo de desarrollo del lenguaje JavaScript ha aumentado sustancialmente en los últimos años, y después de un período de eliminar peculiaridades y estabilizar el lenguaje, ahora vemos que se están incorporando nuevas funciones potentes. El borrador de la especificación ECMAScript 6 (ES6) se ha finalizado, y aunque aún no se ha hecho oficial, ya está encontrando una adopción generalizada. Se está trabajando en ECMAScript 7 (ES7), pero muchas de sus funciones ya están siendo adoptadas por las bibliotecas más avanzadas.
¿Cómo es esto posible? Tal vez piense que no puede aprovechar estas nuevas y brillantes funciones de JavaScript hasta que sean compatibles con Internet Explorer, pero piénselo de nuevo. Los transpiladores ES ya se han vuelto tan omnipresentes que incluso podemos prescindir de la compatibilidad adecuada con el navegador. El mejor transpilador de ES disponible en este momento es Babel: tomará su código ES6+ más nuevo y lo transformará en ES5 estándar, por lo que puede usar cualquier nueva característica de ES tan pronto como se invente (e implemente en Babel, lo que generalmente sucede bastante). rápidamente).
Las funciones de JavaScript más nuevas son útiles en todos los marcos de front-end, y React se ha actualizado recientemente para que funcione bien con las especificaciones ES6 y ES7. Estas nuevas características deberían eliminar muchos dolores de cabeza al desarrollar con React. Echemos un vistazo a algunas de las adiciones más útiles y cómo pueden beneficiar un proyecto React. Más adelante, veremos cómo usar algunas herramientas y bibliotecas útiles con React mientras aprovechamos esta sintaxis mejorada.
clases de ES6
La programación orientada a objetos es un paradigma poderoso y ampliamente adoptado, pero la versión de JavaScript es un poco exótica. La mayoría de los marcos front-end, ya sea Backbone, Ember, Angular o React, han adoptado sus propias formas patentadas de definir clases y crear objetos. Pero con ES6, ahora tenemos clases tradicionales en JavaScript y simplemente tiene sentido usarlas en lugar de escribir nuestra propia implementación. Entonces, en lugar de:
React.createClass({ displayName: 'HelloMessage', render() { return <div>Hello {this.props.name}</div>; } })
podemos escribir:
class HelloMessage extends React.Component { render() { return <div>Hello {this.props.name}</div>; } }
Para un ejemplo más elaborado, considere este código, usando la sintaxis anterior:
React.createClass({ displayName: 'Counter', getDefaultProps: function(){ return {initialCount: 0}; }, getInitialState: function() { return {count: this.props.initialCount} }, propTypes: {initialCount: React.PropTypes.number}, tick() { this.setState({count: this.state.count + 1}); }, render() { return ( <div onClick={this.tick}> Clicks: {this.state.count} </div> ); } });
Y comparar con la versión ES6:
class Counter extends React.Component { static propTypes = {initialCount: React.PropTypes.number}; static defaultProps = {initialCount: 0}; constructor(props) { super(props); this.state = {count: props.initialCount}; } state = {count: this.props.initialCount}; tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div onClick={this.tick.bind(this)}> Clicks: {this.state.count} </div> ); } }
Aquí, los métodos de ciclo de vida de React getDefaultProps
y getInitialState
ya no son necesarios. getDefaultProps
se convierte en la variable de clase estática defaultProps
y el estado inicial simplemente se define en el constructor. El único inconveniente es que los métodos ya no se vinculan automáticamente, por lo que debe usar bind
cuando llame a los controladores desde JSX.
Decoradores
Los decoradores son una característica útil de ES7. Le permiten aumentar el comportamiento de una función o clase envolviéndola dentro de otra función. Por ejemplo, supongamos que desea tener el mismo controlador de cambios en algunos de sus componentes, pero no desea comprometerse con el antipatrón de herencia. En su lugar, puede utilizar un decorador de clase. Definamos el decorador de la siguiente manera:
addChangeHandler: function(target) { target.prototype.changeHandler = function(key, attr, event) { var state = {}; state[key] = this.state[key] || {}; state[key][attr] = event.currentTarget.value; this.setState(state); }; return target; }
Lo importante aquí es que la función addChangeHandler
agrega la función changeHandler
al prototipo de la clase objetivo.
Para aplicar el decorador, podríamos escribir:
MyClass = changeHandler(MyClass)
o más elegantemente, con la sintaxis de ES7:
@addChangeHandler class MyClass { ... }
En cuanto al contenido de la función changeHandler
en sí, con la ausencia de enlace de datos bidireccional de React, trabajar con entradas en React puede ser tedioso. La función changeHandler
intenta hacerlo más fácil. El primer parámetro especifica una key
en el objeto de estado, que servirá como objeto de datos para la entrada. El segundo parámetro es el atributo en el que se guardará el valor del campo de entrada. Esos dos parámetros se establecen desde JSX usando la palabra clave bind
.
@addChangeHandler class LoginInput extends React.Component { constructor(props) { super(props); this.state = { login: {} }; } render() { return ( <input type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'login', 'username')} /> <input type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'login', 'password')} /> ) } }
Cuando el usuario cambia el campo de nombre de usuario, su valor se guarda en this.state.login.username
, sin necesidad de definir más controladores personalizados.
Funciones de flecha
La dinámica de this
contexto de JavaScript ha sido un dolor constante para los desarrolladores porque, de forma poco intuitiva, this
contexto de una función anidada se restablece a global, incluso dentro de una clase. Para solucionar esto, generalmente es necesario guardar this
en alguna variable de alcance externo (generalmente _this
) y usarlo en funciones internas:
class DirectorsStore { onFetch(directors) { var _this = this; this.directorsHash = {}; directors.forEach(function(x){ _this.directorsHash[x._id] = x; }) } }
Con la nueva sintaxis de ES6, la function(x){
se puede reescribir como (x) => {
. Esta definición de método de "flecha" no solo vincula correctamente this
al alcance externo, sino que también es considerablemente más corto, lo que definitivamente cuenta cuando se escribe una gran cantidad de código asincrónico.
onFetch(directors) { this.directorsHash = {}; directors.forEach((x) => { this.directorsHash[x._id] = x; }) }
Desestructuración de asignaciones
La desestructuración de tareas, introducida en ES6, le permite tener un objeto compuesto en el lado izquierdo de una tarea:
var o = {p: 42, q: true}; var {p, q} = o; console.log(p); // 42 console.log(q); // true
Esto es bueno, pero ¿cómo nos ayuda realmente en React? Considere el siguiente ejemplo:
function makeRequest(url, method, params) { var config = { url: url, method: method, params: params }; ... }
Con la desestructuración, puede ahorrar algunas pulsaciones de teclas. Al literal de claves {url, method, params}
se le asignan automáticamente valores del alcance con los mismos nombres que las claves. Esta expresión se usa con bastante frecuencia, y la eliminación de la repetición hace que el código sea menos propenso a errores.
function makeRequest(url, method, params) { var config = {url, method, params}; ... }
La desestructuración también puede ayudarlo a cargar solo un subconjunto de un módulo:
const {clone, assign} = require('lodash'); function output(data, optional) { var payload = clone(data); assign(payload, optional); }
Argumentos: Predeterminado, Descanso y Extensión
Los argumentos de función son más poderosos en ES6. Finalmente, puede establecer el argumento predeterminado :
function http(endpoint, method='GET') { console.log(method) ... } http('/api') // GET
¿Cansado de jugar con el objeto de arguments
difíciles de manejar? Con la nueva especificación, puede obtener el resto de los argumentos como una matriz:
function networkAction(context, method, ...rest) { // rest is an array return method.apply(context, rest); }
Y si no le gusta llamar a apply()
, simplemente puede distribuir una matriz en argumentos de función:
myArguments = ['foo', 'bar', 123]; myFunction(...myArguments);
Generadores y funciones asíncronas
ES6 introdujo generadores de JavaScript. Un generador es básicamente una función de JavaScript cuya ejecución se puede pausar y luego reanudar más tarde, recordando su estado. Cada vez que se encuentra la palabra clave yield
, la ejecución se detiene y el argumento yield
se devuelve al objeto que llama:
function* sequence(from, to) { console.log('Ready!'); while(from <= to) { yield from++; } }
Aquí hay un ejemplo de este generador en acción:
> var cursor = sequence(1,3) Ready! > cursor.next() { value: 1, done: false } > cursor.next() { value: 2, done: false } > cursor.next() { value: 3, done: false } > cursor.next() { value: undefined, done: true }
Cuando llamamos a la función generadora, se ejecuta hasta el primer yield
y luego se detiene. Después de llamar a next()
, devuelve el primer valor y reanuda la ejecución. Cada yield
devuelve otro valor, pero después de la tercera llamada, la función del generador termina y cada llamada subsiguiente a next()
devolverá { value: undefined, done: true }
.
Por supuesto, el objetivo de los generadores no es crear secuencias numéricas elaboradas. La parte emocionante es su capacidad para detener y reanudar la ejecución de la función, que se puede usar para controlar el flujo de programa asincrónico y finalmente deshacerse de esas molestas funciones de devolución de llamada.
Para demostrar esta idea, primero necesitamos una función asíncrona. Por lo general, tendríamos alguna operación de E/S, pero para simplificar, usemos setTimeout
y devolvamos una promesa. (Tenga en cuenta que ES6 también introdujo promesas nativas para JavaScript).
function asyncDouble(x) { var deferred = Promise.defer(); setTimeout(function(){ deferred.resolve(x*2); }, 1000); return deferred.promise; }
A continuación, necesitamos una función de consumidor:
function consumer(generator){ var cursor = generator(); var value; function loop() { var data = cursor.next(value); if (data.done) { return; } else { data.value.then(x => { value = x; loop(); }) } } loop(); }
Esta función toma cualquier generador como argumento y sigue llamando a next()
mientras haya valores para yield
. En este caso, los valores producidos son promesas, por lo que es necesario esperar a que se resuelvan las promesas y usar la recursividad con loop()
para lograr el bucle entre funciones anidadas.
El valor devuelto se resuelve en el controlador then()
y se pasa a value
, que se define en el ámbito externo, y que pasará a la llamada next(value)
. Esta llamada hace que el valor sea el resultado de la expresión de rendimiento correspondiente. Esto significa que ahora podemos escribir de forma asincrónica sin ninguna devolución de llamada:
function* myGenerator(){ const data1 = yield asyncDouble(1); console.log(`Double 1 = ${data1}`); const data2 = yield asyncDouble(2); console.log(`Double 2 = ${data2}`); const data3 = yield asyncDouble(3); console.log(`Double 3 = ${data3}`); } consumer(myGenerator);
El generador myGenerator
se detendrá en cada yield
, esperando que el consumidor entregue la promesa resuelta. Y, de hecho, veremos que los números calculados aparecen en la consola en intervalos de un segundo.
Double 1 = 2 Double 2 = 4 Double 3 = 6
Esto demuestra el concepto básico, sin embargo, no recomiendo que use este código en producción. En su lugar, elija una biblioteca bien probada como co. Esto le permitirá escribir fácilmente código asíncrono con rendimientos, incluido el manejo de errores:
co(function *(){ var a = yield Promise.resolve(1); console.log(a); var b = yield Promise.resolve(2); console.log(b); var c = yield Promise.resolve(3); console.log(c); }).catch(function(err){ console.error(err.stack); });
Entonces, este ejemplo muestra cómo escribir código asincrónico sin devoluciones de llamada usando generadores ES6. ES7 lleva este enfoque un paso más allá al introducir las palabras clave async
y await
y eliminar por completo la necesidad de una biblioteca generadora. Con esta capacidad, el ejemplo anterior se vería así:
async function (){ try { var a = await Promise.resolve(1); console.log(a); var b = await Promise.resolve(2); console.log(b); var c = await Promise.resolve(3); console.log(c); } catch (err) { console.error(err.stack); } };
En mi opinión, esto elimina el dolor de trabajar con código asíncrono en JavaScript. No solo en React, sino también en cualquier otro lugar.
Los generadores no solo son más concisos y sencillos, sino que también nos permiten usar técnicas que serían muy difíciles de implementar con devoluciones de llamada. Un ejemplo destacado de la bondad del generador es la biblioteca de middleware koa para Node.js. Su objetivo es reemplazar a Express y, para lograr ese objetivo, viene con una característica excelente: la cadena de middleware fluye no solo hacia abajo (con la solicitud del cliente), sino también hacia arriba , lo que permite una mayor modificación de la respuesta del servidor. Considere el siguiente ejemplo de servidor koa:
// Response time logger middleware app.use(function *(next){ // Downstream var start = new Date; yield next; // Upstream this.body += ' World'; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // Response handler app.use(function *(){ this.body = 'Hello'; }); app.listen(3000);

El middleware de respuesta yield
s aguas abajo en el controlador de respuestas, que establece el cuerpo de la respuesta, y en el flujo ascendente (después de la expresión de yield
), se permite la modificación adicional de this.body
, así como otras funciones como el registro de tiempo, que es posible porque el upstream y el downstream comparten el mismo contexto de función. Esto es mucho más poderoso que Express, en el que un intento de lograr lo mismo terminaría así:
var start; // Downstream middleware app.use(function(req, res, next) { start = new Date; next(); // Already returned, cannot continue here }); // Response app.use(function (req, res, next){ res.send('Hello World') next(); }); // Upstream middleware app.use(function(req, res, next) { // res already sent, cannot modify var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); next(); }); app.listen(3000);
Probablemente ya pueda detectar lo que está mal aquí; el uso de una variable de start
"global" dará como resultado una condición de carrera, devolviendo tonterías con solicitudes concurrentes. La solución es una solución alternativa no obvia, y puede olvidarse de modificar la respuesta en el flujo ascendente.
Además, al usar koa, obtendrá el flujo de trabajo asíncrono del generador de forma gratuita:
app.use(function *(){ try { const part1 = yield fs.readFile(this.request.query.file1, 'utf8'); const part2 = yield fs.readFile(this.request.query.file2, 'utf8'); this.body = part1 + part2; } catch (err) { this.status = 404 this.body = err; } }); app.listen(3000);
Puede imaginar las promesas y las devoluciones de llamada involucradas en la recreación de este pequeño ejemplo en Express.
¿Cómo se relaciona toda esta charla de Node.js con React? Bueno, Node es la primera opción cuando se considera un back-end adecuado para React. Dado que Node también está escrito en JavaScript, admite el uso compartido de código entre el back-end y el front-end, lo que nos permite crear aplicaciones web React isomórficas. Pero, más sobre esto más adelante.
Biblioteca de flujo
React es excelente para crear componentes de vista componibles, pero necesitamos alguna forma de administrar los datos y el estado en toda la aplicación. Se ha acordado casi universalmente que React se complementa mejor con la arquitectura de aplicaciones Flux. Si eres completamente nuevo en Flux, te recomiendo una actualización rápida.
Lo que no ha sido tan universalmente aceptado es cuál de las muchas implementaciones de Flux elegir. Facebook Flux sería la opción obvia, pero para la mayoría de las personas es demasiado detallado. Las implementaciones alternativas se enfocan principalmente en reducir la cantidad de repetitivo requerido con un enfoque de convención sobre configuración, y también con algunas funciones convenientes para componentes de orden superior, representación del lado del servidor, etc. Aquí se pueden ver algunos de los principales contendientes, con varias métricas de popularidad. Investigué Alt, Reflux, Flummox, Fluxxor y Marty.js.
Mi forma de elegir la biblioteca correcta no es de ninguna manera objetiva, pero podría ayudar de todos modos. Fluxxor fue una de las primeras bibliotecas que revisé, pero ahora parece un poco obsoleta. Marty.js es interesante y tiene muchas características, pero aún implica mucho repetitivo, y algunas de las funciones parecen superfluas. Reflux se ve muy bien y tiene algo de tracción, pero se siente un poco difícil para los principiantes y también carece de la documentación adecuada. Flummox y Alt son muy similares, pero Alt parece tener incluso menos repeticiones, un desarrollo muy activo, documentación actualizada y una comunidad útil de Slack. Por lo tanto, elegí Alt.
Flujo alternativo
Con Alt, el flujo de trabajo de Flux se vuelve mucho más simple sin perder nada de su poder. La documentación de Flux de Facebook dice mucho sobre el despachador, pero podemos ignorarlo porque, en Alt, el despachador está implícitamente conectado a acciones por convención y, por lo general, no requiere ningún código personalizado. Esto nos deja solo con tiendas , acciones y componentes . Estas tres capas se pueden usar de tal manera que se correspondan perfectamente con el modelo de pensamiento de MVC : las tiendas son modelos , las acciones son controladores y los componentes son vistas . La principal diferencia es el flujo de datos unidireccional fundamental para el patrón Flux, lo que significa que los controladores (acciones) no pueden modificar directamente las vistas (componentes), sino que, en cambio, solo pueden desencadenar modificaciones del modelo (almacenamiento), a las que las vistas están vinculadas pasivamente. Esta ya era una práctica recomendada para algunos desarrolladores de Angular ilustrados.
El flujo de trabajo es el siguiente:
- Los componentes inician acciones.
- Las tiendas escuchan acciones y actualizan datos.
- Los componentes están vinculados a las tiendas y se vuelven a representar cuando se actualizan los datos.
Comportamiento
Cuando se usa la biblioteca Alt Flux, las acciones generalmente vienen en dos sabores: automático y manual. Las acciones automáticas se crean utilizando la función generateActions
y van directamente al despachador. Los métodos manuales se definen como métodos de sus clases de acción y pueden ir al despachador con una carga útil adicional. El caso de uso más común de las acciones automáticas es notificar a las tiendas sobre algún evento en la aplicación. Las acciones manuales son, entre otras cosas, la forma preferida de tratar con las interacciones del servidor.
Entonces, las llamadas a la API REST pertenecen a las acciones. El flujo de trabajo completo es el siguiente:
- El componente desencadena una acción.
- El creador de la acción ejecuta una solicitud de servidor asíncrona y el resultado se envía al despachador como carga útil.
- La tienda escucha la acción, el controlador de acción correspondiente recibe el resultado como argumento y la tienda actualiza su estado en consecuencia.
Para las solicitudes de AJAX, podemos usar la biblioteca axios, que, entre otras cosas, maneja datos y encabezados JSON sin problemas. En lugar de promesas o devoluciones de llamada, podemos usar el patrón async
/ await
de ES7. Si el estado de la respuesta POST
no es 2XX, se genera un error y enviamos los datos devueltos o el error recibido.
Veamos una página de inicio de sesión para ver un ejemplo simple del flujo de trabajo Alt. La acción de cierre de sesión no necesita hacer nada especial, solo notificar a la tienda, para que podamos generarla automáticamente. La acción de inicio de sesión es manual y espera datos de inicio de sesión como parámetro para el creador de la acción. Después de recibir una respuesta del servidor, enviamos datos de éxito o, si se produce un error, enviamos el error recibido.
class LoginActions { constructor() { // Automatic action this.generateActions('logout'); } // Manual action async login(data) { try { const response = await axios.post('/auth/login', data); this.dispatch({ok: true, user: response.data}); } catch (err) { console.error(err); this.dispatch({ok: false, error: err.data}); } } } module.exports = (alt.createActions(LoginActions));
Historias
La tienda Flux tiene dos propósitos: tiene controladores de acción y transporta estado. Continuemos con nuestro ejemplo de la página de inicio de sesión para ver cómo funciona.
Vamos a crear LoginStore
, con dos atributos de estado: user
, para el usuario que inició sesión actualmente, y error
, para el error relacionado con el inicio de sesión actual. En el espíritu de reducir el modelo estándar, Alt nos permite vincular todas las acciones de una clase con una sola función bindActions
.
class LoginStore { constructor() { this.bindActions(LoginActions); this.user = null; this.error = null; } ...
Los nombres de los controladores se definen por convención, anteponiéndose on
nombre de la acción correspondiente. Entonces, la acción de inicio de login
es manejada por onLogin
, y así sucesivamente. Tenga en cuenta que la primera letra del nombre de la acción se escribirá en mayúscula al estilo camelCase. En nuestro LoginStore
, tenemos los siguientes controladores, llamados por las acciones correspondientes:
... onLogin(data) { if (data.ok) { this.user = data.user; this.error = null; router.transitionTo('home'); } else { this.user = null; this.error = data.error } } onLogout() { this.user = null; this.error = null; } }
Componentes
La forma habitual de vincular las tiendas a los componentes es usar algún tipo de mezcla React. Pero como los mixins están pasando de moda, tiene que haber alguna otra forma. Uno de los nuevos enfoques es utilizar componentes de orden superior. Tomamos nuestro componente y lo colocamos dentro de un componente de envoltura, que se encargará de escuchar las tiendas y llamar a volver a renderizar. Nuestro componente recibirá el estado de la tienda en props
. Este enfoque también es útil para organizar nuestro código en componentes inteligentes y tontos, que se han puesto de moda últimamente. Para Alt, el contenedor de componentes se implementa mediante AltContainer
:
export default class Login extends React.Component { render() { return ( <AltContainer stores={{LoginStore: LoginStore}}> <LoginPage/> </AltContainer> )} }
Nuestro componente LoginPage
también usa el decorador changeHandler
presentado anteriormente. Los datos de LoginStore
se utilizan para mostrar errores en caso de un inicio de sesión fallido, y AltContainer
se encarga de volver a renderizar. Al hacer clic en el botón de inicio de sesión, se ejecuta la acción de inicio de login
, completando el flujo de trabajo Alt flux:
@changeHandler export default class LoginPage extends React.Component { constructor(props) { super(props); this.state = { loginForm: {} }; } login() { LoginActions.login(this.state.loginForm) } render() { return ( <Alert>{{this.props.LoginStore.error}}</Alert> <Input label='Username' type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'loginForm', 'username')} /> <Input label='Password' type='password' value={this.state.login.password} onChange={this.changeHandler.bind(this, 'loginForm', 'password')} /> <Button onClick={this.login.bind(this)}>Login</Button> )} }
Representación isomorfa
Las aplicaciones web isomorfas son un tema candente en estos días porque resuelven algunas de las tareas más importantes de las aplicaciones tradicionales de una sola página. En esas aplicaciones, JavaScript crea dinámicamente el marcado en el navegador. El resultado es que el contenido no está disponible para los clientes con JavaScript desactivado, en particular, los rastreadores web de los motores de búsqueda. Esto significa que su página web no está indexada y no aparece en los resultados de búsqueda. Hay formas de evitar esto, pero están lejos de ser óptimas. El enfoque isomórfico intenta solucionar este problema procesando previamente la URL solicitada de una aplicación de una sola página en el servidor. Con Node.js, tiene JavaScript en el servidor, lo que significa que React también puede ejecutarse en el lado del servidor. Esto no debería ser demasiado difícil, ¿verdad?
Un obstáculo es que algunas bibliotecas de Flux, especialmente aquellas que usan singletons, tienen dificultades con la representación del lado del servidor. Cuando tiene tiendas Flux singleton y múltiples solicitudes simultáneas a su servidor, los datos se mezclarán. Algunas bibliotecas resuelven esto mediante el uso de instancias de Flux, pero esto presenta otros inconvenientes, en particular la necesidad de pasar esas instancias en su código. Alt también ofrece instancias de Flux, pero también ha resuelto el problema de la representación del lado del servidor con singletons; vacía las tiendas después de cada solicitud, de modo que cada solicitud simultánea comience con una pizarra limpia.
React.renderToString
proporciona el núcleo de la funcionalidad de representación del lado del servidor. Toda la aplicación frontal de React también se ejecuta en el servidor. De esta manera, no necesitamos esperar a que el JavaScript del lado del cliente cree el marcado; está preconstruido en el servidor para la URL a la que se accede y se envía al navegador como HTML. Cuando se ejecuta el JavaScript del cliente, continúa donde lo dejó el servidor. Para respaldar esto, podemos usar la biblioteca Iso, que está diseñada para usarse con Alt.
Primero, inicializamos Flux en el servidor usando alt.bootstrap
. Es posible llenar previamente las tiendas Flux con datos para renderizar. También es necesario decidir qué componente representar para qué URL, que es la funcionalidad del Router
del lado del cliente. Estamos usando la versión singleton de alt
, por lo que después de cada procesamiento, necesitamos alt.flush()
las tiendas para tenerlas limpias para otra solicitud. Usando el complemento iso
, el estado de Flux se serializa en el marcado HTML, para que el cliente sepa dónde recoger:
// We use react-router to run the URL that is provided in routes.jsx var getHandler = function(routes, url) { var deferred = Promise.defer(); Router.run(routes, url, function (Handler) { deferred.resolve(Handler); }); return deferred.promise; }; app.use(function *(next) { yield next; // We seed our stores with data alt.bootstrap(JSON.stringify(this.locals.data || {})); var iso = new Iso(); const handler = yield getHandler(reactRoutes, this.request.url); const node = React.renderToString(React.createElement(handler)); iso.add(node, alt.flush()); this.render('layout', {html: iso.render()}); });
En el lado del cliente, tomamos el estado del servidor y arrancamos alt
con los datos. Luego ejecutamos Router
y React.render
en el contenedor de destino, que actualizará el marcado generado por el servidor según sea necesario.
Iso.bootstrap(function (state, _, container) { // Bootstrap the state from the server alt.bootstrap(state) Router.run(routes, Router.HistoryLocation, function (Handler, req) { let node = React.createElement(Handler) React.render(node, container) }) })
¡Encantador!
Bibliotecas front-end útiles
Una guía del ecosistema React no estaría completa sin mencionar algunas bibliotecas front-end que funcionan especialmente bien con React. Estas bibliotecas abordan las tareas más comunes que se encuentran en casi todas las aplicaciones web: diseños y contenedores CSS, formularios y botones con estilo, validaciones, selección de fechas, etc. No tiene sentido reinventar la rueda cuando esos problemas ya se han resuelto.
Reaccionar-Bootstrap
Twitter's Bootstrap framework has become commonplace since it is of immense help to every web developer who does not want to spend a ton of time working in CSS. In the prototyping phase especially, Bootstrap is indispensable. To leverage the power of bootstrap in a React application, it's good to use it with React-Bootstrap, which comes with nice React syntax and re-implements Bootstrap's jQuery plugins using native React components. The resulting code is terse and easy to understand:
<Navbar brand='React-Bootstrap'> <Nav> <NavItem eventKey={1} href='#'>Link</NavItem> <NavItem eventKey={2} href='#'>Link</NavItem> <DropdownButton eventKey={3} title='Dropdown'> <MenuItem eventKey='1'>Action</MenuItem> <MenuItem eventKey='2'>Another action</MenuItem> <MenuItem eventKey='3'>Something else here</MenuItem> <MenuItem divider /> <MenuItem eventKey='4'>Separated link</MenuItem> </DropdownButton> </Nav> </Navbar>
Personally, I cannot escape the feeling that this is what HTML should always have been like.
If you want to use Bootstrap source with Webpack, consider using less-loader or bootstrap-sass-loader, depending on the preprocessor you prefer. It will allow you to easily customize which Bootstrap components to include, and also allow the usage of LESS or SASS global variables in your CSS code.
React Router
React Router has become the de-facto standard for routing in React. It allows nested routing, support for redirections, plays nicely with isomorphic rendering, and has a simple JSX-based syntax:
<Router history={new BrowserHistory}> <Route path="/" component={App}> <Route path="about" name="about" component={About}/> <Route path="users" name="users" component={Users} indexComponent={RecentUsers}> <Route path="/user/:userId" name="user" component={User}/> </Route> <Route path="*" component={NoMatch}/> </Route> </Router>
React Router also provides a Link
component that you can use for navigation in your application, specifying only the route name:
<nav> <Link to="about">About</Link> <Link to="users">Users</Link> </nav>
There is even a library for integration with React-Bootstrap, so if you are using Bootstrap's components and don't feel like setting the active
class on them manually all the time, you can use react-router-bootstrap and write code like this:
<Nav> <NavItemLink to="about">About</NavItemLink> <NavItemLink to="users">Users</NavItemLink> </Nav>
No additional setup is necessary. Active links will take care of themselves.
Formsy-React
Working with forms can be tedious, so let's get some help from the formsy-react library, which will help us manage validations and data models. The Formsy-React library, strangely enough, does not include the actual form inputs because users are encouraged to write their own (which is great). But if you are content with the common ones, just use formsy-react-components. Bootstrap classes are included:
import Formsy from 'formsy-react'; import {Input} from 'formsy-react-components'; export default class FormsyForm extends React.Component { enableButton() { this.setState({canSubmit: true}); } disableButton() { this.setState({canSubmit: true}); } submit(model) { FormActions.saveEmail(model.email); } render() { return ( <Formsy.Form onValidSubmit={this.submit} onValid={this.enableButton} onInvalid={this.disableButton}> <Input name="email" validations="isEmail" validationError="This is not a valid email" required/> <button type="submit" disabled={!this.state.canSubmit}>Submit</button> </Formsy.Form> )} }
Calendar and Typeahead
Calendar and typeahead are icing on the cake of every UI toolkit. Sadly, these two components were removed from Bootstrap 3, probably because they are too specialized for a general purpose CSS framework. Luckily, I've been able to find worthy replacements in react-pikaday and react-select. I've tested more than 10 libraries, and these two came out as the best. They are dead easy to use, as well:
import Pikaday from 'react-pikaday'; import Select from 'react-select'; export default class CalendarAndTypeahead extends React.Component { constructor(props){ super(props); this.options = [ { value: 'one', label: 'One' }, { value: 'two', label: 'Two' } ]; } dateChange(date) { this.setState({date: date}); }, selectChange(selected) { this.setState({selected: selected}); }, render() { return ( <Pikaday value={this.state.date} onChange={this.dateChange} /> <Select name="form-field-name" value={this.state.selected} options={this.options} onChange={selectChange} /> )} }
Conclusion - React.JS
In this article I've presented libraries and techniques that I consider some of the most productive in current web development. Some of them are React-specific, but due to React's open nature, many of them are usable in other environments as well. Technological progress is sometimes hindered by fear of the newest stuff, so I hope this article will help to dissipate doubts concerning React, Flux and the newest features in ECMAScript.
If you are interested, you can take a look at my example application built with these technologies. The source code is available on GitHub.
¡Gracias por leer!