Angular vs. React: ¿Cuál es mejor para el desarrollo web?
Publicado: 2022-03-11Hay innumerables artículos que debaten si React o Angular es la mejor opción para el desarrollo web. ¿Necesitamos otro más?
La razón por la que escribí este artículo es porque ninguno de los artículos publicados ya, aunque contienen grandes conocimientos, son lo suficientemente profundos como para que un desarrollador front-end práctico decida cuál puede satisfacer sus necesidades.
En este artículo, aprenderá cómo Angular y React tienen como objetivo resolver problemas front-end similares, aunque con filosofías muy diferentes, y si elegir uno u otro es simplemente una cuestión de preferencia personal. Para compararlos, construiremos la misma aplicación dos veces, una vez con Angular y luego otra vez con React.
Anuncio inoportuno de Angular
Hace dos años, escribí un artículo sobre React Ecosystem. Entre otros puntos, el artículo argumentaba que Angular se había convertido en víctima de “muerte por preanuncio”. En ese entonces, la elección entre Angular y casi cualquier otra cosa era fácil para cualquiera que no quisiera que su proyecto se ejecutara en un marco obsoleto. Angular 1 estaba obsoleto y Angular 2 ni siquiera estaba disponible en la versión alfa.
En retrospectiva, los temores estaban más o menos justificados. Angular 2 cambió drásticamente e incluso pasó por una reescritura importante justo antes del lanzamiento final.
Dos años después, tenemos Angular 4 con la promesa de una estabilidad relativa a partir de ahora.
¿Ahora que?
Angular vs. React: Comparando manzanas y naranjas
Algunas personas dicen que comparar React y Angular es como comparar manzanas con naranjas. Mientras que una es una biblioteca que se ocupa de las vistas, la otra es un marco completo.
Por supuesto, la mayoría de los desarrolladores de React agregarán algunas bibliotecas a React para convertirlo en un marco completo. Por otra parte, el flujo de trabajo resultante de esta pila suele ser muy diferente al de Angular, por lo que la comparabilidad sigue siendo limitada.
La mayor diferencia radica en la gestión estatal. Angular viene con el enlace de datos incluido, mientras que React hoy en día suele estar aumentado por Redux para proporcionar un flujo de datos unidireccional y trabajar con datos inmutables. Esos son enfoques opuestos por derecho propio, y ahora hay innumerables discusiones sobre si el enlace de datos/mutable es mejor o peor que inmutable/unidireccional.
Un campo de juego nivelado
Como React es famoso por ser más fácil de piratear, he decidido, para el propósito de esta comparación, construir una configuración de React que refleje Angular razonablemente cerca para permitir la comparación lado a lado de fragmentos de código.
Ciertas características de Angular que se destacan pero que no están en React por defecto son:
Rasgo | paquete angular | Reaccionar biblioteca |
---|---|---|
Enlace de datos, inyección de dependencia (DI) | @ angular/núcleo | MobX |
Propiedades calculadas | rxjs | MobX |
Enrutamiento basado en componentes | @angular/enrutador | Reaccionar enrutador v4 |
componentes de diseño de materiales | @angular/material | Caja de herramientas de reacción |
CSS en el ámbito de los componentes | @ angular/núcleo | Módulos CSS |
Validaciones de formularios | @angular/formularios | Estado del formulario |
Generador de proyectos | @angular/cli | Reaccionar secuencias de comandos TS |
El enlace de datos
Podría decirse que el enlace de datos es más fácil para empezar que el enfoque unidireccional. Por supuesto, sería posible ir en una dirección completamente opuesta y usar Redux o mobx-state-tree con React y ngrx con Angular. Pero eso sería tema para otro post.
Propiedades calculadas
En lo que respecta al rendimiento, los captadores simples en Angular simplemente están fuera de discusión, ya que se les llama en cada renderizado. Es posible usar BehaviorSubject de RsJS, que hace el trabajo.
Con React, es posible usar @computed de MobX, que logra el mismo objetivo, posiblemente con una API un poco mejor.
Inyección de dependencia
La inyección de dependencia es algo controvertida porque va en contra del paradigma React actual de programación funcional e inmutabilidad. Resulta que algún tipo de inyección de dependencia es casi indispensable en los entornos de vinculación de datos, ya que ayuda con el desacoplamiento (y, por lo tanto, la simulación y las pruebas) donde no existe una arquitectura de capa de datos separada.
Una ventaja más de DI (compatible con Angular) es la capacidad de tener diferentes ciclos de vida de diferentes tiendas. La mayoría de los paradigmas de React actuales usan algún tipo de estado de aplicación global que se asigna a diferentes componentes, pero según mi experiencia, es muy fácil introducir errores al limpiar el estado global al desmontar el componente.
Tener una tienda que se crea en el montaje del componente (y estar disponible sin problemas para los hijos de este componente) parece ser un concepto realmente útil y, a menudo, se pasa por alto.
Fuera de la caja en Angular, pero también bastante fácilmente reproducible con MobX.
Enrutamiento
El enrutamiento basado en componentes permite que los componentes administren sus propias subrutas en lugar de tener una gran configuración de enrutador global. Este enfoque finalmente llegó a react-router
en la versión 4.
Diseño de materiales
Siempre es bueno comenzar con algunos componentes de nivel superior, y el diseño de materiales se ha convertido en algo así como una opción predeterminada universalmente aceptada, incluso en proyectos que no son de Google.
Deliberadamente, elegí React Toolbox en lugar de Material UI, que generalmente se recomienda, ya que Material UI tiene serios problemas de rendimiento confesos con su enfoque de CSS en línea, que planean resolver en la próxima versión.
Además, PostCSS/cssnext utilizado en React Toolbox está comenzando a reemplazar a Sass/LESS de todos modos.
CSS con alcance
Las clases CSS son algo así como variables globales. Existen numerosos enfoques para organizar CSS para evitar conflictos (incluido BEM), pero existe una clara tendencia actual en el uso de bibliotecas que ayudan a procesar CSS para evitar esos conflictos sin la necesidad de que un desarrollador front-end diseñe sistemas de nombres CSS elaborados.
Validación de formulario
Las validaciones de formularios son una característica no trivial y muy utilizada. Es bueno tenerlos cubiertos por una biblioteca para evitar la repetición de código y los errores.
Generador de proyectos
Tener un generador CLI para un proyecto es un poco más conveniente que tener que clonar plantillas de GitHub.
Misma aplicación, construida dos veces
Así que vamos a crear la misma aplicación en React y Angular. Nada espectacular, solo un Shoutboard que permite que cualquier persona publique mensajes en una página común.
Puedes probar las aplicaciones aquí:
- Tablero Angular
- Reacción del tablero de anuncios
Si desea tener el código fuente completo, puede obtenerlo de GitHub:
- Fuente angular de tablero de anuncios
- Fuente de reacción del tablero de anuncios
Notará que también hemos usado TypeScript para la aplicación React. Las ventajas de la verificación de tipos en TypeScript son obvias. Y ahora, como un mejor manejo de las importaciones, async/await y rest spread finalmente han llegado a TypeScript 2, deja a Babel/ES7/Flow en el polvo.
Además, agreguemos Apollo Client a ambos porque queremos usar GraphQL. Quiero decir, REST es genial, pero después de una década más o menos, se vuelve obsoleto.
Bootstrap y enrutamiento
Primero, echemos un vistazo a los puntos de entrada de ambas aplicaciones.
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' }
Básicamente, todos los componentes que queremos usar en la aplicación deben ir a las declaraciones. Todas las bibliotecas de terceros a las importaciones y todas las tiendas globales a los proveedores. Los componentes para niños tienen acceso a todo esto, con la oportunidad de agregar más cosas locales.
Reaccionar
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') )
El componente <Provider/>
se utiliza para la inyección de dependencia en MobX. Guarda las tiendas en contexto para que los componentes de React puedan inyectarlas más tarde. Sí, el contexto de React puede (posiblemente) usarse de manera segura.
La versión de React es un poco más corta porque no hay declaraciones de módulo; por lo general, solo importa y está listo para usar. A veces, este tipo de dependencia estricta no es deseada (prueba), por lo que para las tiendas globales únicas, tuve que usar este patrón GoF de décadas de antigüedad:
export class AppStore { static instance: AppStore static getInstance() { return AppStore.instance || (AppStore.instance = new AppStore()) } @observable username = 'Mr. User' }
El Router de Angular es inyectable, por lo que se puede usar desde cualquier lugar, no solo componentes. Para lograr lo mismo en reaccionar, usamos el paquete mobx-react-router e inyectamos el routerStore
.
Resumen: Arrancar ambas aplicaciones es bastante sencillo. React tiene la ventaja de ser más simple, usando solo importaciones en lugar de módulos, pero, como veremos más adelante, esos módulos pueden ser muy útiles. Hacer singletons manualmente es un poco molesto. En cuanto a la sintaxis de declaración de enrutamiento, JSON vs. JSX es solo una cuestión de preferencia.
Enlaces y Navegación Imperativa
Entonces, hay dos casos para cambiar una ruta. Declarativa, utilizando elementos <a href...>
e imperativa, llamando directamente a la API de enrutamiento (y, por lo tanto, a la ubicación).
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>
Angular Router detecta automáticamente qué routerLink
está activo y le asigna una clase routerLinkActive
adecuada para que se pueda diseñar.
El enrutador usa el elemento especial <router-outlet>
para representar cualquier ruta actual que dicte. Es posible tener muchos <router-outlet>
s, a medida que profundizamos en los subcomponentes de la aplicación.
@Injectable() export class FormService { constructor(private router: Router) { } goBack() { this.router.navigate(['/posts']) } }
El módulo del enrutador se puede inyectar a cualquier servicio (medio mágicamente por su tipo TypeScript), la declaración private
luego lo almacena en la instancia sin necesidad de una asignación explícita. Utilice el método de navigate
para cambiar de URL.
Reaccionar
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>
React Router también puede establecer la clase de enlace activo con activeClassName
.
Aquí, no podemos proporcionar el nombre de la clase directamente, porque el compilador de módulos CSS lo ha hecho único, y necesitamos usar el asistente de style
. Más sobre eso más adelante.
Como se vio arriba, React Router usa el elemento <Switch>
dentro de un elemento <App>
. Como el elemento <Switch>
simplemente envuelve y monta la ruta actual, significa que las subrutas del componente actual son solo this.props.children
. Así que eso también es componible.
export class FormStore { routerStore: RouterStore constructor() { this.routerStore = RouterStore.getInstance() } goBack = () => { this.routerStore.history.push('/posts') } }
El mobx-router-store
también permite una fácil inyección y navegación.
Resumen: Ambos enfoques de enrutamiento son bastante comparables. Angular parece ser más intuitivo, mientras que React Router tiene una composición un poco más sencilla.
Inyección de dependencia
Ya se ha demostrado que es beneficioso separar la capa de datos de la capa de presentación. Lo que estamos tratando de lograr con DI aquí es hacer que los componentes de las capas de datos (aquí denominados modelo/tienda/servicio) sigan el ciclo de vida de los componentes visuales y, por lo tanto, permitan crear una o varias instancias de dichos componentes sin la necesidad de tocar globalmente. estado. Además, debería ser posible mezclar y combinar datos compatibles y capas de visualización.
Los ejemplos en este artículo son muy simples, por lo que todas las cosas de DI pueden parecer exageradas, pero resultan útiles a medida que crece la aplicación.
Angular
@Injectable() export class HomeService { message = 'Welcome to home page' counter = 0 increment() { this.counter++ } }
Por lo tanto, cualquier clase puede convertirse en @injectable
y sus propiedades y métodos pueden estar disponibles para los componentes.
@Component({ selector: 'app-home', templateUrl: './home.component.html', providers: [ HomeService ] }) export class HomeComponent { constructor( public homeService: HomeService, public appService: AppService, ) { } }
Al registrar el HomeService
a los providers
del componente, lo ponemos a disposición de este componente exclusivamente. Ahora no es un singleton, pero cada instancia del componente recibirá una nueva copia, nueva en el montaje del componente. Eso significa que no hay datos obsoletos del uso anterior.
Por el contrario, AppService
se ha registrado en app.module
(ver arriba), por lo que es un singleton y permanece igual para todos los componentes, durante la vida útil de la aplicación. Ser capaz de controlar el ciclo de vida de los servicios desde los componentes es un concepto muy útil, pero poco apreciado.
DI funciona asignando las instancias de servicio al constructor del componente, identificado por tipos de TypeScript. Además, las palabras clave public
asignan automáticamente los parámetros a this
, por lo que ya no necesitamos escribir esas aburridas líneas 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>
La sintaxis de la plantilla de Angular, posiblemente bastante elegante. Me gusta el atajo [()]
, que funciona como un enlace de datos bidireccional, pero bajo el capó, en realidad es un evento de enlace de atributo +. Como dicta el ciclo de vida de nuestros servicios, homeService.counter
se restablecerá cada vez que naveguemos fuera de /home
, pero appService.username
permanece y es accesible desde cualquier lugar.
Reaccionar
import { observable } from 'mobx' export class HomeStore { @observable counter = 0 increment = () => { this.counter++ } }
Con MobX, necesitamos agregar el decorador @observable
a cualquier propiedad que queramos hacer observable.
@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 administrar el ciclo de vida correctamente, necesitamos hacer un poco más de trabajo que en el ejemplo de Angular. HomeComponent
dentro de un Provider
, que recibe una nueva instancia de HomeStore
en cada montaje.
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 el decorador @observer
para escuchar los cambios en las propiedades @observable
.
El mecanismo oculto de esto es bastante interesante, así que analicémoslo brevemente aquí. El decorador @observable
reemplaza una propiedad en un objeto con getter y setter, lo que le permite interceptar llamadas. Cuando se llama a la función de representación de un componente aumentado de @observer
, se llama a esos captadores de propiedades y mantienen una referencia al componente que los llamó.
Luego, cuando se llama a setter y se cambia el valor, se llaman las funciones de representación de los componentes que usaron la propiedad en la última representación. Ahora, los datos sobre qué propiedades se usan y dónde se actualizan y todo el ciclo puede comenzar de nuevo.
Un mecanismo muy simple, y bastante eficaz también. Explicación más detallada aquí.
El decorador @inject
se usa para inyectar instancias de appStore
y homeStore
en los accesorios de HomeComponent
. En este punto, cada una de esas tiendas tiene un ciclo de vida diferente. appStore
es el mismo durante la vida de la aplicación, pero homeStore
se crea de nuevo en cada navegación a la ruta "/ home".
El beneficio de esto es que no es necesario limpiar las propiedades manualmente como es el caso cuando todas las tiendas son globales, lo cual es una molestia si la ruta es una página de "detalle" que contiene datos completamente diferentes cada vez.
Resumen: Como la gestión del ciclo de vida del proveedor es una característica inherente de la DI de Angular, es, por supuesto, más simple lograrlo allí. La versión React también se puede usar, pero implica mucho más repetitivo.
Propiedades calculadas
Reaccionar
Comencemos con React en este, tiene una solución más sencilla.
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` } }
Así que tenemos una propiedad calculada que se une al counter
y devuelve un mensaje correctamente pluralizado. El resultado de counterMessage
se almacena en caché y se vuelve a calcular solo cuando cambia el counter
.
<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>
Luego, hacemos referencia a la propiedad (y al método de increment
) de la plantilla JSX. El campo de entrada está dirigido por el enlace a un valor y dejando que un método de appStore
maneje el evento del usuario.
Angular
Para lograr el mismo efecto en Angular, debemos ser un poco más 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) } }
Necesitamos definir todos los valores que sirven como base para una propiedad calculada como BehaviorSubject
. La propiedad calculada en sí también es un BehaviorSubject
, porque cualquier propiedad calculada puede servir como entrada para otra propiedad calculada.
Por supuesto, RxJS
puede hacer mucho más que esto, pero ese sería un tema para otro artículo completamente diferente. La desventaja menor es que este uso trivial de RxJS solo para propiedades calculadas es un poco más detallado que el ejemplo de reacción, y necesita administrar las suscripciones manualmente (como aquí en el constructor).
<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>
Tenga en cuenta cómo podemos hacer referencia al tema RxJS con el | async
tubería | async
. Ese es un buen toque, mucho más corto que tener que suscribirse a sus componentes. El componente de input
está controlado por la directiva [(ngModel)]
. A pesar de parecer extraño, en realidad es bastante elegante. Solo un azúcar sintáctico para el enlace de datos de valor a appService.username
y el valor de asignación automática del evento de entrada del usuario.

Resumen: las propiedades calculadas son más fáciles de implementar en React/MobX que en Angular/RxJS, pero RxJS podría proporcionar algunas funciones de FRP más útiles, que se apreciarán más adelante.
Plantillas y CSS
Para mostrar cómo se comparan las plantillas entre sí, usemos el componente Publicaciones que muestra una lista de publicaciones.
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 simplemente conecta HTML, CSS y servicios inyectados y también llama a la función para cargar publicaciones desde la API en la inicialización. AppService
es un singleton definido en el módulo de la aplicación, mientras que PostsService
es transitorio, con una nueva instancia creada cada vez que se crea un componente. El CSS al que se hace referencia desde este componente está en el ámbito de este componente, lo que significa que el contenido no puede afectar nada fuera del 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>
En la plantilla HTML, hacemos referencia principalmente a componentes de Angular Material. Para tenerlos disponibles, era necesario incluirlos en las importaciones de app.module
(ver arriba). La directiva *ngFor
se usa para repetir el componente md-card
para cada publicación.
CSS locales:
.mat-card { margin-bottom: 1rem; }
El CSS local simplemente aumenta una de las clases presentes en el componente md-card
.
CSS globales:
.float-right { float: right; }
Esta clase se define en el archivo global style.css
para que esté disponible para todos los componentes. Se puede hacer referencia a él de la manera estándar, class="float-right"
.
CSS compilado:
.float-right { float: right; } .mat-card[_ngcontent-c1] { margin-bottom: 1rem; }
En el CSS compilado, podemos ver que el CSS local se ha limitado al componente representado mediante el selector de atributos [_ngcontent-c1]
. Cada componente Angular renderizado tiene una clase generada como esta para fines de alcance de CSS.
La ventaja de este mecanismo es que podemos hacer referencia a las clases normalmente, y el alcance se maneja "bajo el capó".
Reaccionar
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> } }
En React, nuevamente, necesitamos usar el enfoque del Provider
para hacer que la dependencia de PostsStore
sea "transitoria". También importamos estilos CSS, a los que se hace referencia como style
y appStyle
, para poder usar las clases de esos archivos CSS en 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, JSX se siente mucho más como JavaScript que las plantillas HTML de Angular, lo que puede ser bueno o malo dependiendo de tus gustos. En lugar de la directiva *ngFor
, usamos la construcción del map
para iterar sobre las publicaciones.
Ahora, Angular podría ser el marco que más promociona TypeScript, pero en realidad es JSX donde TypeScript realmente brilla. Con la adición de módulos CSS (importados arriba), realmente convierte la codificación de su plantilla en zen de finalización de código. Cada cosa está marcada. Componentes, atributos, incluso clases CSS ( appStyle.floatRight
y style.messageCard
, ver más abajo). Y, por supuesto, la naturaleza esbelta de JSX fomenta la división en componentes y fragmentos un poco más que las plantillas de Angular.
CSS locales:
.messageCard { margin-bottom: 1rem; }
CSS globales:
.floatRight { float: right; }
CSS compilado:
.floatRight__qItBM { float: right; } .messageCard__1Dt_9 { margin-bottom: 1rem; }
Como puede ver, el cargador de Módulos CSS agrega un sufijo aleatorio a cada clase CSS, lo que garantiza la unicidad. Una forma sencilla de evitar conflictos. Luego se hace referencia a las clases a través de los objetos importados del paquete web. Un posible inconveniente de esto puede ser que no puede simplemente crear un CSS con una clase y aumentarlo, como hicimos en el ejemplo de Angular. Por otro lado, esto puede ser algo bueno, porque te obliga a encapsular los estilos correctamente.
Resumen: Personalmente, me gusta JSX un poco más que las plantillas Angular, especialmente debido a la compatibilidad con la finalización de código y la verificación de tipos. Esa es realmente una característica increíble. Angular ahora tiene el compilador AOT, que también puede detectar algunas cosas, la finalización del código también funciona para aproximadamente la mitad de las cosas allí, pero no es tan completo como JSX/TypeScript.
GraphQL - Cargando datos
Así que decidimos usar GraphQL para almacenar datos para esta aplicación. Una de las formas más sencillas de crear el back-end de GraphQL es usar algunos BaaS, como Graphcool. Así que eso es lo que hicimos. Básicamente, solo define modelos y atributos, y su CRUD está listo para funcionar.
Código común
Como parte del código relacionado con GraphQL es 100% igual para ambas implementaciones, no lo repitamos dos veces:
const PostsQuery = gql` query PostsQuery { allPosts(orderBy: createdAt_DESC, first: 5) { id, name, title, message } } `
GraphQL es un lenguaje de consulta destinado a proporcionar un conjunto más completo de funcionalidades en comparación con los puntos finales RESTful clásicos. Analicemos esta consulta en particular.
-
PostsQuery
es solo un nombre para que esta consulta haga referencia más adelante, se puede llamar cualquier cosa. -
allPosts
es la parte más importante: hace referencia a la función para consultar todos los registros con el modelo `Post`. Este nombre fue creado por Graphcool. -
orderBy
yfirst
son parámetros de la funciónallPosts
.createdAt
es uno de los atributos del modeloPost
.first: 5
significa que devolverá solo los primeros 5 resultados de la consulta. -
id
,name
,title
ymessage
son los atributos del modeloPost
que queremos incluir en el resultado. Se filtrarán otros atributos.
Como ya puedes ver, es bastante poderoso. Consulte esta página para familiarizarse más con las consultas de GraphQL.
interface Post { id: string name: string title: string message: string } interface PostsQueryResult { allPosts: Array<Post> }
Sí, como buenos ciudadanos de TypeScript, creamos interfaces para los resultados de 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 }) } }
La consulta GraphQL es un observable RxJS y nos suscribimos a ella. Funciona un poco como una promesa, pero no del todo, por lo que no tenemos suerte al usar async/await
. Por supuesto, todavía hay toPromise, pero de todos modos no parece ser la forma Angular. Configuramos fetchPolicy: 'network-only'
porque, en este caso, no queremos almacenar en caché los datos, sino recuperarlos cada vez.
Reaccionar
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 } }
La versión de React es casi idéntica, pero como apolloClient
aquí usa promesas, podemos aprovechar la sintaxis async/await
. Hay otros enfoques en React que simplemente "pegan" las consultas de GraphQL a componentes de orden superior, pero me pareció que mezclaba demasiado la capa de datos y la de presentación.
Resumen: Las ideas de la suscripción RxJS frente a async/await son bastante parecidas.
GraphQL - Guardar datos
Código común
De nuevo, algo de código relacionado con GraphQL:
const AddPostMutation = gql` mutation AddPostMutation($name: String!, $title: String!, $message: String!) { createPost( name: $name, title: $title, message: $message ) { id } } `
El propósito de las mutaciones es crear o actualizar registros. Por lo tanto, es beneficioso declarar algunas variables con la mutación porque esa es la forma de pasarle datos. Así que tenemos variables de name
, title
y message
, escritas como una String
, que debemos completar cada vez que llamamos a esta mutación. La función createPost
, nuevamente, está definida por Graphcool. Especificamos que las claves del modelo Post
tendrán valores de nuestras variables de mutación, y también que solo queremos que se envíe a cambio la id
de la publicación recién creada.
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) }) } }
Al llamar a apollo.mutate
, debemos proporcionar la mutación que llamamos y las variables también. Obtenemos el resultado en la devolución de llamada de subscribe
y usamos el router
inyectado para navegar de regreso a la lista de publicaciones.
Reaccionar
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') } }
Muy similar al anterior, con la diferencia de una inyección de dependencia más "manual" y el uso de async/await
.
Resumen: Una vez más, no hay mucha diferencia aquí. subscribe vs async/await es básicamente todo lo que difiere.
formularios
Queremos lograr los siguientes objetivos con formularios en esta aplicación:
- Enlace de datos de campos a un modelo
- Mensajes de validación para cada campo, múltiples reglas
- Soporte para verificar si todo el formulario es válido
Reaccionar
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 }) }
Entonces, la biblioteca formstate funciona de la siguiente manera: para cada campo de su formulario, define un FieldState
. El parámetro pasado es el valor inicial. La propiedad validators
toma una función que devuelve "falso" cuando el valor es válido y un mensaje de validación cuando el valor no es válido. Con las funciones auxiliares check
y checkRequired
, todo puede parecer muy declarativo.
Para tener la validación de todo el formulario, es beneficioso envolver también esos campos con una instancia de FormState
, que luego proporciona la validez 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} />
La instancia de FormState
proporciona propiedades value
, onChange
y error
, que se pueden usar fácilmente con cualquier componente front-end.
<Button label='Cancel' onClick={formStore.goBack} raised accent /> <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.