Integrando React en aplicaciones antiguas


La temida migración

Llegó el momento... 10 años desarrollando con nuestra querida tecnología web (ASP.NET Web Forms, PHP, ASP.NET MVC, Ruby...) y han venido del departamento de ventas con las siguientes quejas del sitio web que hay montado:

  • Hay clientes que quieren hacer sus transacciones tirados en el sofá y no pueden, les hace falta tener un portátil a mano.
  • Hay clientes que no pueden completar los pedidos.
  • El 30% de nuestros clientes tiene móviles baratos y no le tira nuestra web.
  • Cuando un cliente está en el campo no puede trabajar con nuestro sitio porque la conexión es muy mala.

Esto traducido a nuestro "idioma":

  • Tenemos una web pobre, no responsiva, que no se adapta a los gestos y demás que podemos hacer con una tablet o un móvil.
  • Tenemos tanta lógica en sphaguetti JavaScript que es ingobernable, de ahí que nuestra aplicación tenga petes aleatorios.
  • Que nuestra aplicación tiene mucha dependencia con el servidor para cosas que se podrían gestionar en cliente.
  • Que nuestra aplicación es muy pesada y consume ancho de banda, batería de móvil y recursos.

Lo peor... que esto pasa hasta con elecciones no tan antiguas ¿Te acuerdas de Angular 1? ¿ Tienes problemas de rendimiento?

Ahora llega el momento de la elección, seguramente tu aplicación web sea un mamotreto enorme y no puedes cerrar las cortinas un par de años y migrarla por completo.

¿No habría forma de ir haciendo migraciones parciales? ¡React al rescate!

React

React es una librería ligera para manejar interfaz de usuario que tiene un rendimiento muy bueno, además nos permite componentizar nuestras páginas.

Una de las principales ventajas de React con respecto a otras tecnologías web es que nos permite ir reemplazando partes de una página y coexistir con librerías antiguas.

Veamos cómo.

Aproximaciones

Todos los ejemplos que van a ser mostrados se encuentran visibles de forma pública en el repositorio de GitHub de Lemoncode: integrate-react-legacy-apps.

Opción 1 - Componentes presentacionales de React junto a jQuery

Aunque React y jQuery son dos librerías que se encargan de resolver diferentes problemas y puedan parecer opuestas (jQuery se basa en la manipulación directa del DOM, por el contrario, React anima a evitar tocar el DOM en lo máximo posible) son capaces de coexistir.

Un ejemplo básico sería el tener un módulo que al inicializarse pida datos al servidor y los vuelque en una tabla que es servida al usuario.

 
 

Podemos crear una pequeña función que extienda el prototipo de jQuery y permita dado un selector poder montar sobre dicho elemento un componente de React. La implementación podría ser la siguiente:

jquery-react

$.fn.extend({
  react: function (Component, props, callback) {
    var mountedComponent = ReactDOM.render(
      <Component {...props} />,
      this.get(0)
    );

    if (typeof callback === 'function') {
      return callback(mountedComponent);
    }

    return mountedComponent;
  }
});

Esto nos permite, dado una definición de un componente React y unas propiedades, montar dicho componente en el nodo del selector de jQuery:

contactsModule

var ContactsTableComponent = App.components.ContactsTableComponent;
var contacts, $mountedTableComponent;

// initialize components
var createReactComponents = function () {
  $mountedContactsTableComponent = $('#tableComponent');
  showContacts(null); // initial render, no data
};

// Fill table, then mount/update React component
var showContacts = function (contacts, callback) {
  $mountedContactsTableComponent.react(ContactsTableComponent, { contacts: contacts || [] }, callback);
};

La idea de esta implementación es usar los selectores de jQuery para servir como puntos de montaje de componentes de React. Los datos del modelo son almacenados en el módulo o página y servidos a componentes de React mediante propiedades.

React component

var ContactPropTypes = App.PropTypes.ContactPropTypes;
var ContactRowComponent = App.components.ContactRowComponent;

var ContactsTableComponent = function (props) {
  var contacts = props.contacts || [];
  return (
    <table className="table table-stripped table-bordered table-hover">
      <thead>
        <tr>
          <th>Name</th>
          <th>Phone number</th>
          <th>Email</th>
        </tr>
      </thead>
      <tbody>
        {contacts.map(function (contact, index) {
          return <ContactRowComponent key={index} contact={contact} />;
        })}
      </tbody>
    </table>
  );
};

ContactsTableComponent.displayName = 'ContactsTableComponent';
ContactsTableComponent.propTypes = {
  contacts: React.PropTypes.arrayOf(ContactPropTypes)
};

Así, al responder el servidor a una petición AJAX con los datos que necesita el componente podemos actualizarlo:

var fetchContacts = function () {
  $.when(contactsService.fetchContacts())
    .then(function (fetchedContacts) {
    contacts = fetchedContacts;
    showContacts(contacts);
  });
};
 
 

 

La demostración de este ejemplo se encuentra en el siguiente enlace al repositorio anteriormente mencionado: 02 Props and Render.

Opción 2- Componentes de React con estado junto a jQuery

Cuando trabajamos con componentes, es normal que tengamos componentes con mayor lógica de presentación y necesiten almacenar un estado interno. Estos componentes son comúnmente llamados componentes contenedores. Un componente contenedor no es más que un componente que se encarga de manejar el estado de una parte de la aplicación, es decir, se encarga de la lógica de negocio.

Desde jQuery podríamos entonces realizar más acciones a parte de montarlo, como poder acceder a propiedades y métodos públicos, permitiendo hacer cambios en el estado del componente:

var ContactsTableContainer = App.components.ContactsTableContainer;
var contacts, $mountedContactsTableContainer;

var createReactComponents = function () {
    $mountedContactsTableContainer = $('#tableComponent').react(ContactsTableContainer, null);
};

var showContacts = function (contacts, callback) {
  // Accessing React component API methods
  $mountedContactsTableContainer.setState({ contacts: contacts });
};

En este caso se almacena la instancia del componente montado por ReactDOM y se almacena en una variable. De esta forma podemos llamar a su método setState para modificar el estado. Cabe mencionar que dicha llamada podría estar perfectamente encapsulada en un método público que hayamos definido que realice algunas validaciones antes de cambiar el estado.

 

Esta opción puede ser útil en algunos casos pero tampoco es lo ideal, ya que el llamar de forma incorrecta a métodos del ciclo de vida de React podría tener efectos inesperados. Lo más común es que sean los propios componentes de React quienes interactuen entre sí mediante los métodos recibidos por sus propiedades.

 

 
 

La implementación completa se encuentra en el apartado 03 Stateful Component del repositorio.

 

Opción 3 - Patrón Pub/Sub mediante $.Callbacks de jQuery

 

El patrón Publish-Subscribe es un patrón que puede ser muy útil para comunicar React con jQuery ya que las acciones enviadas por los canales de comunicación son enviadas a aquellos que estén suscritos a dicho canal sin tener conocimiento de los demás oyentes. Veamos cómo podríamos añadir una simple implementación del patrón Pub/Sub mediante un método que pueda ser accedido desde jQuery:

$.observe = (function () {
  var subjects = {};
  return function (id) {
    var callbacks;
    var subject = id && subjects[id];

    if (!subject) {
      callbacks = $.Callbacks();
      subject = {
        publish: callbacks.fire,
        subscribe: callbacks.add,
        unsubscribe: callbacks.remove
      };
     if (id) {
        subjects[id] = subject;
      }
    }
    return subject;
  };
})();

Los canales de comunicación son almacenados en el objeto subject y son servidos utilizando los métodos de $.Callbacks publicados mediante acciones más comunes asociadas a este patrón, como publish, subscribe,  y unsubscribe. Para poder implementar dicho patrón los componentes de React se suscriben/de-suscriben a los canales de comunicación en los métodos del ciclo de vida componentDidMount y componentWillUnmount:

var ContactPropTypes = App.PropTypes.ContactPropTypes;
var ContactsTableComponent = App.components.ContactsTableComponent;

var ContactsTableContainer = React.createClass({
  displayName: 'ContactsTableContainer',
  onAddContact: function (contact) {
    this.setState({
      contacts: this.state.contacts.concat(contact)
    });
  },
  componentDidMount: function () {
    $.observe('addContacts').subscribe(this.onAddContact);
  },
  componentWillUnmount: function () {
    $.observe('addContacts').unsubscribe(this.onAddContact);
  },
  getInitialState: function () {
    return {
      contacts: []
    };
  },
  render: function () {
    return <ContactsTableComponent contacts={this.state.contacts} />;
  }
});

El componente expone un método para cambiar su estado que será llamado una vez alguien envíe datos mediante $.observe().publish por el canal addContacts. Así, cuando nuestra aplicación necesite cambiar el estado del componente basta con enviar los datos por ese canal. Veamos cómo podemos pasar los datos traídos del servidor al componente:

var fetchContacts = function () {
  $.when(contactsService.fetchContacts())
    .then(function (fetchedContacts) {
      contacts = fetchedContacts;
      $.observe('addContacts').publish(contacts);
    });
};

 

Al hacer un publish en el canal addContacts los contactos son recibidos por el método onAddContact del componente y cambia su estado, por lo que se activan los métodos del ciclo de vida del componente, entre ellos render y provoca un cambio en el HTML. Utilizando este patrón no tenemos la necesidad de almacenar instancias del componente en el módulo:

var ContactsTableContainer = App.components.ContactsTableContainer;
var contacts;

var createReactComponents = function () {
  ReactDOM.render(
    <ContactsTableContainer />,
    $('#tableComponent').get(0)
  );
};
 
 

 

La implementación completa la puedes encontrar en el apartado 04 Event Emitters.

 

Opción 4 - React dentro de aplicaciones Angular 1.X con arquitectura MVC

Angular permite crear componentes mediante el uso de angular.directive, por lo que integrar componentes de React no es tan difícil a grandes rasgos gracias a la librería ngReact, que nos permite crear componentes utilizando la directiva react-component o el factory reactDirective.

Veamos un simple ejemplo donde tenemos un pequeño formulario que solicita un dato para codificar en base64 e integramos un componente de React para mostrar su resultado usando react-component:

 

Formulario (index.html)

<section ng-controller="Main as ctrl">
  <form class="col-md-5" action="#" ng-submit="ctrl.encode($event)">
    <h3>Base64 encoder</h3>
    <div class="form-group">
      <label for="txtMessage">Encode a text</label>
      <input type="text" class="form-control" id="txtMessage" ng-model="ctrl.text" placeholder="Message to encode">
    </div>
    <div class="form-group">
      <button type="submit" class="btn btn-primary btn-block">Encode</button>
    </div>
  </form>
  <div class="col-md-8">
    <!-- React Component here to visualize encoded text -->
  </div>
</section>

 

Creamos el módulo con dependencia a la librería ngReact, y el controlador para almacenar el modelo:

 

Módulo App

angular.module('app', ['react']);

Controlador

// src/controllers/main-controller.js
function Main() {
  var self = this;
  self.text = '';
  self.encodedText = '';
  self.encode = function (event) {
    event.preventDefault();
    self.encodedText = btoa(self.text);
    self.text = '';
  };
}
angular.module('app').controller('Main', [Main]);

 

Y por último el componente de React que recibirá el texto codificado por propiedades:

Componente React

var ShowEncoded = function (props) {
  return (
    <div>
      <h4><strong>Encoded text</strong></h4>
      <pre>{props.encoded || 'Nothing written'}</pre>
    </div>
  );
};

ShowEncoded.propTypes = {
  encoded: React.PropTypes.string.isRequired
};

// Store React component in Angular module
angular.module('app').value('ShowEncoded', ShowEncoded);

Para llamar a nuestro componente bastará con añadir la etiqueta react-component en el formulario que apunte a nuestro componente basta con añadir:

<react-component name="ShowEncoded" props="{encoded: ctrl.encodedText}" watch-depth="reference" />

Internamente ngReact delega las propiedades del modelo al componente mediante el atributo props cuando cambien consiguiendo que nuestro componente se actualice. Bastante simple, ¿verdad?

 

Puedes ver el código fuente con el ejemplo montado en 05.A Angular controllerAS & Directive.

 

Otra forma de añadir el componente a nuestra aplicación de Angular es cambiar la exportación del componente mediante la factory reactDirective que provee ngReact:

 

// AngularJS directive definition
var showEncoded = function (reactDirective) {
  return reactDirective(ShowEncoded);
};

angular.module('app').directive('showEncoded', ['reactDirective', showEncoded]);

Así en el formulario nuestro componente queda de la siguiente manera:

<show-encoded encoded="ctrl.encodedText" watch-depth="reference" />
 
 

La implementación de esta aproximación está disponible en la sección 05.B Angular controllerAS & factory.

Opción 5 - React dentro de aplicaciones Angular 1.X basadas en componentes

A partir de Angular 1.5 es posible utilizar el método angular.component para componentizar nuestra aplicación. Una de las grandes ventajas que ofrece la arquitectura basada en componentes es el poder dividir la aplicación en pequeñas piezas reusables que puedan ser aprovechadas en más de un lugar. Partiremos de la base de que tenemos un componente de acordeón creado en Angular y nos gustaría migrarlo a React. Nuestro componente de acordeón podría paracerse al siguiente:

 

Accordion Controller

function AccordionController() {
  var self = this;
  var panels = [];

  self.addPanel = function (panel) {
    panels.push(panel);
    if (panels.length) {
      panels[0].show();
    }
  };

  self.select = function (selectedPanel) {
    panels.forEach(function (panel) {
      if (panel === selectedPanel) {
        panel.show();
      } else {
        panel.hide();
      }
    });
  };
}

angular
  .module('app')
  .component('accordion', {
    bindings: {
      feeds: '<'
    },
    templateUrl: './dist/components/accordion/accordion.html',
    controller: AccordionController
  });

Accordion Template

<!-- components/accordion/accordion.html -->
<div class="panel-group" role="tablist">
  <accordion-panel ng-repeat="feed in $ctrl.feeds track by $index" feed="feed" />
</div>

 

Dicho acordeón mostrará un grupo de paneles mostrando los feeds recibidos de un componente padre. El panel no es más que otro componente reusable con controlador y plantilla:

 

AccordionPanel Controller

function AccordionPanel() {
  var self = this;
  var selected = false;

  self.$onInit = function () {
    self.parent.addPanel(self);
  };

  self.select = function () {
    self.parent.select(self);
  };

  self.show = function () {
    if (selected) {
      self.hide();
    } else {
      selected = true;
      self.active = 'in';
    }
  };

  self.hide = function () {
    selected = false;
    self.active = '';
  };
}

angular
  .module('app')
  .component('accordionPanel', {
    bindings: {
      feed: '<'
    },
    require: {
      parent: '^accordion'
    },
    templateUrl: './dist/components/accordion/accordion-panel/accordion-panel.html',
    controller: AccordionPanel
  });

 

AccordionPanel Template

<!-- components/accordion/accordion-panel.html -->
<div class="panel panel-default">
  <div class="panel-heading" style="cursor: pointer" ng-click="$ctrl.select()" role="panel">
    <h3 class="panel-title">{{$ctrl.feed.heading}}</h3>
  </div>
  <div class="panel-body collapsible" ng-class="$ctrl.active">
    <p class="feed-content">{{$ctrl.feed.content}}</p>
  </div>
</div>

 

Implementación

<main class="container">
  <div class="col-sm-9">
    <accordion feeds="$ctrl.feeds" />
  </div>
</main>

 

Veamos cómo quedaría el acordeón sustituído por componentes de React:

 

React Accordion

var Accordion = function (AccordionPanel) {
  return React.createClass({
    displayName: 'Accordion',
    getInitialState: function () {
      return {
        selected: null
      };
    },
    render: function () {
      var feeds = this.props.feeds || [];
      var selected = this.state.selected;
      var self = this;
      return (
        <div className="panel-group" role="tablist">
          <h1>{this.props.foo}</h1>
          {feeds.map(function (feed, index) {
            return (
              <AccordionPanel
                active={selected === feed.id}
                onSelect={self.select}
                key={index}
                feed-id={index}
                feed={feed}
              />
            );
          })}
        </div>
      );
    },
    select: function (selected) {
      if (selected === this.state.selected) {
        selected = null;
      }
      this.setState({ selected });
    }
  });
}

angular
  .module('app')
  .factory('Accordion', ['AccordionPanel', Accordion]);

React AccordionPanel

function AccordionPanel(props) {
  var select = function () {
    return props.onSelect(props.feed.id);
  };
  var className = 'panel-body collapsible';
  if (props.active) {
    className += ' in';
  }
  return (
    <div className="panel panel-default">
      <div className="panel-heading" style={{ cursor: 'pointer' }} onClick={select} role="panel">
        <h3 className="panel-title">{props.feed.heading}</h3>
      </div>
      <div className={className}>
        <p className="feed-content">{props.feed.content}</p>
      </div>
    </div>
  );
}

AccordionPanel.displayName = 'AccordionPanel';
AccordionPanel.propTypes = {
  active: React.PropTypes.bool,
  onSelect: React.PropTypes.func,
  feed: React.PropTypes.shape({
    id: React.PropTypes.number.isRequired,
    heading: React.PropTypes.string,
    content: React.PropTypes.string
  })
};

angular
  .module('app')
  .value('AccordionPanel', AccordionPanel);

Implementación

<main class="container">
  <div class="col-sm-9">
    <react-component name="Accordion" props="{feeds: $ctrl.feeds}" watch-depth="reference" />
  </div>
</main>

 

El ejemplo completo lo tienes disponible en 05.C Angular components & directive.

 

Conclusión

 

React es una librería bastante completa y de alto rendimiento que nos permite desarrollar de una forma muy modular y escalable, además de tener un excelente grupo de herramientas que nos facilitan su desarrollo como React Developer Tools o librerías de testing como Enzyme o Jest para garantizar la calidad de nuestra aplicación.

 

Aunque React en sí no se encargue de todas las partes de una aplicación sí tiene un ecosistema enorme de librerías que integran perfectamente entre sí y cubren todos los ámbitos en el desarrollo de una aplicación web como Redux, para mantener el estado de la aplicación,  o React-Router para manejo de rutas y sincronización de la navegación con componentes de la aplicación. Además puedes reutilizar los componentes para crear apliaciones híbridas con React Native