Appearance
Gérer les dépendances circulaires avec forwardRef
Découvrons comment implémenter des composants qui s'appellent mutuellement, particulièrement utile pour afficher des données hiérarchiques comme des commentaires imbriqués.
Un exemple concret: Un système de commentaires
Imaginez un système de commentaires comme sur Reddit :
- Chaque commentaire peut avoir des réponses
- Chaque réponse est elle-même un commentaire
- L'imbrication peut être infinie
- Chaque niveau peut être replié/déplié
Voici comment créer cette structure avec des composants récursifs :
ts
export interface Comment {
id: number;
content: string;
author: string;
replies: Comment[];
level: number;
}
ts
import { Component, Input, forwardRef } from '@angular/core';
import { Comment } from './comment.interface';
import { CommentListComponent } from './comment-list.component';
@Component({
selector: 'app-comment',
standalone: true,
imports: [forwardRef(() => CommentListComponent)],
template: `
<div class="comment" [style.margin-left.px]="level * 20">
<div class="comment-header">
<strong>{{ comment.author }}</strong>
</div>
<div class="comment-content">
{{ comment.content }}
</div>
@if (comment.replies.length > 0) {
<button (click)="toggleReplies()">
{{ showReplies ? 'Masquer' : 'Afficher' }} les réponses
</button>
@if (showReplies) {
<app-comment-list
[comments]="comment.replies"
[level]="level + 1">
</app-comment-list>
}
}
</div>
`
})
export class CommentComponent {
@Input() comment!: Comment;
@Input() level: number = 0;
showReplies = false;
toggleReplies() {
this.showReplies = !this.showReplies;
}
}
ts
import { Component, Input, forwardRef } from '@angular/core';
import { Comment } from './comment.interface';
import { CommentComponent } from './comment.component';
@Component({
selector: 'app-comment-list',
standalone: true,
imports: [forwardRef(() => CommentComponent)],
template: `
@for (comment of comments; track comment.id) {
<app-comment
[comment]="comment"
[level]="level">
</app-comment>
}
`
})
export class CommentListComponent {
@Input() comments: Comment[] = [];
@Input() level: number = 0;
}
ts
import { Component } from '@angular/core';
import { CommentListComponent } from './comment-list.component';
import { Comment } from './comment.interface';
@Component({
selector: 'app-home',
standalone: true,
imports: [CommentListComponent],
template: `
<h2>Commentaires</h2>
<app-comment-list [comments]="comments"></app-comment-list>
`
})
export class HomeComponent {
comments: Comment[] = [
{
id: 1,
author: 'Alice',
content: 'Super article !',
level: 0,
replies: [
{
id: 2,
author: 'Bob',
content: 'Je suis d\'accord !',
level: 1,
replies: [
{
id: 3,
author: 'Alice',
content: 'Merci Bob !',
level: 2,
replies: []
}
]
}
]
}
];
}
Pourquoi utiliser forwardRef
?
Dans notre exemple, nous avons deux composants qui dépendent l'un de l'autre :
CommentComponent
a besoin deCommentListComponent
pour afficher les réponsesCommentListComponent
a besoin deCommentComponent
pour afficher chaque commentaire
Sans forwardRef
, nous obtiendrions une erreur :
ERREUR DE DÉPENDANCE CIRCULAIRE
ERROR TypeError: Cannot read properties of undefined (reading 'ɵcmp')
Cette erreur se produit car Angular ne peut pas résoudre les dépendances circulaires lors de la compilation.
Comment forwardRef
résout le problème
forwardRef
permet de "reporter" la résolution de la dépendance après l'initialisation des composants. Voici comment :
ts
// ❌ Sans forwardRef - Provoque une erreur
@Component({
imports: [CommentListComponent], // Erreur: CommentListComponent n'est pas encore défini
})
// ✅ Avec forwardRef - Fonctionne correctement
@Component({
imports: [forwardRef(() => CommentListComponent)], // La résolution est reportée
})
ts
// ❌ Sans forwardRef - Provoque une erreur
@Component({
imports: [CommentComponent], // Erreur: Dépendance circulaire
})
// ✅ Avec forwardRef - Fonctionne correctement
@Component({
imports: [forwardRef(() => CommentComponent)], // La résolution est reportée
})
forwardRef
crée une fonction qui retournera la référence au composant- Cette fonction n'est évaluée qu'après l'initialisation des deux composants
- Cela brise le cycle de dépendance en reportant la résolution
CONSEIL
Utilisez forwardRef
uniquement quand c'est nécessaire, c'est-à-dire dans les cas de dépendances circulaires. Pour les imports normaux, utilisez la syntaxe standard.
Autres cas d'utilisation
Les dépendances circulaires peuvent apparaître dans d'autres situations :
- Services qui se référencent mutuellement
- Composants parent/enfant récursifs
- Modules qui dépendent l'un de l'autre
Dans tous ces cas, forwardRef
est la solution recommandée par Angular pour résoudre les dépendances circulaires.
ATTENTION À LA PROFONDEUR
Dans un cas réel, il est recommandé de limiter la profondeur d'imbrication pour des raisons de performance et d'expérience utilisateur. Vous pouvez ajouter une vérification :
ts
@if (comment.replies.length > 0 && level < 5) {
<app-comment-list [comments]="comment.replies" [level]="level + 1">
</app-comment-list>
}