Table of Contents
- Intro
- TLDR;
- What is a computed property?
- Computed properties in Angular
- Summary
Intro
Many frameworks and libraries have built-in support for computed properties.
For example Svelte:
1let firstName = „Chris“;2let lastName = „Kohler“;3$: fullName = `${firstName} ${lastName}`;
Or Vue:
1computed: {2 fullName() {3 return `${this.firstName} ${this.lastName}`4 }5}
Or MobX:
1class TodoList {2 @observable firstName = „Chris“3 @observable lastName = „Kohler“45 @computed6 get fullName() {7 return `${firstName} ${lastName}`;8 }9}
So if you are coming from a framework that has a computed keyword, you might be wondering how to define computed properties in Angular.
TLDR;
There is no computed keyword or decorator in Angular. But there are many ways how to define computed properties in Angular. If you are already using observables, just use pipe(map)
. Otherwise, getters
and pipes
are a good way to define computed properties.
What is a computed property?
A computed property is a value that is derived from state.
For example, if you have a spreadsheet with a list of purchases and one line with the total cost, the purchases are state and the total cost is derived state.
The advantage of defining computed properties is that when you change the state, the computed property is automatically updated. In this example when you change the cost of an item or add/remove an item, the total will be updated automatically.
Computed properties in Angular
There is no computed keyword or decorator in Angular. But that doesn‘t mean it is not possible to define computed properties. In fact, there are multiple ways to define them. Let‘s have a look at them.
We go through the following ways:
- ngOnChanges
- getters
- pipe
- RxJS
- ngrx/component-store
Computed properties in “dumb” / presentational components
In this post, I focus on “dumb” components with computed properties. That’s where I most often use those patterns. For smart / container components I often use a state management library with built-in support for computed properties. That said, all the approaches can also be used for container components.
Example App
We use this simple interface for all the examples:
1interface Todo {2 name: string;3 done: boolean;4}
In the examples, we implement the todo-stats
component with the different approaches. The component is then used in the AppComponent as shown below:
1@Component({2 template: `3 <!-- Display: "Todos done: 5" -->4 <todo-stats [todos]="todos"></todo-stats>5 `,6 selector: "my-app"7})8export class AppComponent {9 todos: Todo[] = [10 { name: "shop", done: false },11 { name: "clean", done: true },12 { name: "travel", done: true }13 ];14}
Also, all implementations can use this helper function to filter todos by the status “done”:
1function byDone(todo: Todo): boolean {2 return todo.done;3}
Approach with ngOnChanges
1@Component({2 template: `3 Todos done: {{ done }}4 `,5 selector: "todo-stats"6})7export class TodoStatsComponent implements OnChanges {8 @Input() todos: Todo[] = [];910 done = 0;1112 ngOnChanges(): void {13 this.done = this.todos.filter(byDone).length;14 }15}
ngOnChanges
is triggered when an @Input
changes. Whenever this happens we make sure to update the done count.
This approach is simple and easy to understand. But only for small examples like this one. When the component grows, it is more difficult to see how the done
property depends on ngOnChanges
. This approach can also be error-prone since we have to always remember to update the done
property whenever we change the todos
. I try to avoid this approach for derived state. That said, in cases where I have many inputs it can be a good option to orchestrate.
- ✅ Simple and easy to understand
- ✅ Good for multiple inputs
- ⚠️ Can be error-prone with a more complex components
Approach with a getter
1@Component({2 template: `3 Todos done: {{ done }}4 `,5 selector: "todo-stats"6})7export class TodoStatsComponent {8 @Input() todos: Todo[] = [];910 get done() {11 return this.todos.filter(byDone).length;12 }13}
Compared to the ngOnChanges, this approach has a few advantages. First, we see directly how done
is derived. Second, we never have to remember to update the done
property when changing the todos
.
This approach is very close to how we write formulas in a spreadsheet. One downside is, that every time the component is rerendered, the done
property is recomputed. In this example, it is fine if the component is only rerendered when the todos
array changes. This can be easily achieved by changing the change detection to OnPush
.
I like the readability of getters and use is often for components where I don’t have any observables and only 1-2 inputs.
What about perfomance? You might think that this approach is bad for performance because there is no caching. That’s true, there is no caching. But in components with 1-2 inputs it is very likely that you anyway always have to update the derived property. Caching also adds some overhead and is not generally necessary for every computation.
- ✅ Derived state is clearly defined
- ✅ Getters is a fundamental language feature, no new concept to learn
Approach with an Angular Pipe
1@Pipe({ name: "numberOfDoneTodos" })2export class DoneTodosPipe implements PipeTransform {3 transform(todos: Todo[]): number {4 return todos.filter(byDone).length;5 }6}78@Component({9 template: `10 Todos done: {{ todos | numberOfDoneTodos }}11 `,12 selector: "todo-stats"13})14export class TodoStatsComponent {15 @Input() todos: Todo[] = [];16}
I like this approach for the very clear separation of concerns. Another plus is that (pure) pipes are cached by default. That means that property is no recomputed if the input didn’t change
- ✅ Clear separation
- ✅ Build in cache (pure pipes)
Approach with a generic Angular Pipe
The approach with an Angular pipe has the disadvantage that we need to write a new pipe for every filter. This results in quite some boilerplate.
Another option is to create a generic FilterPipe
:
1@Pipe({ name: 'filter' })2export class FilterPipe implements PipeTransform {3 transform(array: any[], predicate: (value: any) => any): any {4 return array.filter(predicate);5 }6}
This pipe can then be used with any filter predicate:
1@Component({2 template: `3 Todos done: {{ (todos | filter: byDone).length }}4 `,5 selector: 'todo-stats'6})7export class TodoStatsComponent {8 @Input() todos: Todo[] = [];910 byDone = (todo: Todo) => todo.done11}
Thank you @NetanelBasal for the inspiration. I really like the idea. Keeps the component code very readable.
- ✅ Readability
- ✅ Build in cache (pure pipes)
- ✅ Can be combined with other pipes
Approach with RxJS
1@Component({2 template: `3 Todos done: {{ done$ | async }}4 `,5 selector: "todo-stats"6})7export class TodoStatsComponent {8 @Input() set todos(todos: Todo[]) {9 this.todos$.next(todos);10 }1112 todos$ = new BehaviorSubject<Todo[]>([]);1314 done$ = this.todos$.pipe(15 map(todos => todos.filter(byDone).length)16 );17}
The reactive approach. This is my go-to solution if I am already in the RxJS world. Very powerful with many operators out of the box. For our simple example it might be overengineered but for more complex logic RxJS helps a lot to write clean code.
- ✅ Recommended when part of the state is already in the RxJS world (observables)
- ✅ Very powerful with many operators out of the box
Approach with ngrx/component-store
1@Component({2 template: `3 Todos done: {{ todosDone$ | async }}4 `,5 selector: "todo-stats",6 providers: [ComponentStore]7})8export class TodoStatsComponent {9 @Input() set todos(todos: Todo[]) {10 this.todosStore.setState(todos);11 }1213 todosDone$ = this.todosStore.select(14 todos => todos.filter(byDone).length15 );1617 constructor(18 protected todosStore: ComponentStore<Todo[]>19 ) {}20}
Bonus approach. I see this approach as a variant of the RxJS approach. If you often use the RxJS approach, using the ngrx/component-store
library can be a nice addition. ComponentStore is a standalone library that helps to manage local/component state and comes with methods like setState
or select
.
You can start with a generic store like I do in this example. When you see the component gets too complex, it is easy to extract the store and define selectors in the store instead of in the component itself.
- ✅ Recommended as an alternative to the RxJS approach
- ✅ Nice refactor options
- ✅
select
is in my opinion more readable thanpipe(map)
Summary
There are many ways how to define computed properties in Angular. There is no right or wrong. I use almost all the approaches in my apps, often many approaches in the same app.
Here is how I choose between the approaches.
- If I am not using observables I either use the
getter
orpipe
approach. - If I am already using observables in a component, I stay in the RxJS world with either pure RxJS or ngrx/component-store.
- I rarely use
ngOnChanges
to update derived state.
Do you have another approach? Let me know on Twitter
Spread the word
Did you like the article? Please spread the word 🙌 and follow me on Twitter for more posts on web technologies.
Did you find typos 🤓? Please help improve the blogpost and open an issue here or post your feedback here