Malas prácticas en Angular: Avanzado
Antonio Cardenas
Posted on November 4, 2020
Autor de el post original en ingles Armen Vardanyan publicado para indepth.dev artículo original en inglés
Hace algún tiempo el señor Armen Vardanyan publicó un artículo en inDepth recopilando las malas prácticas usadas siempre por desarrolladores en aplicaciones Angular. el cual puedes ver en español aquí.
El día de hoy, el enfoque será centrarnos en algunos otros patrones que hacen que nuestros componentes/directivas/servicios y otras partes de nuestras aplicaciones Angular sean menos legibles y más difíciles de razonar. Sin más preámbulos, ¡comencemos!
Contaminar el ngOnInit
ngOnInit puede ser el gancho del ciclo de vida(lifecycle hook) más importante en los componentes Angular; se utiliza para inicializar datos, configurar algunos listeners, crear conexiones, etc. Pero a veces esto puede hacer que nuestro ciclo de vida sea demasiado abrumador:
ngOnInit
puede ser el gancho del ciclo de vida(lifecycle hook) más importante en los componentes Angular; se utiliza para inicializar datos, configurar algunos listeners, crear conexiones, etc. Pero a veces esto puede hacer que nuestro ciclo de vida sea demasiado abrumador:
@Component({
selector: 'alguna',
template: 'plantilla',
})
export class SomeComponent implements OnInit, OnDestroy {
@ViewChild('btn') buttonRef: ElementRef<HTMLButtonElement>;
form = this.formBuilder.group({
nombre: [''],
apellido: [''],
edad: [''],
ocupacion: [''],
})
destroy$ = new Subject<void>();
constructor(
private readonly service: Service,
private formBuilder: FormBuilder,
) {}
ngOnInit() {
this.service.getSomeData().subscribe(res => {
// manejar respuesta
});
this.service.getSomeOtherData().subscribe(res => {
// Mucha lógica puede ir aquí
});
this.form.get('age').valueChanges.pipe(
map(age => +age),
takeUntil(this.destroy$),
).subscribe(age => {
if (age >= 18) {
// Hacer otras cosas
} else {
// Hacer otras cosas
}
});
this.form.get('ocupacion').valueChanges.pipe(
filter(ocupacion => ['ingeniero', 'doctor', 'actor'].indexOf(occupation) > -1),
takeUntil(this.destroy$),
).subscribe(ocupacion => {
// Haz un poco de trabajo pesado aquí
});
combineLatest(
this.form.get('nombre').valueChanges,
this.form.get('apellido').valueChanges,
).pipe(
debounceTime(300),
map(([nombre, apellido]) => `${nombre} ${apellido}`),
switchMap(nombreCompleto => this.service.getUser(nombreCompleto)),
takeUntil(this.destroy$),
).subscribe(user => {
// Hacer Algo
});
fromEvent(this.buttonRef.nativeElement, 'click').pipe(
takeUntil(this.destroy$),
).subscribe(event => {
// manejar evento
})
}
ngOnDestroy() {
this.destroy$.next();
}
}
Eche un vistazo a este componente. No tiene muchos métodos; en realidad, solo tiene dos ciclos de vida. Pero el método ngOnInit
es, francamente hablando, aterrador. Se suscribe a diferentes eventos de cambio de formulario, desde flujos defromEvent, también carga una gran cantidad de datos. Tiene 40 líneas de código, pero en realidad hemos omitido el contenido de las devoluciones de llamada de subscribe
; con ellos, puede que sean más de 100 líneas, lo que ya va en contra de la mayoría de las pautas de software. Además, normalmente trabajamos con otros métodos y no con ngOnInit
, por lo que necesitaremos un mejor acceso a los otros métodos, pero ahora tendríamos que desplazarnos por todo este lío para llegar a ellos (o cerrar / reabrir ngOnInit cada vez que necesitemos ver eso). Además, encontrar algo dentro del método ngOnInit
en sí se vuelve más difícil porque hay muchos conceptos y tareas mezclados.
Ahora echemos un vistazo a esta versión revisada del mismo componente:
@Component({
selector: 'alguna',
template: 'plantilla =',
})
export class SomeComponent implements OnInit, OnDestroy {
@ViewChild('btn') buttonRef: ElementRef<HTMLButtonElement>;
form = this.formBuilder.group({
nombre: [''],
apellido: [''],
edad: [''],
ocupacion: [''],
})
destroy$ = new Subject<void>();
constructor(
private readonly service: Service,
private formBuilder: FormBuilder,
) {}
ngOnInit() {
this.loadInitialData();
this.setupFormListeners();
this.setupEventListeners();
}
private setupFormListeners() {
this.form.get('edad').valueChanges.pipe(
map(edad => +edad),
takeUntil(this.destroy$),
).subscribe(age => {
if (edad >= 18) {
// hacer alguna cosa
} else {
// hacer alguna cosa
}
});
this.form.get('ocupacion').valueChanges.pipe(
filter(ocupacion => ['ingeniero', 'doctor', 'actor'].indexOf(occupation) > -1),
takeUntil(this.destroy$),
).subscribe(ocupacion => {
// Hacer un poco de trabajo pesado aquí
});
combineLatest(
this.form.get('nombre').valueChanges,
this.form.get('apellido').valueChanges,
).pipe(
debounceTime(300),
map(([nombre, apellido]) => `${nombre} ${apellido}`),
switchMap(nombreCompleto => this.service.getUser(nombreCompleto)),
takeUntil(this.destroy$),
).subscribe(user => {
// Do some stuff
});
}
private loadInitialData() {
this.service.getSomeData().subscribe(res => {
// manejar respuesta
});
this.service.getSomeOtherData().subscribe(res => {
// Mucha de la logica va aqui
});
}
private setupEventListeners() {
fromEvent(this.buttonRef.nativeElement, 'click').pipe(
takeUntil(this.destroy$),
).subscribe(event => {
// handle event
})
}
ngOnDestroy() {
this.destroy$.next();
}
}
La lógica del componente es la misma, pero la forma en que organizamos nuestro código es diferente. Ahora, el método ngOnInit
llama a tres métodos diferentes para cargar los datos iniciales de los servicios, configurar oyentes de cambio de formulario y configurar oyentes de eventos DOM (si es necesario). Después de este cambio, leer el componente desde cero se vuelve más fácil (lea ngOnInit
: comprenda lo que comienza de un vistazo y, si necesita detalles de implementación, visite los métodos correspondientes). Encontrar la fuente de los errores también es relativamente más fácil: si los escuchas de formulario no funcionan correctamente, vaya a setupFormListeners
y así sucesivamente.
No contamine su método ngOnInit- divídalo en partes!
Escribir selectores de directivas inútiles
Las directivas Angular son una herramienta poderosa que nos permite aplicar lógica personalizada a diferentes elementos HTML. Al hacerlo, utilizamos selectores css, lo que en realidad nos da mucho más poder de lo que queremos darnos cuenta. Aquí hay un ejemplo: imagine una directiva que verifica si el formControl del elemento correspondiente tiene errores y le aplica algún estilo; llamémoslo ErrorHighlightDirective. Ahora digamos que le damos un selector de atributos, digamos [errorHighlight]. Funciona bien, pero ahora tenemos que encontrar todos los elementos de formulario con el atributo formControl y poner nuestro[errorHighlight]en ellos, lo cual es una tarea tediosa. Pero, por supuesto, podemos usar el selector de atributos de la directiva [formControl], por lo que nuestra directiva se verá así:
@Directive({
selector: '[formControl],[formControlName]'
})
export class ErrorHighlightDirective {
// implementacion
}
Ahora nuestra directiva se vinculará automáticamente a todos los controles de formulario en nuestro módulo.
Pero el uso no termina ahí. Imagine que queremos aplicar una animación inestable a todos los formControls
del formulario que tienen una clase has-error.
Podemos escribir fácilmente una directiva y vincularla usando un selector de clases: .has-error.
Utilice mejores selectores para sus directivas para evitar saturar su HTML con atributos innecesarios
Lógica dentro de un constructor de servicios
Los servicios son clases y, como tales, tienen un constructor
, que generalmente se usa para inyectar dependencias. Pero a veces los desarrolladores también escriben algún código/lógica de inicialización dentro de él. Y, a veces, esta no es la mejor idea, y este es el motivo.
Imagine un servicio que crea y mantiene una conexión de socket, envía datos al servidor en tiempo real y los envía incluso desde el servidor. Aquí hay una implementación ingenua:
@Injectable()
class SocketService {
private connection: SocketConnection;
constructor() {
this.connection = openWebSocket(); // detalles de implementación omitidos
}
subscribe(eventName: string, cb: (event: SocketEvent) => any) {
this.connection.on(eventName, cb);
}
send<T extends any>(event: string, payload: T) {
this.connection.send(event, payload);
}
}
Este servicio básico crea una conexión de socket y maneja interacciones con ella. ¿Notas algo fuera de lugar?
El problema es que cada vez que se crea una nueva instancia de este servicio, se abre una nueva conexión. ¡Y puede que este no sea el caso que queremos!
En realidad, muchas veces una aplicación usará una conexión de un solo socket, por lo que cuando usemos este servicio dentro de módulos cargados de forma diferida, obtendremos una nueva conexión abierta. Para evitar esto, necesitamos eliminar la lógica de inicialización de este constructor y encontrar otra forma de compartir la conexión entre los módulos cargados de forma diferida. Además, es posible que deseemos tener un método que nos permita recargar la conexión a voluntad (esencialmente, volver a abrirla, por ejemplo, si se cierra inesperadamente):
@Injectable()
class SocketService {
constructor(
private connection: SocketConnection
// la conexión de socket en sí se proporciona en la raíz de la aplicación y es la misma en todas partes
) { }
// manejar para recargar un socket, implementación ingenua
openConnection() {
this.connection = openWebSocket();
}
subscribe(eventName: string, cb: (event: SocketEvent) => any) {
this.connection.on(eventName, cb);
}
send<T extends any>(event: string, payload: T) {
this.connection.send(event, payload);
}
}
Agregar un nuevo estado cuando puede derivarlo del estado existente
Cada componente tiene su estado: un conjunto de propiedades que contienen datos esenciales para representar la interfaz de usuario. El estado es la parte lógica más importante de nuestra aplicación, por lo que manejarlo correctamente tiene grandes beneficios.
El estado puede describirse como original y _derivado _. El estado original se puede describir como datos independientes que existen en sí mismos, - por ejemplo, el estado de inicio de sesión. El estado derivado depende completamente de alguna parte del estado original, - por ejemplo, un aviso de texto que dice "Inicie sesión" si el el usuario está desconectado o "Desconectar" si el usuario ha iniciado sesión. Esencialmente, no necesitamos guardar ese valor de texto en ningún lugar; siempre que lo necesitemos, podemos calcularlo en función del estado de autenticación. Entonces esta pieza de código:
@Component({
selector: 'some',
template: '<button>{{ text }}</button>',
})
export class SomeComponent {
isAuth = false;
text = 'Desconectar';
constructor(
private authService: AuthService,
) {}
ngOnInit() {
this.authService.authChange.subscribe(auth => {
this.isAuth = auth;
this.text = this.isAuth ? 'Desconectar ' : 'Iniciar Session';
});
}
}
se convertira en esto:
@Component({
selector: 'some',
template: `<button>{{ isAuth ? 'Desconectar' : 'Iniciar Session' }}</button>`,
})
export class SomeComponent {
isAuth = false;
constructor(
private authService: AuthService,
) {}
ngOnInit() {
this.authService.authChange.subscribe(auth => this.isAuth = auth);
}
}
Como puedes ver, la propiedad de text fue un estado derivado y era completamente innecesario. Eliminarlo hizo que el código fuera más fácil de leer y razonar.
No cree variables y propiedades independientes para almacenar el estado derivado; calcúlalo siempre que sea necesario
Este puede parecer un poco fácil de detectar, pero cuando se trata de datos cada vez más complejos, incluso los desarrolladores más experimentados a veces cometen este error, especialmente con las transmisiones RxJS. En este artículo, exploro cómo se debe manejar este concepto en las aplicaciones RxJS Angular.
Conclusión
Hay muchos errores que se pueden cometer al escribir una aplicación con Angular. Pero algunos errores son muy comunes y se convierten en patrones, que se re utilizan y abusan. Conocer los más comunes y cómo evitarlos puede ser muy beneficioso para nuestras aplicaciones Angular.
Autor de este post Armen Vardanyan publicado para indepth.dev artículo original en inglés
Posted on November 4, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.