Navegando no ecossistema React.js
Publicados: 2022-03-11A velocidade da inovação no JavaScript Land é tão alta, que algumas pessoas até acham que é contraproducente. Uma biblioteca pode passar de um brinquedo de adoção inicial, de última geração, à obsolescência no decorrer de alguns meses. Ser capaz de identificar uma ferramenta que permanecerá relevante por pelo menos mais um ano está se tornando uma arte em si.
Quando o React.js foi lançado dois anos atrás, eu estava apenas aprendendo Angular, e rapidamente descartei o React como uma biblioteca obscura de templates. Durante esses dois anos, o Angular realmente ganhou espaço entre os desenvolvedores de JavaScript e quase se tornou sinônimo de desenvolvimento moderno de JS. Eu até comecei a ver o Angular em ambientes corporativos muito conservadores, e considerei seu futuro brilhante como garantido.
Mas de repente, uma coisa estranha aconteceu. Parece que o Angular se tornou vítima do efeito Osborne, ou “morte por pré-anúncio”. A equipe anunciou que, primeiro, o Angular 2 será completamente diferente, sem um caminho de migração claro do Angular 1 e, segundo, que o Angular 2 não estará disponível por mais um ano. O que isso diz a alguém que deseja iniciar um novo projeto na web? Você quer escrever seu novo projeto em um framework que se tornará obsoleto por uma nova versão?
Essa ansiedade entre os desenvolvedores jogou nas mãos do React, que buscava se estabelecer na comunidade. Mas o React sempre se comercializou como o “V” em “MVC”. Isso causou certa frustração entre os desenvolvedores web, que estão acostumados a trabalhar com frameworks completos. Como preencher as peças que faltam? Devo escrever o meu? Devo usar apenas uma biblioteca existente? Se sim, qual?
Com certeza, o Facebook (criadores do React.js) tinha mais um ás na manga: o fluxo de trabalho Flux, que prometia preencher as funções “M” e “C” ausentes. Para tornar as coisas ainda mais interessantes, o Facebook afirmou que o Flux é um “padrão”, não uma estrutura, e sua implementação do Flux é apenas um exemplo do padrão. Fiel à sua palavra, sua implementação foi realmente simplista e envolveu escrever muito clichê repetitivo e detalhado para fazer as coisas acontecerem.
A comunidade de código aberto veio em socorro e, um ano depois, temos dezenas de bibliotecas Flux e até alguns meta-projetos com o objetivo de compará-las. Isto é uma coisa boa; em vez de lançar uma estrutura corporativa pronta, o Facebook conseguiu despertar o interesse da comunidade e encorajou as pessoas a apresentarem suas próprias soluções.
Há um efeito colateral interessante nessa abordagem: quando você precisa combinar muitas bibliotecas diferentes para obter seu framework completo, você está efetivamente escapando do aprisionamento do fornecedor, e as inovações que surgem durante a construção do seu próprio framework podem ser prontamente reutilizadas em outro lugar.
É por isso que as novidades em torno do React são tão interessantes; a maior parte pode ser prontamente reutilizada em outros ambientes JavaScript. Mesmo que você não planeje usar o React, dar uma olhada em seu ecossistema é inspirador. Você pode querer simplificar seu sistema de compilação usando o poderoso, mas comparativamente fácil de configurar, pacote de módulos Webpack, ou começar a escrever ECMAScript 6 e até ECMAScript 7 hoje com o compilador Babel.
Neste artigo, analisarei alguns dos recursos e bibliotecas interessantes que estão disponíveis. Então, vamos explorar o ecossistema React!
Sistema de construção
O sistema de compilação é, sem dúvida, a primeira coisa com a qual você deve se preocupar ao criar um novo aplicativo da web. O sistema de compilação não é apenas uma ferramenta para executar scripts, mas no mundo JavaScript, geralmente molda a estrutura geral de seu aplicativo. As tarefas mais importantes que um sistema de compilação deve cobrir são as seguintes:
- Gerenciando dependências externas e internas
- Executando compiladores e pré-processadores
- Otimizando ativos para produção
- Executando o servidor web de desenvolvimento, o observador de arquivos e o recarregador de navegador
Nos últimos anos, o fluxo de trabalho do Yeoman com Bower e Grunt foi apresentado como a santíssima trindade do desenvolvimento de frontend moderno, resolvendo os problemas de geração de clichês, gerenciamento de pacotes e execução de tarefas comuns, respectivamente, com os mais progressistas mudando de Grunt para Gulp recentemente.
No ambiente React, você pode esquecê-los com segurança. Não que você não possa usá-los, mas as chances são de que você pode simplesmente usar o Webpack e o bom e velho NPM. Como isso é possível? O Webpack é um empacotador de módulos, que implementa a sintaxe do módulo CommonJS, comum no mundo Node.js, também no navegador. Na verdade, torna as coisas mais simples, já que você não precisa aprender outro gerenciador de pacotes para front-end; você apenas usa o NPM e compartilha dependências entre o servidor e o front-end. Você também não precisa lidar com o problema de carregar os arquivos JS na ordem correta porque é inferido a partir de importações de dependência especificadas em cada arquivo, e todo o enxame é concatenado corretamente em um script carregável.
Para tornar as coisas ainda mais atraentes, o Webpack, ao contrário de seu primo mais antigo Browserify, também pode lidar com outros tipos de ativos. Por exemplo, com carregadores, você pode transformar qualquer arquivo de ativo em uma função JavaScript que inline ou carregue o arquivo referenciado. Portanto, esqueça o pré-processamento manual e a referência de ativos do HTML. Basta require
seus arquivos CSS/SASS/LESS do JavaScript, e o Webpack cuida do resto com um simples arquivo de configuração. O Webpack também inclui um servidor web de desenvolvimento e um observador de arquivos. Além disso, você pode usar a chave "scripts"
em package.json
para definir one-liners 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" } ... }
E isso é tudo que você precisa para substituir Gulp e Bower. Claro, você ainda pode usar o Yeoman para gerar o clichê do aplicativo. Não desanime quando não houver gerador Yeoman para as coisas que você deseja (as bibliotecas mais avançadas geralmente não têm um). Você ainda pode clonar alguns clichês do GitHub e hackear.
ECMAScript de amanhã, hoje
O ritmo de desenvolvimento da linguagem JavaScript aumentou substancialmente nos últimos anos e, após um período de remoção de peculiaridades e estabilização da linguagem, agora vemos novos recursos poderosos chegando. O rascunho da especificação ECMAScript 6 (ES6) foi finalizado e, embora ainda não foi oficializado, já está encontrando ampla adoção. O trabalho no ECMAScript 7 (ES7) está em andamento, mas muitos de seus recursos já estão sendo adotados pelas bibliotecas mais avançadas.
Como isso é possível? Talvez você pense que não pode tirar proveito desses novos recursos JavaScript brilhantes até que eles sejam suportados no Internet Explorer, mas pense novamente. Os transpiladores ES já se tornaram tão onipresentes que podemos até fazer sem o suporte adequado do navegador. O melhor transpilador ES disponível no momento é o Babel: ele pegará seu código ES6+ mais recente e o transformará em vanilla ES5, então, você pode usar qualquer novo recurso ES assim que for inventado (e implementado no Babel, o que geralmente acontece bastante rapidamente).
Os recursos JavaScript mais recentes são úteis em todos os frameworks front-end, e o React foi atualizado recentemente para funcionar bem com as especificações ES6 e ES7. Esses novos recursos devem eliminar muitas dores de cabeça ao desenvolver com React. Vamos dar uma olhada em algumas das adições mais úteis e como elas podem beneficiar um projeto React. Mais tarde, veremos como usar algumas ferramentas e bibliotecas úteis com o React enquanto aproveitamos essa sintaxe aprimorada.
aulas ES6
A programação orientada a objetos é um paradigma poderoso e amplamente adotado, mas a abordagem do JavaScript é um pouco exótica. A maioria dos frameworks front-end, seja Backbone, Ember, Angular ou React, adotaram suas próprias formas proprietárias de definir classes e criar objetos. Mas com o ES6, agora temos classes tradicionais em JavaScript, e simplesmente faz sentido usá-las em vez de escrever nossa própria implementação. Assim, em vez de:
React.createClass({ displayName: 'HelloMessage', render() { return <div>Hello {this.props.name}</div>; } })
nós podemos escrever:
class HelloMessage extends React.Component { render() { return <div>Hello {this.props.name}</div>; } }
Para um exemplo mais elaborado, considere este código, usando a sintaxe antiga:
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> ); } });
E compare com a versão 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> ); } }
Aqui, os métodos de ciclo de vida do React getDefaultProps
e getInitialState
não são mais necessários. getDefaultProps
se torna a variável de classe estática defaultProps
, e o estado inicial é definido apenas no construtor. A única desvantagem é que os métodos não são mais vinculados automaticamente, portanto, você precisa usar o bind
ao chamar manipuladores do JSX.
Decoradores
Decoradores são um recurso útil do ES7. Eles permitem que você aumente o comportamento de uma função ou classe envolvendo-a dentro de outra função. Por exemplo, vamos supor que você deseja ter o mesmo manipulador de alterações em alguns de seus componentes, mas não deseja se comprometer com o antipadrão de herança. Você pode usar um decorador de classe em vez disso. Vamos definir o decorador da seguinte forma:
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; }
O importante aqui é que a função addChangeHandler
adiciona a função changeHandler
ao protótipo da classe de destino.
Para aplicar o decorador, poderíamos escrever:
MyClass = changeHandler(MyClass)
ou mais elegantemente, com a sintaxe ES7:
@addChangeHandler class MyClass { ... }
Quanto ao conteúdo da função changeHandler
em si, com a ausência de vinculação de dados bidirecional do React, trabalhar com entradas no React pode ser tedioso. A função changeHandler
tenta facilitar. O primeiro parâmetro especifica uma key
no objeto de estado, que servirá como objeto de dados para a entrada. O segundo parâmetro é o atributo, no qual será salvo o valor do campo de entrada. Esses dois parâmetros são definidos do JSX usando a palavra-chave 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')} /> ) } }
Quando o usuário altera o campo username, seu valor é salvo em this.state.login.username
, sem a necessidade de definir mais manipuladores personalizados.
Funções de seta
A dinâmica this
contexto do JavaScript tem sido uma dor constante para os desenvolvedores porque, de forma pouco intuitiva, o contexto this
de uma função aninhada é redefinido para global, mesmo dentro de uma classe. Para corrigir isso, geralmente é necessário salvá this
em alguma variável de escopo externo (geralmente _this
) e usá-lo em funções internas:
class DirectorsStore { onFetch(directors) { var _this = this; this.directorsHash = {}; directors.forEach(function(x){ _this.directorsHash[x._id] = x; }) } }
Com a nova sintaxe ES6, a function(x){
pode ser reescrita como (x) => {
. Essa definição de método “seta” não apenas vincula this
corretamente ao escopo externo, mas também é consideravelmente mais curta, o que definitivamente conta ao escrever muito código assíncrono.
onFetch(directors) { this.directorsHash = {}; directors.forEach((x) => { this.directorsHash[x._id] = x; }) }
Desestruturando Atribuições
As atribuições de desestruturação, introduzidas no ES6, permitem que você tenha um objeto composto no lado esquerdo de uma atribuição:
var o = {p: 42, q: true}; var {p, q} = o; console.log(p); // 42 console.log(q); // true
Isso é legal, mas como isso realmente nos ajuda no React? Considere o seguinte exemplo:
function makeRequest(url, method, params) { var config = { url: url, method: method, params: params }; ... }
Com a desestruturação, você pode salvar algumas teclas. O literal de chaves {url, method, params}
recebe valores automaticamente do escopo com os mesmos nomes das chaves. Esse idioma é usado com bastante frequência e eliminar a repetição torna o código menos propenso a erros.
function makeRequest(url, method, params) { var config = {url, method, params}; ... }
A desestruturação também pode ajudá-lo a carregar apenas um subconjunto de um módulo:
const {clone, assign} = require('lodash'); function output(data, optional) { var payload = clone(data); assign(payload, optional); }
Argumentos: Default, Rest e Spread
Os argumentos de função são mais poderosos no ES6. Finalmente, você pode definir o argumento padrão :
function http(endpoint, method='GET') { console.log(method) ... } http('/api') // GET
Cansado de mexer com o objeto de arguments
pesado? Com a nova especificação, você pode obter o restante dos argumentos como um array:
function networkAction(context, method, ...rest) { // rest is an array return method.apply(context, rest); }
E se você não gosta de chamar apply()
, você pode simplesmente espalhar um array em argumentos de função:
myArguments = ['foo', 'bar', 123]; myFunction(...myArguments);
Geradores e funções assíncronas
O ES6 introduziu geradores de JavaScript. Um gerador é basicamente uma função JavaScript cuja execução pode ser pausada e retomada posteriormente, lembrando seu estado. Cada vez que a palavra-chave yield
é encontrada, a execução é pausada e o argumento yield
é passado de volta para o objeto de chamada:
function* sequence(from, to) { console.log('Ready!'); while(from <= to) { yield from++; } }
Aqui está um exemplo deste gerador em ação:
> 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 }
Quando chamamos a função geradora, ela executa até o primeiro yield
e depois para. Depois que chamamos next()
, ele retorna o primeiro valor e retoma a execução. Cada yield
retorna outro valor, mas após a terceira chamada, a função geradora termina e cada chamada subsequente para next()
retornará { value: undefined, done: true }
.
É claro que o objetivo dos geradores não é criar sequências numéricas elaboradas. A parte interessante é a capacidade de interromper e retomar a execução da função, que pode ser usada para controlar o fluxo de programa assíncrono e, finalmente, se livrar dessas funções de retorno de chamada irritantes.
Para demonstrar essa ideia, primeiro precisamos de uma função assíncrona. Normalmente, teríamos alguma operação de E/S, mas para simplificar, vamos apenas usar setTimeout
e retornar uma promessa. (Observe que o ES6 também introduziu promessas nativas para JavaScript.)
function asyncDouble(x) { var deferred = Promise.defer(); setTimeout(function(){ deferred.resolve(x*2); }, 1000); return deferred.promise; }
Em seguida, precisamos de uma função 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(); }
Essa função recebe qualquer gerador como argumento e continua chamando next()
nele enquanto houver valores para yield
. Nesse caso, os valores gerados são promessas e, portanto, é necessário esperar que as promessas sejam resolvidas e usar a recursão com loop()
para obter o loop nas funções aninhadas.
O valor de retorno é resolvido no manipulador then()
e passado para value
, que é definido no escopo externo e que será passado para a chamada next(value)
. Essa chamada torna o valor um resultado da expressão yield correspondente. Isso significa que agora podemos escrever de forma assíncrona sem nenhum retorno de chamada:
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);
O gerador myGenerator
será pausado em cada yield
, esperando que o consumidor entregue a promessa resolvida. E, de fato, veremos os números calculados aparecerem no console em intervalos de um segundo.
Double 1 = 2 Double 2 = 4 Double 3 = 6
Isso demonstra o conceito básico, porém, não recomendo que você use esse código em produção. Em vez disso, escolha uma biblioteca bem testada, como co. Isso permitirá que você escreva facilmente código assíncrono com rendimentos, incluindo tratamento de erros:
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); });
Portanto, este exemplo mostra como escrever código assíncrono sem retornos de chamada usando geradores ES6. O ES7 leva essa abordagem um passo adiante, introduzindo as palavras-chave async
e await
e eliminando completamente a necessidade de uma biblioteca geradora. Com esse recurso, o exemplo anterior ficaria assim:
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); } };
Na minha opinião, isso elimina a dor de trabalhar com código assíncrono em JavaScript. Não apenas no React, mas em todos os outros lugares também.
Os geradores não são apenas mais concisos e diretos, mas também nos permitem usar técnicas que seriam muito difíceis de implementar com retornos de chamada. Um exemplo proeminente de qualidade do gerador é a biblioteca de middleware koa para Node.js. Ele visa substituir o Express e, para isso, vem com um recurso matador: a cadeia de middleware flui não apenas downstream (com solicitação do cliente), mas também upstream , permitindo modificações adicionais na resposta do servidor. Considere o seguinte exemplo 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);

O middleware de resposta gera s downstream no manipulador de resposta, que define o corpo da resposta, e no fluxo upstream (após a expressão yield
) yield
é permitida a modificação adicional de this.body
, bem como outras funções, como registro de tempo, que é possível porque o upstream e o downstream compartilham o mesmo contexto de função. Isso é muito mais poderoso que o Express, no qual uma tentativa de realizar a mesma coisa terminaria assim:
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);
Você provavelmente já consegue identificar o que está errado aqui; usar uma variável start
“global” resultará em uma condição de corrida, retornando sem sentido com solicitações simultâneas. A solução é uma solução alternativa não óbvia e você pode esquecer de modificar a resposta no fluxo upstream.
Além disso, ao usar o koa, você obterá o fluxo de trabalho assíncrono do gerador gratuitamente:
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);
Você pode imaginar as promessas e retornos de chamada envolvidos na recriação deste pequeno exemplo no Express.
Como toda essa conversa do Node.js se relaciona com o React? Bem, o Node é a primeira escolha ao considerar um back-end adequado para o React. Como o Node também é escrito em JavaScript, ele suporta o compartilhamento de código entre back-end e front-end, permitindo-nos construir aplicativos web isomórficos React. Mas, mais sobre isso mais tarde.
Biblioteca de fluxo
O React é ótimo na criação de componentes de visualização compostos, mas precisamos de alguma maneira de gerenciar dados e estados em todo o aplicativo. Foi quase universalmente aceito que o React é melhor complementado pela arquitetura do aplicativo Flux. Se você é completamente novo no Flux, recomendo uma atualização rápida.
O que não foi tão universalmente aceito é qual das muitas implementações do Flux escolher. O Facebook Flux seria a escolha óbvia, mas para a maioria das pessoas é muito detalhado. As implementações alternativas se concentram principalmente na redução da quantidade de clichê necessária com uma abordagem de convenção sobre configuração e também com algumas funções de conveniência para componentes de ordem superior, renderização do lado do servidor e assim por diante. Alguns dos principais concorrentes, com várias métricas de popularidade, podem ser vistos aqui. Pesquisei Alt, Reflux, Flummox, Fluxxor e Marty.js.
Minha maneira de escolher a biblioteca certa não é objetiva, mas pode ajudar de qualquer maneira. O Fluxxor foi uma das primeiras bibliotecas que verifiquei, mas agora parece um pouco obsoleta. Marty.js é interessante e tem muitos recursos, mas ainda envolve muito clichê, e algumas das funções parecem supérfluas. Reflux parece ótimo e tem alguma tração, mas parece um pouco difícil para iniciantes e também carece de documentação adequada. Flummox e Alt são muito semelhantes, mas Alt parece ter ainda menos clichê, desenvolvimento muito ativo, documentação atualizada e uma comunidade Slack útil. Portanto, escolhi Alt.
AltFlux
Com Alt, o fluxo de trabalho do Flux se torna muito mais simples sem perder seu poder. A documentação do Flux do Facebook diz muito sobre o dispatcher, mas somos livres para ignorar isso porque, em Alt, o dispatcher está implicitamente conectado a ações por convenção e geralmente não requer nenhum código personalizado. Isso nos deixa apenas com armazenamentos , ações e componentes . Essas três camadas podem ser usadas de forma a mapear bem no modelo de pensamento MVC : Stores são Models , actions são Controllers e components são Views . A principal diferença é o fluxo de dados unidirecional central para o padrão Flux, o que significa que os controladores (ações) não podem modificar diretamente as visualizações (componentes), mas, em vez disso, podem apenas acionar modificações no modelo (armazenamento), às quais as visualizações são vinculadas passivamente. Esta já era uma prática recomendada para alguns desenvolvedores Angular esclarecidos.
O fluxo de trabalho é o seguinte:
- Componentes iniciam ações.
- As lojas ouvem as ações e atualizam os dados.
- Os componentes são vinculados a armazenamentos e são renderizados novamente quando os dados são atualizados.
Ações
Ao usar a biblioteca Alt Flux, as ações geralmente vêm em dois tipos: automáticas e manuais. As ações automáticas são criadas usando a função generateActions
e vão diretamente para o dispatcher. Os métodos manuais são definidos como métodos de suas classes de ação e podem ir para o dispatcher com uma carga adicional. O caso de uso mais comum de ações automáticas é notificar as lojas sobre algum evento no aplicativo. As ações manuais são, entre outras coisas, a maneira preferida de lidar com as interações do servidor.
Portanto, as chamadas da API REST pertencem a ações. O fluxo de trabalho completo é o seguinte:
- O componente aciona uma ação.
- O criador da ação executa uma solicitação de servidor assíncrona e o resultado vai para o dispatcher como uma carga útil.
- A loja escuta a ação, o manipulador de ação correspondente recebe o resultado como um argumento e a loja atualiza seu estado de acordo.
Para solicitações AJAX, podemos usar a biblioteca axios, que, entre outras coisas, lida com dados e cabeçalhos JSON perfeitamente. Em vez de promessas ou retornos de chamada, podemos usar o padrão ES7 async
/ await
. Se o status da resposta POST
não for 2XX, um erro será gerado e enviaremos os dados retornados ou o erro recebido.
Vejamos uma página de login para um exemplo simples do fluxo de trabalho Alt. A ação de logout não precisa fazer nada de especial, apenas notificar a loja, para que possamos gerá-la automaticamente. A ação de login é manual e espera dados de login como parâmetro para o criador da ação. Após recebermos uma resposta do servidor, despachamos os dados de sucesso ou, se um erro for lançado, despachamos o erro recebido.
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));
Lojas
O armazenamento Flux serve a dois propósitos: possui manipuladores de ação e carrega o estado. Vamos continuar nosso exemplo de página de login para ver como isso funciona.
Vamos criar LoginStore
, com dois atributos de estado: user
, para o usuário conectado no momento, e error
, para o erro relacionado ao login atual. No espírito de reduzir o clichê, Alt nos permite vincular a todas as ações de uma classe com uma única função bindActions
.
class LoginStore { constructor() { this.bindActions(LoginActions); this.user = null; this.error = null; } ...
Os nomes dos manipuladores são definidos por convenção, precedendo o nome on
ação correspondente. Portanto, a ação de login
é tratada por onLogin
e assim por diante. Observe que a primeira letra do nome da ação será maiúscula no estilo camelCase. Em nosso LoginStore
, temos os seguintes handlers, chamados pelas ações correspondentes:
... 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
A maneira usual de vincular lojas a componentes é usar algum tipo de mixin React. Mas como os mixins estão saindo de moda, precisa haver outra maneira. Uma das novas abordagens é usar componentes de ordem superior. Pegamos nosso componente e o colocamos dentro de um componente wrapper, que cuidará de ouvir as lojas e chamar o re-render. Nosso componente receberá o estado da loja em props
. Essa abordagem também é útil para organizar nosso código em componentes inteligentes e burros, que estão na moda ultimamente. Para Alt, o wrapper de componente é implementado por AltContainer
:
export default class Login extends React.Component { render() { return ( <AltContainer stores={{LoginStore: LoginStore}}> <LoginPage/> </AltContainer> )} }
Nosso componente LoginPage
também usa o decorador changeHandler
apresentado anteriormente. Os dados do LoginStore
são usados para exibir erros no caso de um login malsucedido, e a nova renderização é feita pelo AltContainer
. Clicar no botão de login executa a ação de login
, concluindo o fluxo de trabalho 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> )} }
Renderização isomórfica
Os aplicativos da Web isomórficos são um tema quente hoje em dia porque resolvem algumas das maiores tarefas dos aplicativos tradicionais de página única. Nesses aplicativos, a marcação é criada dinamicamente pelo JavaScript no navegador. O resultado é que o conteúdo não está disponível para clientes com JavaScript desativado, principalmente, rastreadores da web de mecanismos de pesquisa. Isso significa que sua página da web não está indexada e não aparece nos resultados da pesquisa. Existem maneiras de contornar isso, mas elas estão longe de serem ideais. A abordagem isomórfica tenta corrigir esse problema pré-renderizando a URL solicitada de um aplicativo de página única no servidor. Com o Node.js, você tem JavaScript no servidor, o que significa que o React também pode ser executado no lado do servidor. Isso não deve ser muito difícil, certo?
Um obstáculo é que algumas bibliotecas Flux, especialmente aquelas que usam singletons, têm dificuldades com a renderização do lado do servidor. Quando você tem armazenamentos Flux singleton e várias solicitações simultâneas ao seu servidor, os dados serão misturados. Algumas bibliotecas resolvem isso usando instâncias do Flux, mas isso vem com outras desvantagens, principalmente a necessidade de passar essas instâncias em seu código. O Alt também oferece instâncias do Flux, mas também resolveu o problema de renderização do lado do servidor com singletons; ele libera os armazenamentos após cada solicitação, para que cada solicitação simultânea comece com uma ardósia limpa.
O núcleo da funcionalidade de renderização do lado do servidor é fornecido por React.renderToString
. Todo o aplicativo front-end do React também é executado no servidor. Dessa forma, não precisamos esperar que o JavaScript do lado do cliente crie a marcação; ele é pré-construído no servidor para o URL acessado e enviado ao navegador como HTML. Quando o JavaScript do cliente é executado, ele retoma de onde o servidor parou. Para suportar isso, podemos usar a biblioteca Iso, que deve ser usada com Alt.
Primeiro, inicializamos o Flux no servidor usando alt.bootstrap
. É possível pré-preencher os armazenamentos Flux com dados para renderização. Também é necessário decidir qual componente renderizar para qual URL, que é a funcionalidade do Router
do lado do cliente. Estamos usando a versão singleton de alt
, portanto, após cada renderização, precisamos alt.flush()
nas lojas para limpá-las para outra solicitação. Usando o complemento iso
, o estado do Flux é serializado para a marcação HTML, para que o cliente saiba onde pegar:
// 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()}); });
No lado do cliente, pegamos o estado do servidor e iniciamos o alt
com os dados. Em seguida, executamos Router
e React.render
no contêiner de destino, que atualizará a marcação gerada pelo servidor conforme necessário.
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 úteis
Um guia para o ecossistema React não estaria completo sem mencionar algumas bibliotecas front-end que funcionam especialmente bem com o React. Essas bibliotecas lidam com as tarefas mais comuns encontradas em quase todos os aplicativos da Web: layouts e contêineres CSS, formulários e botões estilizados, validações, seleção de datas e assim por diante. Não adianta reinventar a roda quando esses problemas já foram resolvidos.
React-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.
Obrigado por ler!