Published 10-25-2023
export class FineGrainComponent {
hideCompleted = signal(false);
// → Easy to setup, no additional libraries ✅
todoList = signal<Todo[]>([
{ title: "Write article", complete: signal(true) },
{ title: "????", complete: signal(false) },
{ title: "Profit!!!", complete: signal(false) },
]);
// → computed reacts to nested signal ✅
filteredList = computed(() =>
!this.hideCompleted()
? this.todoList()
: this.todoList().filter((t) => !t.complete()),
);
updateTodo(complete: WritableSignal<boolean>) {
// → clean & efficient array updates ✅
complete.update((c) => !c);
}
}
This is a todo list component. Notice the complete
property. It’s a signal, inside of a signal. Nested signals (fine-grain reactivity) can unlock some interesting patterns. Such as simplifying updates to arrays & lists.
For a simple todo component this creates a really convenient and interesting approach to reactivity.
This may not practically scale for all scenarios. However, frameworks like SolidJS implement conveniences to make nested signals practical. It would be exciting to see if the Angular team is considering adding solutions like SolidJS’s store pattern. As they’ve been working with the creator of SolidJS on signals.
If you’re not familiar with Angular signals see these previous articles
For complete code & styling, see 🚀 Full Source Code
hideCompleted
is a signal powered toggle. When activated, the computed signal will filter out the todos with complete: true
.
Since complete()
is defined in our computed
any update to each individual complete
will rebuild our filteredList
.
hideCompleted = signal(false);
toggleCompleted() {
this.hideCompleted.update(c => !c)
}
filteredList = computed(() => !this.hideCompleted() ?
this.todoList() :
this.todoList().filter(t => !t.complete())
)
<label for="switch">
<input
type="checkbox"
name="switch"
role="switch"
[checked]="hideCompleted()"
(change)="toggleCompleted()"
/>
Hide Complete
</label>
We’ll iterate over our computed
signal to build our todo list. Notice how we type, access, and set the complete
value.
updateTodo(complete: WritableSignal<boolean>) {
complete.update(c => !c)
}
<!-- Iterate over computed & access nested signal via `()` -->
<div *ngFor="let todo of filteredList()">
<label for="checkbox">
<input
type="checkbox"
name="checkbox"
[checked]="todo.complete()"
(change)="updateTodo(todo.complete)"
/>
{{todo.title}}
</label>
</div>
Each complete
is bound to a checkbox. Using nested signals:
computed
addTodo(title: string) {
this.todoList.update(v => [...v, {title, complete: signal(false)}])
}
<form (ngSubmit)="addTodo(title.value)">
<input #title type="text" name="addtodo" />
<button type="submit">+Add Todo</button>
</form>
To create a new todo, we update our source signal with a new object. We create another signal, nested inside of that object.
In a framework like SolidJS, the entire list won’t have to re-render when the complete
property changes. Only the row with the changed complete
re-renders. Hence, “fine-grain reactivity”.
From my testing, this didn’t seem to be the case with Angular. It seemed like the loop was still re-rendered. But, I could have made a mistake in my testing or needed to implement trackBy
a certain way to make this work.
For a small simple component this provides some interesting DX
complete
It would be interesting to see if the Angular team creates conveniences around this (Like SolidJS Store).
import { Component, WritableSignal, computed, signal } from "@angular/core";
interface Todo {
title: string;
complete: WritableSignal<boolean>;
}
@Component({
selector: "app-fine-grain",
templateUrl: "./fine-grain.component.html",
styleUrls: ["./fine-grain.component.css"],
})
export class FineGrainComponent {
hideCompleted = signal(false);
todoList = signal<Todo[]>([
{ title: "Refactor entire app", complete: signal(true) },
{ title: "????", complete: signal(false) },
{ title: "Profit!!!", complete: signal(false) },
]);
filteredList = computed(() =>
!this.hideCompleted()
? this.todoList()
: this.todoList().filter((t) => !t.complete()),
);
updateTodo(complete: WritableSignal<boolean>) {
complete.update((c) => !c);
}
toggleCompleted() {
this.hideCompleted.update((c) => !c);
}
addTodo(title: string) {
this.todoList.update((v) => [...v, { title, complete: signal(false) }]);
}
}
Read the rest of the series here.
Enjoyed this article? Content similar to this is available on Flotes as studyable Notebooks. Information is delivered like Anki-style flashcards. Allowing you to fill in blanks and evaluate difficultly, to maximize learning efficiency.