Angular vs. React: Qual é o melhor para o desenvolvimento web?

Publicados: 2022-03-11

Existem inúmeros artigos por aí debatendo se React ou Angular é a melhor escolha para desenvolvimento web. Precisamos de mais um?

A razão pela qual escrevi este artigo é porque nenhum dos artigos já publicados - embora contenham grandes insights - é suficientemente aprofundado para que um desenvolvedor de front-end prático decida qual deles pode atender às suas necessidades.

Neste artigo, você aprenderá como o Angular e o React visam resolver problemas de front-end semelhantes, embora com filosofias muito diferentes, e se escolher um ou outro é apenas uma questão de preferência pessoal. Para compará-los, vamos construir o mesmo aplicativo duas vezes, uma vez com Angular e depois novamente com React.

Anúncio prematuro de Angular

Dois anos atrás, escrevi um artigo sobre o Ecossistema React. Entre outros pontos, o artigo argumentava que Angular havia se tornado vítima de “morte por pré-anúncio”. Naquela época, a escolha entre Angular e quase qualquer outra coisa era fácil para quem não queria que seu projeto fosse executado em um framework obsoleto. O Angular 1 estava obsoleto e o Angular 2 nem estava disponível na versão alfa.

Em retrospectiva, os medos eram mais ou menos justificados. Angular 2 mudou drasticamente e até passou por uma grande reescrita pouco antes do lançamento final.

Dois anos depois, temos o Angular 4 com a promessa de relativa estabilidade daqui para frente.

O que agora?

Angular vs. Reagir: comparando maçãs e laranjas

Algumas pessoas dizem que comparar React e Angular é como comparar maçãs com laranjas. Enquanto uma é uma biblioteca que lida com visualizações, a outra é uma estrutura completa.

Claro, a maioria dos desenvolvedores do React adicionará algumas bibliotecas ao React para transformá-lo em um framework completo. Por outro lado, o fluxo de trabalho resultante dessa pilha geralmente ainda é muito diferente do Angular, portanto, a comparabilidade ainda é limitada.

A maior diferença está na gestão do estado. O Angular vem com a vinculação de dados incluída, enquanto o React hoje geralmente é aprimorado pelo Redux para fornecer fluxo de dados unidirecional e trabalhar com dados imutáveis. Essas são abordagens opostas por si só, e inúmeras discussões estão acontecendo agora se a vinculação de dados/mutável é melhor ou pior do que imutável/unidirecional.

Um campo de jogo nivelado

Como o React é notoriamente mais fácil de hackear, decidi, para o propósito desta comparação, construir uma configuração do React que espelhe o Angular razoavelmente de perto para permitir a comparação lado a lado de trechos de código.

Certos recursos do Angular que se destacam, mas não estão no React por padrão, são:

Funcionalidade Pacote angular Biblioteca do React
Vinculação de dados, injeção de dependência (DI) @angular/core MobX
Propriedades computadas rxjs MobX
Roteamento baseado em componentes @angular/roteador Reagir Roteador v4
Componentes de design de materiais @angular/material Caixa de ferramentas de reação
CSS com escopo para componentes @angular/core Módulos CSS
Validações de formulário @angular/forms Estado do formulário
Gerador de projetos @angular/cli Reagir Scripts TS

Ligação de dados

A vinculação de dados é sem dúvida mais fácil de começar do que a abordagem unidirecional. Claro, seria possível ir na direção completamente oposta e usar Redux ou mobx-state-tree com React e ngrx com Angular. Mas isso seria assunto para outro post.

Propriedades computadas

Enquanto o desempenho está em causa, os getters simples em Angular estão simplesmente fora de questão à medida que são chamados em cada renderização. É possível usar BehaviorSubject do RsJS, que faz o trabalho.

Com o React, é possível usar @computed do MobX, que atinge o mesmo objetivo, com uma API indiscutivelmente um pouco melhor.

Injeção de dependência

A injeção de dependência é meio controversa porque vai contra o atual paradigma React de programação funcional e imutabilidade. Acontece que algum tipo de injeção de dependência é quase indispensável em ambientes de vinculação de dados, pois ajuda no desacoplamento (e, portanto, na simulação e nos testes) onde não há arquitetura de camada de dados separada.

Mais uma vantagem do DI (suportado em Angular) é a capacidade de ter diferentes ciclos de vida de diferentes lojas. A maioria dos paradigmas atuais do React usa algum tipo de estado global do aplicativo que mapeia para diferentes componentes, mas pela minha experiência, é muito fácil introduzir bugs ao limpar o estado global na desmontagem do componente.

Ter uma loja que é criada na montagem do componente (e estar perfeitamente disponível para os filhos desse componente) parece ser um conceito realmente útil e muitas vezes esquecido.

Pronto para uso em Angular, mas também facilmente reproduzível com MobX.

Roteamento

O roteamento baseado em componentes permite que os componentes gerenciem suas próprias sub-rotas em vez de ter uma grande configuração de roteador global. Essa abordagem finalmente chegou ao react-router na versão 4.

Design material

É sempre bom começar com alguns componentes de nível superior, e o material design tornou-se algo como uma escolha padrão universalmente aceita, mesmo em projetos que não são do Google.

Eu escolhi deliberadamente o React Toolbox em vez do Material UI normalmente recomendado, pois o Material UI tem sérios problemas de desempenho autoconfessados ​​com sua abordagem de CSS inline, que eles planejam resolver na próxima versão.

Além disso, PostCSS/cssnext usado no React Toolbox está começando a substituir Sass/LESS de qualquer maneira.

CSS com escopo

Classes CSS são algo como variáveis ​​globais. Existem várias abordagens para organizar CSS para evitar conflitos (incluindo BEM), mas há uma tendência clara no uso de bibliotecas que ajudam a processar CSS para evitar conflitos sem a necessidade de um desenvolvedor front-end para criar sistemas de nomenclatura CSS elaborados.

Validação de formulário

As validações de formulários são um recurso não trivial e muito usado. É bom tê-los cobertos por uma biblioteca para evitar repetição de código e bugs.

Gerador de Projetos

Ter um gerador de CLI para um projeto é um pouco mais conveniente do que ter que clonar clichês do GitHub.

Mesma aplicação, construída duas vezes

Então vamos criar o mesmo aplicativo em React e Angular. Nada espetacular, apenas um Shoutboard que permite a qualquer pessoa postar mensagens em uma página comum.

Você pode experimentar os aplicativos aqui:

  • Shoutboard Angular
  • Reação de placar

Aplicativo de placar

Se você quiser ter todo o código-fonte, pode obtê-lo no GitHub:

  • Fonte angular Shoutboard
  • Fonte do Shoutboard React

Você notará que também usamos o TypeScript para o aplicativo React. As vantagens da verificação de tipo no TypeScript são óbvias. E agora, como o melhor manuseio de importações, async/await e rest spread finalmente chegaram ao TypeScript 2, ele deixa o Babel/ES7/Flow comendo poeira.

Além disso, vamos adicionar o Apollo Client a ambos porque queremos usar o GraphQL. Quero dizer, REST é ótimo, mas depois de uma década ou mais, fica velho.

Bootstrap e roteamento

Primeiro, vamos dar uma olhada nos pontos de entrada de ambos os aplicativos.

Angular

 const appRoutes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'posts', component: PostsComponent }, { path: 'form', component: FormComponent }, { path: '', redirectTo: '/home', pathMatch: 'full' } ] @NgModule({ declarations: [ AppComponent, PostsComponent, HomeComponent, FormComponent, ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes), ApolloModule.forRoot(provideClient), FormsModule, ReactiveFormsModule, HttpModule, BrowserAnimationsModule, MdInputModule, MdSelectModule, MdButtonModule, MdCardModule, MdIconModule ], providers: [ AppService ], bootstrap: [AppComponent] })
 @Injectable() export class AppService { username = 'Mr. User' }

Basicamente, todos os componentes que queremos usar na aplicação precisam ir para as declarações. Todas as bibliotecas de terceiros para importações e todas as lojas globais para fornecedores. Os componentes filhos têm acesso a tudo isso, com a oportunidade de adicionar mais coisas locais.

Reagir

 const appStore = AppStore.getInstance() const routerStore = RouterStore.getInstance() const rootStores = { appStore, routerStore } ReactDOM.render( <Provider {...rootStores} > <Router history={routerStore.history} > <App> <Switch> <Route exact path='/home' component={Home as any} /> <Route exact path='/posts' component={Posts as any} /> <Route exact path='/form' component={Form as any} /> <Redirect from='/' to='/home' /> </Switch> </App> </Router> </Provider >, document.getElementById('root') )

O componente <Provider/> é usado para injeção de dependência no MobX. Ele salva as lojas no contexto para que os componentes do React possam injetá-las posteriormente. Sim, o contexto React pode (sem dúvida) ser usado com segurança.

A versão do React é um pouco mais curta porque não há declarações de módulo - geralmente, você apenas importa e está pronto para usar. Às vezes, esse tipo de dependência rígida é indesejada (teste), portanto, para lojas globais de singleton, tive que usar esse padrão GoF de décadas:

 export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }

O Router da Angular é injetável, portanto pode ser usado de qualquer lugar, não apenas componentes. Para conseguir o mesmo em react, usamos o pacote mobx-react-router e injetamos o routerStore .

Resumo: A inicialização de ambos os aplicativos é bastante direta. O React tem a vantagem de ser mais simples, usando apenas importações em vez de módulos, mas, como veremos mais adiante, esses módulos podem ser bastante úteis. Fazer singletons manualmente é um pouco incômodo. Quanto à sintaxe de declaração de roteamento, JSON vs. JSX é apenas uma questão de preferência.

Links e Navegação Imperativa

Portanto, há dois casos para alternar uma rota. Declarativo, usando elementos <a href...> , e imperativo, chamando a API de roteamento (e, portanto, de localização) diretamente.

Angular

 <h1> Shoutboard Application </h1> <nav> <a routerLink="/home" routerLinkActive="active">Home</a> <a routerLink="/posts" routerLinkActive="active">Posts</a> </nav> <router-outlet></router-outlet>

O Angular Router detecta automaticamente qual routerLink está ativo e coloca uma classe routerLinkActive apropriada nele, para que possa ser estilizado.

O roteador usa o elemento especial <router-outlet> para renderizar qualquer que seja o caminho atual. É possível ter muitos <router-outlet> s, à medida que nos aprofundamos nos subcomponentes do aplicativo.

 @Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }

O módulo roteador pode ser injetado em qualquer serviço (meio-magicamente por seu tipo TypeScript), a declaração private então o armazena na instância sem a necessidade de atribuição explícita. Use o método de navigate para alternar os URLs.

Reagir

 import * as style from './app.css' // … <h1>Shoutboard Application</h1> <div> <NavLink to='/home' activeClassName={style.active}>Home</NavLink> <NavLink to='/posts' activeClassName={style.active}>Posts</NavLink> </div> <div> {this.props.children} </div>

O React Router também pode definir a classe do link ativo com activeClassName .

Aqui, não podemos fornecer o nome da classe diretamente, porque ele foi tornado exclusivo pelo compilador de módulos CSS e precisamos usar o auxiliar de style . Mais sobre isso mais tarde.

Como visto acima, o React Router usa o elemento <Switch> dentro de um elemento <App> . Como o elemento <Switch> apenas envolve e monta a rota atual, isso significa que as sub-rotas do componente atual são apenas this.props.children . Então isso também é componível.

 export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } }

O mobx-router-store também permite fácil injeção e navegação.

Resumo: Ambas as abordagens de roteamento são bastante comparáveis. Angular parece ser mais intuitivo, enquanto o React Router tem uma composição um pouco mais direta.

Injeção de dependência

Já foi comprovado ser benéfico separar a camada de dados da camada de apresentação. O que estamos tentando alcançar com DI aqui é fazer com que os componentes das camadas de dados (aqui chamados de modelo/loja/serviço) sigam o ciclo de vida dos componentes visuais e, assim, permitir fazer uma ou várias instâncias de tais componentes sem a necessidade de tocar global Estado. Além disso, deve ser possível misturar e combinar dados e camadas de visualização compatíveis.

Os exemplos neste artigo são muito simples, então todo o material de DI pode parecer um exagero, mas é útil à medida que o aplicativo cresce.

Angular

 @Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } }

Portanto, qualquer classe pode ser @injectable e suas propriedades e métodos disponibilizados aos componentes.

 @Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } }

Ao registrar o HomeService nos providers do componente, o disponibilizamos exclusivamente para este componente. Não é um singleton agora, mas cada instância do componente receberá uma nova cópia, fresca na montagem do componente. Isso significa que não há dados obsoletos de uso anterior.

Por outro lado, o AppService foi registrado no app.module (veja acima), portanto, é um singleton e permanece o mesmo para todos os componentes, apesar da vida útil do aplicativo. Ser capaz de controlar o ciclo de vida dos serviços a partir de componentes é um conceito muito útil, mas pouco apreciado.

A DI funciona atribuindo as instâncias de serviço ao construtor do componente, identificado pelos tipos TypeScript. Além disso, as palavras-chave public atribuem automaticamente os parâmetros a this , para que não precisemos mais escrever aquelas linhas chatas this.homeService = homeService .

 <div> <h3>Dashboard</h3> <md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <br/> <span>Clicks since last visit: {{homeService.counter}}</span> <button (click)='homeService.increment()'>Click!</button> </div>

A sintaxe do modelo do Angular, sem dúvida bastante elegante. Eu gosto do atalho [()] , que funciona como uma ligação de dados de 2 vias, mas sob o capô, na verdade é uma ligação de atributo + evento. Conforme determina o ciclo de vida de nossos serviços, homeService.counter será redefinido toda vez que navegarmos para fora de /home , mas o appService.username permanecerá e poderá ser acessado de qualquer lugar.

Reagir

 import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } }

Com o MobX, precisamos adicionar o decorador @observable a qualquer propriedade que queremos tornar observável.

 @observer export class Home extends React.Component<any, any> { homeStore: HomeStore componentWillMount() { this.homeStore = new HomeStore() } render() { return <Provider homeStore={this.homeStore}> <HomeComponent /> </Provider> } }

Para gerenciar o ciclo de vida corretamente, precisamos trabalhar um pouco mais do que no exemplo Angular. Envolvemos o HomeComponent dentro de um Provider , que recebe uma nova instância de HomeStore em cada montagem.

 interface HomeComponentProps { appStore?: AppStore, homeStore?: HomeStore } @inject('appStore', 'homeStore') @observer export class HomeComponent extends React.Component<HomeComponentProps, any> { render() { const { homeStore, appStore } = this.props return <div> <h3>Dashboard</h3> <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>Clicks since last visit: {homeStore.counter}</span> <button onClick={homeStore.increment}>Click!</button> </div> } }

HomeComponent usa o decorador @observer para ouvir as alterações nas propriedades @observable .

O mecanismo oculto disso é bastante interessante, então vamos analisá-lo brevemente aqui. O decorador @observable substitui uma propriedade em um objeto por getter e setter, o que permite interceptar chamadas. Quando a função de renderização de um componente aumentado @observer é chamada, esses getters de propriedades são chamados e mantêm uma referência ao componente que os chamou.

Então, quando o setter é chamado e o valor é alterado, as funções de renderização dos componentes que usaram a propriedade na última renderização são chamadas. Agora, os dados sobre quais propriedades são usadas e onde são atualizados, e todo o ciclo pode recomeçar.

Um mecanismo muito simples e bastante eficiente também. Explicação mais detalhada aqui.

O decorador @inject é usado para injetar instâncias appStore e homeStore nas props do HomeComponent . Neste ponto, cada uma dessas lojas tem um ciclo de vida diferente. appStore é a mesma durante a vida útil do aplicativo, mas homeStore é recém-criada em cada navegação para a rota “/home”.

A vantagem disso é que não é necessário limpar as propriedades manualmente, como é o caso quando todas as lojas são globais, o que é um problema se a rota for alguma página de “detalhe” que contém dados completamente diferentes a cada vez.

Resumo: Como o gerenciamento do ciclo de vida do provedor é um recurso inerente ao DI do Angular, é claro que é mais simples realizá-lo por lá. A versão React também é utilizável, mas envolve muito mais clichê.

Propriedades computadas

Reagir

Vamos começar com o React neste, ele tem uma solução mais direta.

 import { observable, computed, action } from 'mobx' export class HomeStore { import { observable, computed, action } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } @computed get counterMessage() { console.log('recompute counterMessage!') return `${this.counter} ${this.counter === 1 ? 'click' : 'clicks'} since last visit` } }

Portanto, temos uma propriedade computada que se liga ao counter e retorna uma mensagem devidamente pluralizada. O resultado de counterMessage é armazenado em cache e recalculado somente quando o counter é alterado.

 <Input type='text' label='Edit your name' name='username' value={appStore.username} onChange={appStore.onUsernameChange} /> <span>{homeStore.counterMessage}</span> <button onClick={homeStore.increment}>Click!</button>

Em seguida, fazemos referência à propriedade (e método de increment ) do modelo JSX. O campo de entrada é acionado pela associação a um valor e permitindo que um método da appStore manipule o evento do usuário.

Angular

Para obter o mesmo efeito em Angular, precisamos ser um pouco mais inventivos.

 import { Injectable } from '@angular/core' import { BehaviorSubject } from 'rxjs/BehaviorSubject' @Injectable() export class HomeService { message = 'Welcome to home page' counterSubject = new BehaviorSubject(0) // Computed property can serve as basis for further computed properties counterMessage = new BehaviorSubject('') constructor() { // Manually subscribe to each subject that couterMessage depends on this.counterSubject.subscribe(this.recomputeCounterMessage) } // Needs to have bound this private recomputeCounterMessage = (x) => { console.log('recompute counterMessage!') this.counterMessage.next(`${x} ${x === 1 ? 'click' : 'clicks'} since last visit`) } increment() { this.counterSubject.next(this.counterSubject.getValue() + 1) } }

Precisamos definir todos os valores que servem de base para uma propriedade computada como um BehaviorSubject . A propriedade computada em si também é um BehaviorSubject , pois qualquer propriedade computada pode servir como entrada para outra propriedade computada.

Claro, RxJS pode fazer muito mais do que apenas isso, mas isso seria um tópico para um artigo completamente diferente. A desvantagem menor é que esse uso trivial de RxJS apenas para propriedades computadas é um pouco mais detalhado do que o exemplo de reação, e você precisa gerenciar assinaturas manualmente (como aqui no construtor).

 <md-input-container> <input mdInput placeholder='Edit your name' [(ngModel)]='appService.username' /> </md-input-container> <span>{{homeService.counterMessage | async}}</span> <button (click)='homeService.increment()'>Click!</button>

Observe como podemos referenciar o assunto RxJS com o | async tubo | async . Isso é um toque agradável, muito mais curto do que precisar se inscrever em seus componentes. O componente de input é controlado pela [(ngModel)] . Apesar de parecer estranho, na verdade é bastante elegante. Apenas um açúcar sintático para vinculação de dados de valor para appService.username e valor de atribuição automática do evento de entrada do usuário.

Resumo: Propriedades computadas são mais fáceis de implementar em React/MobX do que em Angular/RxJS, mas o RxJS pode fornecer alguns recursos FRP mais úteis, que podem ser apreciados posteriormente.

Modelos e CSS

Para mostrar como os templates se comparam, vamos usar o componente Posts que exibe uma lista de posts.

Angular

 @Component({ selector: 'app-posts', templateUrl: './posts.component.html', styleUrls: ['./posts.component.css'], providers: [ PostsService ] }) export class PostsComponent implements OnInit { constructor( public postsService: PostsService, public appService: AppService ) { } ngOnInit() { this.postsService.initializePosts() } }

Este componente apenas conecta HTML, CSS e serviços injetados e também chama a função para carregar postagens da API na inicialização. AppService é um singleton definido no módulo do aplicativo, enquanto PostsService é transitório, com uma nova instância criada a cada vez que o componente é criado. O CSS referenciado por este componente tem como escopo este componente, o que significa que o conteúdo não pode afetar nada fora do componente.

 <a routerLink="/form" class="float-right"> <button md-fab> <md-icon>add</md-icon> </button> </a> <h3>Hello {{appService.username}}</h3> <md-card *ngFor="let post of postsService.posts"> <md-card-title>{{post.title}}</md-card-title> <md-card-subtitle>{{post.name}}</md-card-subtitle> <md-card-content> <p> {{post.message}} </p> </md-card-content> </md-card>

No modelo HTML, fazemos referência principalmente aos componentes do Angular Material. Para tê-los disponíveis, foi necessário incluí-los nas importações do app.module (veja acima). A diretiva *ngFor é usada para repetir o componente md-card para cada post.

CSS local:

 .mat-card { margin-bottom: 1rem; }

O CSS local apenas aumenta uma das classes presentes no componente md-card .

CSS global:

 .float-right { float: right; }

Esta classe é definida no arquivo global style.css para torná-la disponível para todos os componentes. Ele pode ser referenciado da maneira padrão, class="float-right" .

CSS compilado:

 .float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; }

Em CSS compilado, podemos ver que o CSS local tem o escopo do componente renderizado usando o seletor de atributo [_ngcontent-c1] . Cada componente Angular renderizado tem uma classe gerada como esta para fins de escopo CSS.

A vantagem desse mecanismo é que podemos referenciar classes normalmente, e o escopo é tratado “sob o capô”.

Reagir

 import * as style from './posts.css' import * as appStyle from '../app.css' @observer export class Posts extends React.Component<any, any> { postsStore: PostsStore componentWillMount() { this.postsStore = new PostsStore() this.postsStore.initializePosts() } render() { return <Provider postsStore={this.postsStore}> <PostsComponent /> </Provider> } }

Em React, novamente, precisamos usar a abordagem Provider para tornar a dependência PostsStore “transitória”. Também importamos estilos CSS, referenciados como style e appStyle , para poder usar as classes desses arquivos CSS em JSX.

 interface PostsComponentProps { appStore?: AppStore, postsStore?: PostsStore } @inject('appStore', 'postsStore') @observer export class PostsComponent extends React.Component<PostsComponentProps, any> { render() { const { postsStore, appStore } = this.props return <div> <NavLink to='form'> <Button icon='add' floating accent className={appStyle.floatRight} /> </NavLink> <h3>Hello {appStore.username}</h3> {postsStore.posts.map(post => <Card key={post.id} className={style.messageCard}> <CardTitle title={post.title} subtitle={post.name} /> <CardText>{post.message}</CardText> </Card> )} </div> } }

Naturalmente, o JSX parece muito mais JavaScript do que os modelos HTML do Angular, o que pode ser bom ou ruim, dependendo do seu gosto. Em vez da diretiva *ngFor , usamos a construção map para iterar sobre as postagens.

Agora, o Angular pode ser o framework que mais elogia o TypeScript, mas na verdade é o JSX onde o TypeScript realmente brilha. Com a adição de módulos CSS (importados acima), ele realmente transforma a codificação do seu modelo em zen de conclusão de código. Cada coisa é verificada por tipo. Componentes, atributos e até classes CSS ( appStyle.floatRight e style.messageCard , veja abaixo). E, claro, a natureza enxuta do JSX incentiva a divisão em componentes e fragmentos um pouco mais do que os modelos do Angular.

CSS local:

 .messageCard { margin-bottom: 1rem; }

CSS global:

 .floatRight { float: right; }

CSS compilado:

 .floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }

Como você pode ver, o carregador de módulos CSS postfixa cada classe CSS com um postfix aleatório, o que garante exclusividade. Uma maneira simples de evitar conflitos. As classes são então referenciadas através dos objetos importados do webpack. Uma possível desvantagem disso pode ser que você não pode simplesmente criar um CSS com uma classe e aumentá-lo, como fizemos no exemplo Angular. Por outro lado, isso pode ser realmente uma coisa boa, porque força você a encapsular os estilos corretamente.

Resumo: Eu pessoalmente gosto um pouco mais do JSX do que dos templates Angular, especialmente por causa do suporte ao preenchimento de código e verificação de tipo. Isso realmente é um recurso matador. Angular agora tem o compilador AOT, que também pode detectar algumas coisas, a conclusão de código também funciona para cerca de metade das coisas, mas não é tão completo quanto o JSX/TypeScript.

GraphQL - Carregando dados

Então decidimos usar o GraphQL para armazenar dados para este aplicativo. Uma das maneiras mais fáceis de criar back-end do GraphQL é usar alguns BaaS, como o Graphcool. Então foi isso que fizemos. Basicamente, você apenas define modelos e atributos, e seu CRUD está pronto.

Código Comum

Como parte do código relacionado ao GraphQL é 100% o mesmo para ambas as implementações, não vamos repeti-lo duas vezes:

 const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `

GraphQL é uma linguagem de consulta destinada a fornecer um conjunto mais rico de funcionalidades em comparação com os endpoints RESTful clássicos. Vamos dissecar essa consulta em particular.

  • PostsQuery é apenas um nome para esta consulta para referência posterior, pode ser qualquer nome.
  • allPosts é a parte mais importante - faz referência à função para consultar todos os registros com o modelo `Post`. Este nome foi criado por Graphcool.
  • orderBy e first são parâmetros da função allPosts . createdAt é um dos atributos do modelo Post . first: 5 significa que retornará apenas os primeiros 5 resultados da consulta.
  • id , name , title e message são os atributos do modelo Post que queremos incluir no resultado. Outros atributos serão filtrados.

Como você já pode ver, é bastante poderoso. Confira esta página para se familiarizar mais com as consultas do GraphQL.

 interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }

Sim, como bons cidadãos do TypeScript, criamos interfaces para resultados do GraphQL.

Angular

 @Injectable() export class PostsService { posts = [] constructor(private apollo: Apollo) { } initializePosts() { this.apollo.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }).subscribe(({ data }) => { this.posts = data.allPosts }) } }

A consulta GraphQL é um observável RxJS e nós a assinamos. Funciona um pouco como uma promessa, mas não exatamente, então estamos sem sorte usando async/await . Claro, ainda há o Promise, mas não parece ser o caminho Angular de qualquer maneira. Definimos fetchPolicy: 'network-only' porque, neste caso, não queremos armazenar em cache os dados, mas buscar novamente a cada vez.

Reagir

 export class PostsStore { appStore: AppStore @observable posts: Array<Post> = [] constructor() { this.appStore = AppStore.getInstance() } async initializePosts() { const result = await this.appStore.apolloClient.query<PostsQueryResult>({ query: PostsQuery, fetchPolicy: 'network-only' }) this.posts = result.data.allPosts } }

A versão React é quase idêntica, mas como o apolloClient aqui usa promessas, podemos aproveitar a sintaxe async/await . Existem outras abordagens no React que apenas “gravam” as consultas do GraphQL para componentes de ordem superior, mas me pareceu misturar os dados e a camada de apresentação um pouco demais.

Resumo: As idéias do RxJS subscribe vs. async/await são realmente as mesmas.

GraphQL - Salvando dados

Código Comum

Novamente, alguns códigos relacionados ao GraphQL:

 const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `

O objetivo das mutações é criar ou atualizar registros. Portanto, é benéfico declarar algumas variáveis ​​com a mutação porque essa é a maneira de passar dados para ela. Portanto, temos as variáveis name , title e message , digitadas como String , que precisamos preencher cada vez que chamamos essa mutação. A função createPost , novamente, é definida pelo Graphcool. Especificamos que as chaves do modelo Post terão valores de variáveis ​​de mutação, e também que queremos que apenas o id do Post recém-criado seja enviado em retorno.

Angular

 @Injectable() export class FormService { constructor( private apollo: Apollo, private router: Router, private appService: AppService ) { } addPost(value) { this.apollo.mutate({ mutation: AddPostMutation, variables: { name: this.appService.username, title: value.title, message: value.message } }).subscribe(({ data }) => { this.router.navigate(['/posts']) }, (error) => { console.log('there was an error sending the query', error) }) } }

Ao chamar apollo.mutate , precisamos fornecer a mutação que chamamos e as variáveis ​​também. Obtemos o resultado no retorno de chamada de subscribe e usamos o router injetado para navegar de volta à lista de postagens.

Reagir

 export class FormStore { constructor() { this.appStore = AppStore.getInstance() this.routerStore = RouterStore.getInstance() this.postFormState = new PostFormState() } submit = async () => { await this.postFormState.form.validate() if (this.postFormState.form.error) return const result = await this.appStore.apolloClient.mutate( { mutation: AddPostMutation, variables: { name: this.appStore.username, title: this.postFormState.title.value, message: this.postFormState.message.value } } ) this.goBack() } goBack = () => { this.routerStore.history.push('/posts') } }

Muito semelhante ao anterior, com a diferença de injeção de dependência mais “manual”, e o uso de async/await .

Resumo: Novamente, não há muita diferença aqui. assinar vs. async/await é basicamente tudo o que difere.

Formulários

Queremos alcançar os seguintes objetivos com formulários neste aplicativo:

  • Associação de dados de campos a um modelo
  • Mensagens de validação para cada campo, várias regras
  • Suporte para verificar se todo o formulário é válido

Reagir

 export const check = (validator, message, options) => (value) => (!validator(value, options) && message) export const checkRequired = (msg: string) => check(nonEmpty, msg) export class PostFormState { title = new FieldState('').validators( checkRequired('Title is required'), check(isLength, 'Title must be at least 4 characters long.', { min: 4 }), check(isLength, 'Title cannot be more than 24 characters long.', { max: 24 }), ) message = new FieldState('').validators( checkRequired('Message cannot be blank.'), check(isLength, 'Message is too short, minimum is 50 characters.', { min: 50 }), check(isLength, 'Message is too long, maximum is 1000 characters.', { max: 1000 }), ) form = new FormState({ title: this.title, message: this.message }) }

Assim, a biblioteca formstate funciona da seguinte forma: Para cada campo do seu formulário, você define um FieldState . O parâmetro passado é o valor inicial. A propriedade validators recebe uma função, que retorna “false” quando o valor é válido e uma mensagem de validação quando o valor não é válido. Com as funções auxiliares check e checkRequired , tudo pode parecer bem declarativo.

Para ter a validação de todo o formulário, é benéfico também envolver esses campos com uma instância FormState , que fornece a validade agregada.

 @inject('appStore', 'formStore') @observer export class FormComponent extends React.Component<FormComponentProps, any> { render() { const { appStore, formStore } = this.props const { postFormState } = formStore return <div> <h2> Create a new post </h2> <h3> You are now posting as {appStore.username} </h3> <Input type='text' label='Title' name='title' error={postFormState.title.error} value={postFormState.title.value} onChange={postFormState.title.onChange} /> <Input type='text' multiline={true} rows={3} label='Message' name='message' error={postFormState.message.error} value={postFormState.message.value} onChange={postFormState.message.onChange} />

A instância FormState fornece as propriedades value , onChange e error , que podem ser facilmente usadas com qualquer componente de front-end.

 <Button label='Cancel' onClick={formStore.goBack} raised accent /> &nbsp; <Button label='Submit' onClick={formStore.submit} raised disabled={postFormState.form.hasError} primary /> </div> } }

When form.hasError is true , we keep the button disabled. The submit button sends the form to the GraphQL mutation presented earlier.

Angular

In Angular, we are going to use FormService and FormBuilder , which are parts of the @angular/forms package.

 @Component({ selector: 'app-form', templateUrl: './form.component.html', providers: [ FormService ] }) export class FormComponent { postForm: FormGroup validationMessages = { 'title': { 'required': 'Title is required.', 'minlength': 'Title must be at least 4 characters long.', 'maxlength': 'Title cannot be more than 24 characters long.' }, 'message': { 'required': 'Message cannot be blank.', 'minlength': 'Message is too short, minimum is 50 characters', 'maxlength': 'Message is too long, maximum is 1000 characters' } }

First, let's define the validation messages.

 constructor( private router: Router, private formService: FormService, public appService: AppService, private fb: FormBuilder, ) { this.createForm() } createForm() { this.postForm = this.fb.group({ title: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(24)] ], message: ['', [Validators.required, Validators.minLength(50), Validators.maxLength(1000)] ], }) }

Using FormBuilder , it's quite easy to create the form structure, even more succintly than in the React example.

 get validationErrors() { const errors = {} Object.keys(this.postForm.controls).forEach(key => { errors[key] = '' const control = this.postForm.controls[key] if (control && !control.valid) { const messages = this.validationMessages[key] Object.keys(control.errors).forEach(error => { errors[key] += messages[error] + ' ' }) } }) return errors }

To get bindable validation messages to the right place, we need to do some processing. This code is taken from the official documentation, with a few small changes. Basically, in FormService, the fields keep reference just to active errors, identified by validator name, so we need to manually pair the required messages to affected fields. This is not entirely a drawback; it, for example, lends itself more easily to internationalization.

 onSubmit({ value, valid }) { if (!valid) { return } this.formService.addPost(value) } onCancel() { this.router.navigate(['/posts']) } }

Again, when the form is valid, data can be sent to GraphQL mutation.

 <h2> Create a new post </h2> <h3> You are now posting as {{appService.username}} </h3> <form [formGroup]="postForm" (ngSubmit)="onSubmit(postForm)" novalidate> <md-input-container> <input mdInput placeholder="Title" formControlName="title"> <md-error>{{validationErrors['title']}}</md-error> </md-input-container> <br> <br> <md-input-container> <textarea mdInput placeholder="Message" formControlName="message"></textarea> <md-error>{{validationErrors['message']}}</md-error> </md-input-container> <br> <br> <button md-raised-button (click)="onCancel()" color="warn">Cancel</button> <button md-raised-button type="submit" color="primary" [disabled]="postForm.dirty && !postForm.valid">Submit</button> <br> <br> </form>

The most important thing is to reference the formGroup we have created with the FormBuilder, which is the [formGroup]="postForm" assignment. Fields inside the form are bound to the form model through the formControlName property. Again, we disable the “Submit” button when the form is not valid. We also need to add the dirty check, because here, the non-dirty form can still be invalid. We want the initial state of the button to be “enabled” though.

Summary: This approach to forms in React and Angular is quite different on both validation and template fronts. The Angular approach involves a bit more “magic” instead of straightforward binding, but, on the other hand, is more complete and thorough.

Bundle size

Oh, one more thing. The production minified JS bundle sizes, with default settings from the application generators: notably Tree Shaking in React and AOT compilation in Angular.

  • Angular: 1200 KB
  • React: 300 KB

Well, not much surprise here. Angular has always been the bulkier one.

When using gzip, the sizes go down to 275kb and 127kb respectively.

Just keep in mind, this is basically all vendor libraries. The amount of actual application code is minimal by comparison, which is not the case in a real-world application. There, the ratio would be probably more like 1:2 than 1:4. Also, when you start including a lot of third-party libraries with React, the bundle size also tends to grow rather quickly.

Flexibility of Libraries vs. Robustness of Framework

So it seems that we have not been able (again!) to turn up a clear answer on whether Angular or React is better for web development.

It turns out that the development workflows in React and Angular can be very similar, depending on which libraries we chose to use React with. Then it's a mainly a matter of personal preference.

If you like ready-made stacks, powerful dependency injection and plan to use some RxJS goodies, chose Angular.

If you like to tinker and build your stack yourself, you like the straightforwardness of JSX and prefer simpler computable properties, choose React/MobX.

Again, you can get the complete source code of the application from this article here and here.

Or, if you prefer bigger, RealWorld examples:

  • RealWorld Angular 4+
  • RealWorld React/MobX

Choose Your Programming Paradigm First

Programming with React/MobX is actually more similar to Angular than with React/Redux. There are some notable differences in templates and dependency management, but they have the same mutable/data binding paradigm.

React/Redux with its immutable/unidirectional paradigm is a completely different beast.

Don't be fooled by the small footprint of the Redux library. It might be tiny, but it's a framework nevertheless. Most of the Redux best practices today are focused on using redux-compatible libraries, like Redux Saga for async code and data fetching, Redux Form for form management, Reselect for memorized selectors (Redux's computed values). and Recompose among others for more fine-grained lifecycle management. Also, there's a shift in Redux community from Immutable.js to Ramda or lodash/fp, which work with plain JS objects instead of converting them.

A nice example of modern Redux is the well-known React Boilerplate. It's a formidable development stack, but if you take a look at it, it is really very, very different from anything we have seen in this post so far.

I feel that Angular is getting a bit of unfair treatment from the more vocal part of JavaScript community. Many people who express dissatisfaction with it probably do not appreciate the immense shift that happened between the old AngularJS and today's Angular. In my opinion, it's a very clean and productive framework that would take the world by storm had it appeared 1-2 years earlier.

Still, Angular is gaining a solid foothold, especially in the corporate world, with big teams and needs for standardization and long-term support. Or to put it in another way, Angular is how Google engineers think web development should be done, if that still amounts to anything.

As for MobX, similar assessment applies. Really great, but underappreciated.

In conclusion: before choosing between React and Angular, choose your programming paradigm first.

mutable/data-binding or immutable/unidirectional , that… seems to be the real issue.