Desenvolvimento

3 nov, 2017

Como migrar uma aplicação de AngularJS para React

Publicidade

Recentemente assisti uma palestra da Kete Martins Rufino e do Christiano Milfont na @ReactConfBR, sobre como migrar aplicações legadas para React e fiquei inspirado em tirar esse artigo da gaveta.

No começo desse ano, fui contratado pela BEN Group, tendo como principal objetivo, migrar uma aplicação legada de Angular para React e Redux. Desde então, criamos algumas soluções dentro do projeto, que acabaram funcionando muito bem para nós.

Nesse artigo, pretendo mostrar as principais abordagens que seguimos e compartilhar algumas soluções que criamos, para poder migrar o projeto gradualmente e sem perder a sanidade.

Disclaimer: O nosso foco aqui não é refatorar código legado, mas sim removê-lo o quanto antes. Então nós evitamos soluções que tomem muito tempo e que foquem em deixar o código atual mais “bonito”. Ao mesmo tempo, nós prezamos por escrever código novo com qualidade.

Mover o build da aplicação para webpack

Esse passo eu considero o mais importante de todo o processo, uma vez que com o Webpack você pode utilizar o import para importar suas dependências e módulos e pode começar a se livrar das Dependency Injection (DI) do Angular. Ele também será necessário para que nós possamos começar a escrever código React na aplicação.

Se você utiliza template cache, Pug(Jade) ou qualquer outra coisa que influencie no build, não se preocupe, o webpack terá um loader para cada um deles. Lembre-se de deixar o Webpack configurado para interpretar es6 e jsx.

O foco desse passo não é mover todas as DI para imports, e sim fazer o seu build funcionar com o Webpack. É importante ter isso em mente, para evitar ficar nessa tarefa por semanas e gerar conflitos em dezenas de arquivos.

Em Angular, normalmente o processo de build pega todas as dependências que você precisa da pasta node_modules e insere dentro do bundle. Nós precisamos manter esse comportamento no novo build.

Você precisa considerar o código legado como um inimigo a ser vencido. Por esse motivo, precisamos agir com cautela e ser estratégicos. Isso também significa, que em certos momentos, você vai precisar fazer coisas que não são agradáveis.

O que nós fizemos, foi criar um arquivo vendor.js, importando todas as dependências, como no exemplo abaixo:

require('angular');
require('angular-resource');
// ...other dependencies

A grande maioria delas, aos ser importada se registra globalmente no objeto window. Então a única coisa que precisamos fazer, é importa-las como no exemplo acima. Porém, algumas dependências não o fazem e precisamos fazê-lo de forma manual. Segue abaixo um exemplo do que precisamos fazer com o moment e o jQuery:

window.moment = require('moment');
window.$ = require('jquery');
window.jquery = window.$;
window.jQuery = window.$;

Essa prática pode soar bem estranha, porém, você precisa levar em conta que muitas de suas dependências estão considerando que o jQuery estará no objeto window. Algumas delas utilizam window.$, outras window.jQuery e outras window.jquery

Após criar o arquivo vendor, importe-o no arquivo entry point da sua aplicação e assim todas as suas dependências estarão no bundle.:

require('./vendors');

Outra etapa, é garantir que os arquivos da aplicação estão no bundle. O ideal, é que cada módulo da sua aplicação, tenha um arquivo index, que importa seus controllers, factories, views e etc. Tendo isso, basta importá-los no entry point, da mesma forma que o arquivo vendors, como no exemplo abaixo:

require('./vendors');
require('./app/common/index');
require('./app/core/index');
require('./app/layout/index');

Se você não tem o index, você pode recorrer a uma solução um pouco mais ousada, porém não muito indicada, que seria encontrar um padrão para os arquivos da sua aplicação e importá-los utilizando regex:

function requireAll(r) {
  r.keys().forEach(r);
}
requireAll(require.context('./app/', true, /\.(js|jsx)$/));

O código acima, fará com que o webpack inclua no bundle todos os arquivos .js e .jsx que estão dentro da pasta app e dentro de suas subpastas. Se você optar por seguir esse caminho, lembre-se que você pode ter arquivos .tes.js ,.spec.js e .stories.js, e terá que exclui-los na regex.

Lembre-se também, que em alguns casos, o Angular está contando com a ordem do carregamento dos seus arquivos, então pode ser que a solução com regexm simplesmente não funcione.

Feito isso, crie um pull request para sua branch principal, teste devidamente e faça um merge o quanto antes. Independente de React, mover o build para webpack já é um grande ganho para sua aplicação. A DI do Angular gera um acoplamento gigantesco e o webpack é o nosso principal aliado contra isso.

Renderizar Componentes React dentro do Angular.

A segunda parte mais importante do processo, pois sem isso, não tem como migrar gradualmente. A ideia desse passo, é que você consiga utilizar os componentes React, dentro do Angular, como se fossem diretivas. Para isso, nós estamos utilizando ngReact em nosso projeto.

O próprio repositório do ngReact está indicando o uso de outra lib, react2Angular. Porém, nós estamos utilizando o Angular na versão 1.5.8, e acabamos enfrentando alguns problemas ao tentar utilizar a outra lib. Eu já utilizei react2Angular em outro projeto, que utilizava uma versão mais recente do Angular e não tive problema algum.

Enfim, o ngReact, mesmo não sendo mais atualizado, tem todas as features que precisamos para transformar nossos componentes em diretivas. A minha dica é, escolha a lib que funcione para você e siga em frente, as duas são bem similares.

Para integrar o ngReact no projeto, basta instala-lo via npm.

$ npm i --save ngreact

E importá-lo no seu arquivo de vendors:

require('ngreact');

Você também precisará instalar o react e o react-dom no seu projeto:

npm i --save react react-dom

E depois, registrar o módulo react ao Angular

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

Feito isso, podemos criar um component Button, como criaríamos em uma aplicação React:

import React from 'react';

const Button = ({ children, ...restProps }) => (
  <button {...restProps}>{children}</button>
);

export default Button;

E então, definimos uma diretiva que utilizará ele:

import Button from 'path/to/Button';

const props = [
  'children',
  'id',
  'className',
  'disabled',
  'etc..',
];
const ReactButton = reactDirective => reactDirective(Button, props);
ReactButton.$inject = ['reactDirective'];

export default ReactButton;

No arquivo da diretiva, nós devemos definir o nome de todas as props que o component utiliza, para que o ngReact entenda o que ele deve passar para o componente.

Definida a diretiva, nós precisamos registrá-la no angular:

import reactButton from 'path/to/react-button';

angular
  .module('app')
  .directive(‘reactButton’, reactButton);

O módulo do Angular que você utilizará para registrá-la, não é crucial, apenas se certifique-se de que ela foi registrada na aplicação.

Uma vez registrada, nós podemos utilizar a diretiva em qualquer view, como no exemplo abaixo:

<div>
  <react-button class-name="btn"></react-button>
</div>

Observe que aqui, ao invés do CamelCase nós utilizamos dash (-) para separar as palavras. Nesse caso, reactButton vira react-button e className se torna class-name. É importante manter isso em mente, pois esse é um erro bem comum e que pode te levar a horas de debug.

É comum utilizarmos o ngReact para renderizar pequenos componentes dentro de uma aplicação Angular. Porém, migrar componente por componente, é pouco produtivo.

O Angular UI Router, permite que nós passemos um parâmetro template nas configurações de rotas. Explorando isso, podemos construir um componente wrapper para cada tela da aplicação, e utilizar conforme o exemplo abaixo:

$stateProvider.state('user.login', {
  url: '/login',
  template: '<react-screen-login></<react-screen-login>',
});

No exemplo acima, definimos uma rota de login e passamos para ela o componente que representa toda a tela de login. Dessa forma, podemos migrar telas inteiras da aplicação, ao invés de pequenos componentes. Minha dica aqui, é ter o Storybook instalado no projeto, para construir e testar os pequenos componentes, assim fica mais fácil de construir uma base solida de componentes e depois integrá-los nas screens.

Screens: Também conhecidas como pages, elas são root components de cada rota. Digamos que você possui uma tela de Login, uma screen seria um wrapper dela toda.

Compartilhar dependências

Definir uma screen inteira é sensacional. Porém, quando chegamos a esse ponto, normalmente nós precisamos compartilhar algumas dependências do Angular com o React.

No nosso caso, as dependências que precisávamos, só estavam prontas após a inicialização do Angular, depois de ele ter executado os seus providers e etc. Nesse caso, não era possível exportá-las utilizando o export. Para resolver esse problema, nós criamos um objeto, com uma função auxiliar para injetar as dependências.

Para implementar essa solução, basta criamos um arquivo chamado ngDeps.js, com o seguinte código:

export const ngDeps = {};

export function injectNgDeps(deps) {
  Object.assign(ngDeps, deps);
};

export default ngDeps;

Nós utilizamos o injectNgDeps dentro de um processo de run do Angular, como no exemplo abaixo:

import { injectNgDeps } from 'path/to/ngDeps';

angular
  .module('app', [])
  .run([
    '$rootScope',
    '$state',
    ($rootScope, $state) => {
      injectNgDeps({ $rootScope, $state });
    },
  ]);

Nós fazemos isso dentro do run, porque o run é um dos primeiros processos a ser executados na inicialização do Angular. Desse modo, conseguimos acesso as dependências o mais breve possível. O injectNgDeps aceita um objeto como parâmetro e faz um merge dele no objeto ngDeps.

Quando você precisar de alguma dependência dentro de um componente React, basta seguir o exemplo abaixo:

import React, { Component } from 'react';
import ngDeps from 'path/to/ngDeps';

class Login extends Component {
  constructor(props) {
    super(props);
    
    const { $state, $rootScope } = ngDeps;
    
    this.$state = $state;
    this.$rootScope = $rootScope;
  }
  
  render() {
    return <div />
  }
}

Obverse que primeiro nós importamos ngDeps. Se você tentar acessar ngDeps.$state logo após o import, o resultado será undefined, pois o processo de run do Angular ainda não ocorreu. Por essa razão, nós acessamos o valor dentro do método contructor do componente, porque nesse momento o Angular já terminou sua inicialização.

Nós pegamos as dependências e atribuímos ao objeto this, porque dessa maneira, nós podemos acessar this.$state dentro de qualquer método da classe.

Assim, é possível compartilhar praticamente qualquer dependência do Angular com componentes React. Porém, use ngDeps com parcimônia. Mantenha a seguinte pergunta em mente: Consigo exportar essa dependência utilizando export? Se a reposta for sim, sempre opte por utilizar export, caso contrário, recorra ao ngDeps.

Outra coisa a ressaltar, é que é interessante manter o acesso ao ngDeps, restrito aos componentes mais acima da árvore, ou seja, nas screens e possivelmente em alguns containers, e então utilizar props para passar para os componentes filhos. Dessa maneira, ficará mais fácil se livrar da ngDeps quando você não precisar mais dela.

Integrar o Redux na aplicação

Após resolver o problema de compartilhar dependências entre os dois lados, nós podemos seguir para fazer a integração do Redux à aplicação. A integração é bem simples, porém, tem suas peculiaridades.

Configure a store seguindo os passos da documentação, como você faria em qualquer aplicação. Porém, uma vez que você criou o objeto store, exporte ele da seguinte maneira:

export const store = createStore(rootReducer);

Isso vai permitir que tenhamos acesso ao objeto store em outros pontos da aplicação.

Em uma aplicação normal, nós integramos nossos containers à store, utilizando o método connect, da lib react-redux. Porém, o método connect só funciona porque nós inserimos um Provider com a store, como um dos componentes raiz da nossa aplicação, como podemos ver na própria documentação da lib:

ReactDOM.render(
  <Provider store={store}>
    <MyAppRootComponent />
  </Provider>,
  rootEl
)

O problema é que nós não teremos um componente raiz da nossa aplicação, nós teremos vários. É inviável nós ficarmos controlando de forma manual , quais componentes deve conter o Provider e quais não. Para isso, nós criamos um High Order Componet, que abstrai essa verificação e insere o Provider como wrapper quando necessário. Para facilitar o acesso, eu publiquei esse HOC no Github e no NPM como redux-connect-standalone.

Para instalá-lo utilizando o npm:

npm i --save redux-connect-standalone

E então, nós podemos criar o nosso arquivo connect e utilizar o seguinte código:

import createConnect from 'redux-connect-standalone';
import store 'path/to/youStore';

export const connect = createConnect(store);

E então, dentro do seus componentes, ao invés de importar o método connect da lib react-redux, você importa do arquivo que você acabou de criar. E o utiliza exatamente da mesma maneira que utilizaria a função connect da lib original:

import { connect } from 'path/to/youConnect';

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(YourContainer);

Como nós estamos respeitando a mesma assinatura do método original, no dia que você possuir um Provider no componente raiz da sua aplicação, você só precisará executar um search replace no import do método e substituí-lo por:

import { connect } from 'react-redux';

Se você utiliza ou pretende utilizar redux-form na sua aplicação, eu também criei e publiquei um HOC para o método reduxForm, o redux-form-connect-standalone. A utilização dele é bem similar a do HOC que vimos acima.

Conclusão

Tendo essas receitas de bolo em mão, é possível migrar a sua aplicação gradualmente. Claro que ao longo do processo, outras dificuldades irão surgir.

É importante manter em mente, que essas são soluções intermediárias, entre ter uma aplicação totalmente em Angular e totalmente em React. O objetivo final, é se livrar de todas essas soluções acima e manter a aplicação utilizando as convenções e boas práticas de React e Redux. Então, sempre que for criar uma solução, pense qual será a dificuldade de se livrar dela depois.

Se você achar alguma dificuldade ou solução interessante, compartilhe com a gente.

Gostou do artigo e achou útil? Ajude a divulgar para que mais pessoas tenham acesso.