Part 2: Stop using @ViewChild

rajesh kumar
3 min readJun 2, 2024

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:

  1. 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.
  2. 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>
  1. Using @ViewChild with {static: true}: You can use the {static: true} option with @ViewChild to get the reference before the ngAfterViewInit hook. However, this approach doesn't work well with change detection and within ngIf or ngFor blocks.

--

--