Part 2: Stop using @ViewChild
In part 1 of this article series, we refactored the code that used @ViewChild
for native element access into an attribute directive.
In this part, we’ll explore how to use multiple directives to compose a feature.
Let’s recall how we created the appCanvas
directive to get the canvas context for drawing. If we need to handle extensive drawing, the appCanvas
directive can become overly congested. Similarly, if we use an appGoogleMap
directive to create various elements on a map, such as markers or shapes, the map directive can become bloated in no time.
To prevent this and create more maintainable, composable, and loosely coupled code, we can create multiple directives. The main directives (appCanvas
, appGoogleMap
) can be used as dependencies and provided to the feature directives.
Here’s a code example demonstrating how to create circle and rectangle drawings using the appCanvas directive:
// Get canvas context
@Directive({
selector: '[appCanvas]'
})
export class CanvasDirective {
canvasContext: CanvasRenderingContext2D | null;
constructor(elementRef: ElementRef<HTMLCanvasElement>) {
const canvas = elementRef.nativeElement;
this.canvasContext = canvas.getContext('2d');
}
}
// Cirecle drawing
@Directive({
selector: '[appCircle]'
})
export class CircleDirective {
@Input() set appCircle(config: { x: number, y: number, r: number }) {
this.canvasDirective.canvasContext?.beginPath();
this.canvasDirective.canvasContext?.arc(config.x, config.y, config.r, 0, 2 * Math.PI);
this.canvasDirective.canvasContext?.stroke();
}
constructor(private canvasDirective: CanvasDirective) { }
}
// Rectangle drawing
@Directive({
selector: '[appRectangle]'
})
export class RectangleDirective {
@Input() set appRectangle(config: { x: number, y: number, w: number, h: number }) {
this.canvasDirective.canvasContext?.beginPath();
this.canvasDirective.canvasContext?.rect(config.x, config.y, config.w, config.h);
this.canvasDirective.canvasContext?.stroke();
}
constructor(private canvasDirective: CanvasDirective) { }
}
// Using canvas directive with cirecle and rectangle directives to draw shapes
@Component({
selector: 'app-drawing',
template: `<canvas appCanvas [appCircle]='{x: 100, y: 100, r: 20}' [appRectangle]='{x: 50, y: 50, h: 100, w: 100}'></canvas>`,
})
export class DrawingComponent { }
Conclusion
- Whenever possible, use attribute directives instead of
@ViewChild
to access native elements. Wrap the API (such as map, canvas, etc.) within the directive for better encapsulation and maintainability. - For complex features, create multiple directives to keep the codebase modular, composable, and loosely coupled.
- By separating different functionalities into distinct directives, you adhere to the Single Responsibility Principle, making each piece of your application easier to manage and understand.
- This approach enhances the reusability of your components. You can remove or modify elements without breaking the entire functionality, as the feature directives are independent.
- Avoid relying on lifecycle hooks like
ngAfterViewInit
for element references, making your directives simpler and more efficient.
Addressing the Comments
This article stems from real-life refactoring experience where complex drawing and calculations were happening on multiple canvases within a single component. The resulting component became a mammoth, exceeding 1000 lines of code. Here are some responses to comments from various forums and previous articles:
- Title Concerns: Some felt the title was clickbait. While I don’t advocate completely stopping the use of
@ViewChild
, most of the time, you can manage without it. - Accessing Components: Sometimes,
@ViewChild
is used to access components and call their functions. You can avoid this by using template reference variables and passing them inside function calls directly in the template.
<app-form #myForm></app-form>
<button (click)="myForm.submit()")></button>
- Using
@ViewChild
with{static: true}
: You can use the{static: true}
option with@ViewChild
to get the reference before thengAfterViewInit
hook. However, this approach doesn't work well with change detection and withinngIf
orngFor
blocks.