[TOC]
El stack tecnológico que estamos usando hoy en día se compone de dos partes que se encuentran íntimamente relacionadas:
- Express + NodeJS (+ Falcor y otros): Proveen las apis y procesamiento server-side.
- React, React-DOM, React-Router, Redux y Redux-Saga (+ Falcor, fetch api)
Ambas partes son Javascript (ES6 o ES2015), y ahí cesan las similitudes.
¿Qué sucede del lado del servidor? Hoy por hoy es una aplicación basada en Express, que conecta a un backend de mongodb para levantar los datos de los usuarios y acl, y a la monster api para servir data de nuestro cluster. Es una aplicación relativamente sencilla en componentes, separada en controladores, servicios y modelos.
- Controladores: son la "cara bonita" de la api, y se encargan de traducir la data del request en parámetros para llamar a servicios.
- Servicios: proveen toda la lógica de negocios de la aplicación.
- Modelos: repositorios de información. Pueden ser modelos de Mongoose o respuestas de una api remota.
- Middlewares: proveen info especial para requests, como es autenticación, autorización, errores y métricas.
La aplicación en Express es un API server. Un App server, para el mundo de React, no sólo puede proveer una API, sino que realizar un primer render de la página y evitar un round trip a la API (Isomorphic Rendering), o bien servir varios assets inline dependiendo de la vista a cargar. Esto es algo que no hacemos en Spectro ni se encuentra en el corto o mediano plazo del diseño de frontend.
Tené esto a mano, no importa cuándo lo leas: React Cheatsheet.
- React
- React DOM
- Redux
- React-Redux
- Redux Saga
- React Router
- Webpack
React es una librería y una filosofía de diseño. Se basa en Componentes, que son unidades autocontenidas de visualización
(podemos pensarlos como templates) y un contexto (propiedades), encapsulando su comportamiento de forma que éstos evitan
tener efectos secundarios. El objetivo de los componentes en React es que puedan ser rendereados de forma tal que sólo
dependan de sus propiedades. La principal función de React es render
, que se encarga de mostrar el componente.
var React = require('react');
class MyComponent extends React.Component {
render() {
return (<div>
<h1>Hello { this.props.msg }</h1>
<span>{ this.props.children }</span>
</div>);
}
}
class MyComponent2 extends React.Component {
render() {
return (<div>
<MyComponent msg="world"> this goes inside the span </MyComponent>
<MyComponent msg="cruel" />
</div>);
}
}
Ahora bien, React es demasiado global y puede ser utilizado en otras plataformas (ver: React Native). Para nuestro caso en particular, nosotros usamos React DOM en el browser. React DOM mantiene un DOM Virtual y se encarga sólo de mutar los elementos del DOM que sufrieron cambios o que deben ser actualizados. Lo interesante es que tanto React como React DOM son independientes del browser, por lo que pueden ser rendereados por NodeJS.
var React = require('react');
var ReactDOM = require('react-dom');
class MyComponent extends React.Component {
render() {
return <div>Hello World</div>;
}
}
ReactDOM.render(<MyComponent />, node);
On the server
var React = require('react');
var ReactDOMServer = require('react-dom/server');
class MyComponent extends React.Component {
render() {
return <div>Hello World</div>;
}
}
ReactDOMServer.renderToString(<MyComponent />);
De hecho, la api completa de React DOM es:
react-dom
- findDOMNode
- render
- unmountComponentAtNode
react-dom/server
- renderToString
- renderToStaticMarkup
And now for something completely different...
Comunicar información y compartir propiedades entre componentes de React es una paja, porque tendría que pasar props a absolutamente todo lo que ilumina el DOM, y además tendría que fijarme si los componentes tienen que re-renderearse.
Para solucionar el tema de la propagación de la información de forma tal que sea consistente, minimizando efectos secundarios, y que sea sencilla de debuggear, Facebook nos trae el patrón Flux, que es implementado por Redux de la siguiente forma:
- Store: the Source of All Truth. Contiene el estado de la aplicación en cualquier momento, y es la única fuente de datos existente.
- Actions: son "mensajes" que serán despachados por Redux. Contienen al menos un tipo y pueden, opcionalmente, tener un payload.
- Reducers: una función sin efectos secundarios que recibe un estado, una Action y retorna un nuevo estado. Es clave que sea una función sin efectos secundarios. Cada Reducer se encarga de un pedazo/key del estado.
- Selectors: son funciones que toman un pedazo del estado y retornan una parte. Se puede pensar a un selector como una consulta SELECT de SQL sobre una base de datos.
- Subscribe/Dispatch: es la forma en la que nos conectamos con Redux.
Nota: Redux es a Flux lo que SpringMVC es a MVC.
import { createStore } from 'redux'
function counterReducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
let store = createStore(counterReducer)
store.subscribe(() => console.log(store.getState()))
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
Hasta ahora Redux no tiene nada que ver con React. De hecho, puede usarse para apps mucho más legacy (por ejemplo,
usando Backbone, podríamos reemplazar los event emitter). La parte copada es cuando empezamos a considerar a los
componentes de React como pertenecientes a dos clases distintas: los presentacionales (que son básicamente un
render
), y los contenedores (que son aquellos que manipulan lógica de negocio).
Redux claramente afecta a los segundos. Entonces, ¿cómo los unimos? Enter React-Redux. Los wachines de Facebook nos traen esta tabla:
Presentational Components | Container Components | |
---|---|---|
Purpose | How things look (markup, styles) | How things work (data fetching, state updates) |
Aware of Redux | No | Yes |
To read data | Read data from props | Subscribe to Redux state |
To change data | Invoke callbacks from props | Dispatch Redux actions |
Are written | By hand | Usually generated by React Redux |
Así, podemos tener el siguiente componente de React:
// link.jsx
import React, { PropTypes } from 'react'
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (<a href="#" onClick={e => { e.preventDefault(); onClick() }}>
{children}
</a>)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
Notemos que ahora tenemos PropTypes. ¿Qué es eso? Resulta que ahora los componentes van a ser instanciados por nosotros y por otros team members, por lo que tenemos que explicarle qué tipo tienen las propiedades y si son necesarias. Las PropTypes son opcionales, pero si no las ponemos la app es indebuggeable.
// linkList.jsx
import React, { PropTypes } from 'react'
import Link from './Link'
import { removeLink } from './actions'
const LinkList = ({ links, onLinkClick }) => (
<ul>
{links.map(link =>
<li><Link id={link.id} onClick={() => onLinkClick(link.id)}>
{link.text}
</Link></li>
)}
</ul>
);
const mapStateToProps = (state, ownProps) => {
return {
links: state.links
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onLinkClick: (id) => {
dispatch(removeLink(id))
}
}
}
LinkList.propTypes = {
links: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
text: PropTypes.string.isRequired
}).isRequired).isRequired,
onLinkClick: PropTypes.func.isRequired
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(LinkList);
Este nuevo componente sí tiene pinta de mantener estado. Para usarlo con Redux, llamamos a
connect
, que devuelve un nuevo componente con todos los cables puestos. ¿Qué hace internamente?
Enchufa al componente en ReactComponent.componentDidMount
con ReduxStore.subscribe
. Cada vez que se
modifica el store (con Redux), llama a mapStateToProps
(que pasa el state a... props). Y mapDispatchToProps
cablea el dispatch
a métodos disponibles en las props.
Convenientemente dejé afuera actions.js
, pero sería algo así:
export function removeLink(id) {
return {
type: 'REMOVE_LINK',
payload: id
};
};
Y el Reducer para esto:
const selectorForLinks = (state) => { return state.links };
function reducer(state, action) {
if (action.type === 'REMOVE_LINK') {
const newState = Object.assign({}, state, {
links: selectorForLinks(state).filter((link) => {
return link.id != action.id;
})
})
return newState;
}
return state;
}
Notemos: hay una función combineReducers
que te deja componer reducers, pero no entremos en detalle.
Cuando tenemos acciones que son asincrónicas, todo el esquema de funciones puras se complica, porque tengo que
avisarle a Redux que cada función async terminó (con un dispatch
). Claramente hay una mejor forma: Redux Sagas.
Para simplificar el manejo del estado, Redux Sagas introduce el concepto de Sagas, que son generadores que
utilizan una serie de funciones (call
, put
y otras) para volver el flujo asincrónico en algo mucho más parecido
a una función sincrónica.
Ejemplo: supongamos que tenemos el siguiente componente:
class UserComponent extends React.Component {
...
onSomeButtonClicked() {
const { userId, dispatch } = this.props
dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})
}
...
}
Claramente, este componente quiere un usuario, que seguro impacta contra una api remota asincrónica.
//sagas.js
import { takeEvery, takeLatest } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
import Api from './api'
// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
function* mySaga() {
yield* takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;
Ahora bien, ¿cómo cableo esto a mi código?
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// then run the saga
sagaMiddleware.run(mySaga)
Hasta ahora React sólo maneja una página, y cuando apretamos un link se va para cualquier lado. Si queremos mostrar diversas páginas en nuestra aplicación entonces debemos recurrir a React Router.
En nuestra app se ve distinto (pasa las rutas como props a Router), pero es análogo a lo siguiente:
ReactDOM.render((
<Router>
<Route path="/" component={MainLayout}>
<IndexRoute component={Home} />
<Route component={SearchLayout}>
<Route path="users" component={UserList} />
<Route path="users/:userId" component={UserProfile} />
<Route path="widgets" component={WidgetList} />
</Route>
</Route>
</Router>
), document.getElementById('root'));
Webpack es un bundler. Se encarga de convertir los *.jsx
, *.css
(y css modules), Javascript e imágenes
en paquetes minificados y targeteados a los browsers que queremos. Para hacer esto, usa las siguientes herramientas:
- SystemJS: es un module loader para el browser. Se encarga de cargar los pedazos de la aplicación asincrónicamente.
- BabelJS: convierte el código en ES6 y ES2017 (últimas versiones de js) en ES5 o algo que los browsers más viejos entiendan.
- NPM: para bajar módulos