Skip to content

Instantly share code, notes, and snippets.

@pcostesi
Created October 21, 2016 23:14
Show Gist options
  • Save pcostesi/07e73935bda71088bbb69f27e4489f9c to your computer and use it in GitHub Desktop.
Save pcostesi/07e73935bda71088bbb69f27e4489f9c to your computer and use it in GitHub Desktop.

Frontend and You

[TOC]


Introduction (a.k.a. Talk your coworkers about React before someone else does)

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.


Server-side

¿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.

API server vs App Server

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.


Client-side

Tené esto a mano, no importa cuándo lo leas: React Cheatsheet.

Ingredientes

  • React
  • React DOM
  • Redux
  • React-Redux
  • Redux Saga
  • React Router
  • Webpack

WTF is this

React

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>);
  }
}

React DOM

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

Redux

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

React-Redux

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.

Redux Saga

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)

React Router

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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment