Malas prácticas en Angular

antoniocardenas

Antonio Cardenas

Posted on November 4, 2020

Malas prácticas en Angular

Artículo original en ingles por Armen Vardanyan link del articulo en ingles aqui

Angular es asombroso . Provee un sin fin de funcionalidades fuera de la caja (routing, animations, Módulos HTTP, Formularios/Validaciones etc ….), acelera el proceso de desarrollo y no es tan difícil de aprender y adoptar (especialmente con una herramienta tan poderosa como lo es el CLI de Angular ).

Pero como siempre , una gran herramienta en las manos equivocadas es un arma de destrucción masiva, y el dia de hoy vamos a hablar acerca de formas y prácticas en Angular que definitivamente debemos evitar. Así que, sin mas comencemos.

Precaución: Se proveerá de ejemplos de componentes usando gist de github el cual no incluye imports o estructuras , lo cual se considera una mala práctica pero que es más cómodo para su lectura y entendimiento

No hacer un uso real de los componentes de Angular

Los componentes son los bloques de construcción esenciales en el ecosistema de Angular, el puente que conecta la lógica de nuestra aplicación con la vista. Pero a veces los desarrolladores pasan por alto los beneficios que provee un componente.

Ejemplo:

@Component({
  selector: 'app-some-component-with-form',
  template: 
`<div [formGroup]="form">
          <div class="form-control">
            <label>Nombre</label>
            <input type="text" formControlName="Nombre" />
          </div>

          <div class="form-control">
            <label>Apellido</label>
            <input type="text" formControlName="Apellido" />
          </div>

          <div class="form-control">
            <label>Edad</label>
            <input type="text" formControlName="edad" />
          </div>
</div>`
})
export class SomeComponentWithForm {

  public form: FormGroup;

  constructor(private formBuilder: FormBuilder){
    this.form = formBuilder.group({
      Nombre: ['', Validators.required],
      Apellido: ['', Validators.required],
      edad: ['', Validators.max(120)],      
    })
  }

}
Enter fullscreen mode Exit fullscreen mode

Como lo puedes ver tenemos un pequeño formulario con tres controles, y una plantilla que contiene los inputs. Cada input está dentro de un elemento div junto a un label, y los tres contenedores que se repiten a sí mismos. Son esencialmente lo mismo, Asi que, quizas, separarlos en un componente sea más dinámico, miremoslo en acción:

@Component({
  selector: 'app-single-control-component',
  template: 
  ` <div class="form-control">
          <label>{{ label }}</label>
          <input type="text" [formControl]="control" />
        </div> `
})
export class SingleControlComponent{
  @Input() control: AbstractControl 
  @Input() label: string;
}
Enter fullscreen mode Exit fullscreen mode

Así que, hemos separado un control único en su propio en su propio componente , y hemos definido los inputs para pasar datos desde el componente padre, en este caso, la instancia del form control y la el label de el input.

Revisemos nuestra primera plantilla de componente:

<div>
  <app-single-control-component [control]="form.controls['Nombre']" [label]="'Nombre'">
  </app-single-control-component>

  <app-single-control-component [control]="form.controls['Apellido']" [label]="'Apellido'">
  </app-single-control-component>

  <app-single-control-component [control]="form.controls['edad']" [label]="'Edad'">
  </app-single-control-component>
</div>
Enter fullscreen mode Exit fullscreen mode

Este fue un ejemplo muy simple , pero este tipo cosas pueden volverse muy complejas, si los componentes no son usados correctamente, Digamos que tienes una página que incorpora un feed de artículos, en un bloque que se puede desplazar infinitamente, separados por temas , con bloques mas pequeños que representan noticias/ artículos de manera individual como : (Medium. De hecho aquí hay una explicación utilizando el feed de articulos de medium como ejemplo).

Ejemplo perfecto de como los componentes de Angular pueden ser usados<br>

Ahora, si estamos dispuestos a crear algo como esto, podemos preguntarnos cómo usar componentes para hacernos la vida más fácil. Así es como deberíamos:

Descomposición de el feed de artículos de Medium en componentes Angular<br>

Ahora, la pieza más grande sería un componente (marcado con rojo). Esto contendrá una lista de artículos destacados, una función de seguir/dejar de seguir y un título del tema. Las piezas más pequeñas también serían componentes (marcados con verde). A su vez, contendrán un objeto con la información de un solo artículo, la función de historia de marcador / informe y un enlace a todo el artículo. Vea cómo esto ayudó a separar la mayor parte de la lógica (¡divide y vencerás!) En piezas de código reutilizables, que serán más manejables más adelante, si es necesario realizar algún cambio.

Puedes pensar “bueno, separar componentes es un concepto simple de Angular, ¿por qué mencionamos esto como algo tan importante? Todo el mundo lo sabe”, pero el problema es que muchos desarrolladores son engañados por el router module de Angular: este mapea una ruta a un componente. , por lo que las personas (en su mayoría novatos, pero a veces también sucede con desarrolladores más experimentados) comienzan a pensar en estos componentes como en páginas separadas. El componente angular NO es una página, es una parte de la vista y varios componentes juntos componen una vista. Otra situación desagradable es cuando tiene un componente pequeño, en su mayoría sin ninguna lógica específica, pero simplemente crece cada vez más a medida que llegan nuevos requisitos y, en un momento dado, debe comenzar a pensar en la separación, o puede terminar con un componente que se convertirá en una monstruosidad fea e incontrolable.

Usando .toPromise()

Angular viene con su propio módulo HTTP listo para usar para que nuestra aplicación se comunique con un servidor remoto. Como ya debe saber (de lo contrario: ¿por qué está leyendo este artículo?), Angular usa Rx.js para admitir solicitudes HTTP, en lugar de Promesas. ¿Sabes que? No todo el mundo conoce Rx.js, pero si va a utilizar Angular para un proyecto a largo plazo, definitivamente debería aprenderlo. Aquellos que son nuevos en Angular tienden a transformar los Observables, que se devuelven de las llamadas a la API en el módulo HTTP, a Promises, usando .toPromise(), sólo porque están familiarizados con él. Bueno, eso es probablemente lo peor que puede hacer con su aplicación, porque debido a la pereza se suele:

  1. Agregar lógica innecesaria a la aplicación, No debes transformar un Observable en un Promise, puedes usar sin problema el observable

  2. Perderse el montón de cosas asombrosas que Rxjs nos da : podemos almacenar en caché una respuesta, podemos manipular los datos antes de suscribirse, podemos encontrar errores lógicos en los datos recibidos (por ejemplo, si su API siempre devuelve 200 OK con una propiedad booleana de ‘éxito’ para determinar si las cosas salieron bien) y vuelva a generar errores para detectarlos más adelante en su aplicación con solo una o dos líneas de código … pero esto se pierde cuando se usa .toPromise().

No usar Rxjs más a menudo

Este es más que un consejo general. Rxjs es asombroso, y debería considerar usarlo para manipular sus datos, eventos y el estado general de su aplicación con él.

Las directivas olvidadas

Y esto ya es algo viejo. Angular no usa directivas tanto como lo hizo Angular.js (teníamos muchas cosas como ng-click, ng-src, la mayoría de ellas ahora reemplazadas por Inputs y Outputs), pero todavía tiene algunas: ngIf, ngForOf.

La regla general para Angular.js era

No hacer manipulación de DOM en un controlador

La regla general para Angular debería ser:

No hacer manipulaciones DOM en un componente

Es todo lo que necesitas saber. No te olvides de las directivas.

No tener interfaces definidas para sus datos

A veces, puede tender a pensar en los datos recuperados de un servidor / API como cualquier dato, eso es todo, escriba cualquiera. Ese no es realmente el caso. Debes definir todos los tipos para cada dato que recibes de tu backend, porque, después de todo, ya sabes, es por eso que Angular elige usarse principalmente en TypeScript.

Hacer manipulaciones de datos en un componente

Esto es complicado. Sugiero no hacer eso tampoco en un servicio. Los servicios son para llamadas a API, compartir datos entre componentes y otras utilidades. En cambio, las manipulaciones de datos deberían pertenecer a clases de modelos separadas. Mira esto:

interface Pelicula {
  id: number;
  title: string;
}

@Component({
  selector: 'app-some-component-with-form',
  template: `...` //nuestra formulario esta aqui
})
export class SomeComponentWithForm {

  public form: FormGroup;
  public peliculas: Array<Pelicula>

  constructor(private formBuilder: FormBuilder){
    this.form = formBuilder.group({
      nombre: ['', Validators.required],
      apellido: ['', Validators.required],
      edad: ['', Validators.max(120)],
      peliculasfavoritas: [[]], /* 
                tendremos un menú desplegable de selección múltiple
                 en nuestra plantilla para seleccionar películas favoritas
                */
    });
  }

  public onSubmit(values){ 
    /* 
      'valores' es en realidad un valor de formulario, que representa a un usuario
       pero imagina que nuestra API no espera enviar una lista de películas
       objetos, solo una lista de id-s, por lo que tenemos que mapear los valores
    */
    values.peliculasfavoritas = values.peliculasfavoritas.map((pelicula: Pelicula) => pelicula.id);
    // luego enviaremos los datos del usuario al servidor usando algún servicio

  }

}
Enter fullscreen mode Exit fullscreen mode

Ahora, esto no parece una catástrofe, solo una pequeña manipulación de datos antes de enviar los valores al backend. Pero imagínese si hay muchas claves externas, campos de muchos a muchos, mucho manejo de datos, dependiendo de algunos casos, variables, el estado de su aplicación … Su método onSubmit puede convertirse rápidamente en un desastre. Ahora considere hacer esto:

interface Pelicula {
  id: number;
  titulo: string;
}

interface User {
  nombre: string;
  apellido: string;
  edad: number;
  peliculaFavorita: Array<Pelicula | number>;
  /*
    observe cómo supusimos que esta propiedad
    puede ser una matriz de objetos de película
    o de identificadores numéricos
  */
}

class UserModel implements User {
  nombre: string;
  apellido: string;
  edad: number;
  peliculaFavorita: Array<Movie | number>;

  constructor(source: User){
    this.nombre = source.nombre;
    this.apellido = source.apellido;
    this.edad = source.edad;
    this.peliculaFavorita = source.favoriteMovies.map((pelicula: Pelicula) => pelicula.id);
    /*
      movimos la manipulación de datos a esta clase separada,
      que también es una representación válida de un modelo de usuario,
      así que no hay desorden innecesario aquí
    */
  }

}
Enter fullscreen mode Exit fullscreen mode

Ahora, como ves, tenemos una clase, que representa a un usuario, con todas las manipulaciones dentro de su constructor. El componente ahora se verá así:

@Component({
  selector: 'app-some-component-with-form',
  template: `...` // nuestro formulario va aca 
})
export class SomeComponentWithForm {

  public form: FormGroup;
  public peliculas: Array<Peliculas>

  constructor(private formBuilder: FormBuilder){
    this.form = formBuilder.group({
      nombre: ['', Validators.required],
      apellido: ['', Validators.required],
      edad: ['', Validators.max(120)],
      peliculafavorita: [[]], /*
                tendremos un menú desplegable de selección
                múltiple en su plantilla para seleccionar películas favoritas
                */
    });
  }

  public onSubmit(values: Usuario){
    /*
      ahora solo crearemos una nueva instancia de usuario desde nuestro formulario,
      con todas las manipulaciones de datos realizadas dentro del constructor
    */
    let usuario: ModeloUsuario = new ModeloUsuario(values);
    // luego enviaremos los datos del modelo de usuario al servidor usando algún servicio
  }

}
Enter fullscreen mode Exit fullscreen mode

Y cualquier otra manipulación de datos irá dentro del constructor del modelo, sin contaminar el código del componente. Como otra regla general, es posible que desee tener una nueva palabra clave antes de enviar datos a un servidor cada vez.

No usar/hacer mal uso de los Pipes

Quiero explicar este con un ejemplo de inmediato. Supongamos que tiene dos menús desplegables que le permiten seleccionar una unidad de medida de peso. Uno representa una medida tal cual, el otro es una medida por algún precio/cantidad (este es un detalle importante). Desea que el primero se presente como está, pero en cuanto al segundo, desea que las etiquetas vayan precedidas de una contrapleca "/", de modo que se vea como "1 dólar/kg" o "7 dólares/oz".

Echa un vistazo a esto:

@Component({
  selector: 'algun-componente',
  template: `
    <div>
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
      <-- Esto generará un menú desplegable basado en las opciones -->
      <input type="text" placeholder="Precio">
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
      <-- Tenemos que hacer que las etiquetas de esta vayan precedidas de una contrapleca -->
    </div>
`
})
export class SomeComponent {
  public UnidadesdePeso = [{value: 1, label: 'kg'}, {value: 2, label: 'oz'}];
Enter fullscreen mode Exit fullscreen mode

Entonces, vemos que ambos componentes desplegables usan la misma matriz de opciones, por lo que se verán similares. Ahora tenemos que separarlos de alguna manera.

Manera tonta :

@Component({
  selector: 'algun-componente',
  template: `
    <div>
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
      <input type="text" placeholder="Precio">
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
     // Tenemos que hacer que las etiquetas de esta vayan precedidas de una contrapleca
    </div>
`
})
export class SomeComponent {
  public UnidadesPeso = [{value: 1, label: 'kg'}, {value: 2, label: 'oz'}];
  public UnidadesPesoConContrapleca = [{value: 1, label: '/kg'}, {value: 2, label: '/oz'}];
  // acabamos de agregar una nueva propiedad
}
Enter fullscreen mode Exit fullscreen mode

Esto, por supuesto, resuelve el problema, pero ¿qué pasa si los valores no son solo valores constantes almacenados dentro de los componentes, sino que, por ejemplo, se recuperan de un servidor? Y, por supuesto, crear una nueva propiedad para cada mutación de datos pronto nos convertirá en un desastre.

Manera peligrosa:

@Component({
  selector: 'algun-componente',
  template: `
    <div>
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
      <input type="text" placeholder="Precio">
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
      // Tenemos que hacer que las etiquetas de esta vayan precedidas de una contrapleca 
    </div>
`
})
export class AlgunComponent {
  public UnidadesPeso = [{value: 1, label: 'kg'}, {value: 2, label: 'oz'}];
  public get UnidadesPesoConContrapleca() {
    return this.weightUnits.map(weightUnit => {
      return { 
        label: '/' + weightUnit.label,
        value: weightUnit.value
      };
    })
  }
// así que ahora asignamos las unidades de peso existentes a una nueva matriz
}
Enter fullscreen mode Exit fullscreen mode

Esto puede parecer una buena solución, pero en realidad es aún peor. El menú desplegable se renderizará y se verá bien, hasta que intente hacer clic en él, y tal vez incluso antes de eso, puede notar que está parpadeando (¡si, parpadeando!). ¿Por qué? Para comprender eso, es posible que deba profundizar un poco en cómo funcionan las entradas y salidas con el mecanismo de detección de cambios de Angular.

El componente desplegable tiene una entrada de opciones y volverá a representar el menú desplegable cada vez que cambie el valor de la entrada. Aquí, el valor se determina después de una llamada a la función, por lo que el mecanismo de detección de cambios no tiene forma de determinar si ha cambiado o no, por lo que solo tendrá que llamar constantemente a la función en cada iteración de detección de cambios, y el menú desplegable será constantemente re-renderizado. Por lo tanto, el problema se resuelve … creando un problema mayor.

La mejor manera posible:

@Pipe({
  name: 'slashed'
})
export class Cortado implements PipeTransform {
  transform(value){
    return value.map(item => {
      return {
        label: '/' + item.label,
        value: item.value
      };
    })
  }
}


@Component({
  selector: 'algun-component',
  template: 
  `<div>
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
      <input type="text" placeholder="Precio">
      <dropdown-component [options]="(UnidadesdePeso | cortado)"></dropdown-component>
     // esto hara el trabajo -->
    </div>
`
})
export class AlgunComponent {
  public UnidadesdePeso = [{value: 1, label: 'kg'}, {value: 2, label: 'oz'}];

  // delegaremos la transformación de datos a un pipe
}
Enter fullscreen mode Exit fullscreen mode

Bueno, por supuesto que estás familiarizado con las tuberías. Este todavía no es un consejo muy específico (bueno, la documentación en sí nos dice que los usemos en tales casos), pero el punto real que quiero hacer no son los pipes en sí. El punto es: a mí tampoco me gusta esta solución. Si tengo muchas mutaciones de datos simples pero diferentes en mi aplicación, ¿debería escribir una clase Pipe para todas y cada una de ellas? ¿Qué pasa si la mayoría de ellos son tan específicos que solo se usan en uno y solo un contexto de un componente? Esto parece una gran cantidad de desorden.

Una solución más avanzada:

@Pipe({
  name: 'map'
})
export class Mapping implements PipeTransform {
  /* 
  esta será una tubería universal para mapeos de matrices. Puede agregar más
  comprobaciones de tipo y comprobaciones en tiempo de ejecución para asegurarse de que funciona correctamente en todas partes
  */
  transform(value, mappingFunction: Function){
    return mappingFunction(value)
  }
}


@Component({
  selector: 'algun-component',
  template: `
    <div>
      <dropdown-component [options]="UnidadesdePeso"></dropdown-component>
      <input type="text" placeholder="Precio">
      <dropdown-component [options]="(UnidadesdePeso | map : cortado)"></dropdown-component>
      // esto hara el trabajo
    </div>
`
})
export classAlgunComponent {
  public UnidadesdePeso = [{value: 1, label: 'kg'}, {value: 2, label: 'oz'}];

  public cortada(units){
    return units.map(unit => {
      return {
        label: '/' + unit.label,
        value: unit.value
      };
    });
  }
// Delegaremos una función de mapeo personalizada a un pipe más genérico, que simplemente la llamará al cambiar el valor
}
Enter fullscreen mode Exit fullscreen mode

¿Cual es la diferencia? Bueno, una pipes llama a su método de transformación cuando y solo cuando cambian los datos. Siempre que UnidadesdePeso no cambie, la pipe se invocará solo una vez en lugar de en cada iteración de detección de cambios.

No digo que deba tener solo uno o dos pipes de mapeo y nada más, pero debería tener más pipes personalizadas en cosas más complejas (trabajando con fecha y hora, etc.) y donde la reutilización es crucial, y para manipulaciones más específicas de componentes puede considerar tener una pipe universal.

Nota: si pasa una función a algún tipo de pipes universal, asegúrese de que esta función sea pura, es decir, sin efectos secundarios e independiente del estado de sus componentes. Regla de oro: si tiene una palabra clave this en un método de este tipo, ¡no funcionará!
Puede leer más sobre funciones puras en este artículo.

Notas generales sobre reutilización

Siempre que escriba un componente que pueda ser reutilizado por otros desarrolladores, considere la posibilidad de realizar comprobaciones coherentes de todo lo que su componente requiere. Si su componente tiene una entrada de tipo T, que debe definirse para que el componente funcione correctamente, solo verifique que el valor de esta entrada está realmente definido en el constructor. La entrada puede ser de tipo T, pero también puede no estar definida en tiempo de ejecución (TypeScript solo proporciona comprobaciones de tipo en tiempo de compilación). Lanza excepciones para que el error real se exponga en un mejor contexto con tu propio mensaje personalizado, en lugar de en algún contexto Zone.js (como suele ocurrir con los errores Angular).

En general, sea coherente y observador. Es posible que encuentre muchas cosas innecesarias en su aplicación

💖 💪 🙅 🚩
antoniocardenas
Antonio Cardenas

Posted on November 4, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Malas prácticas en Angular
angular Malas prácticas en Angular

November 4, 2020