💎 NgRx: deepComputed
Did you know you can easily create computed signals for each nested property of an object literal using the deepComputed function?
💎 NgRx: deepComputed

Did you know you can easily create computed signals for each nested property of an object literal using the deepComputed function?
NgRx Signals is a state management solution that comes in two flavours: SignalState and SignalStore. When you create a state container, a signal is created for each property of the state object:
import { signalState } from "@ngrx/signals";
interface CustomersState {
customers: Customer[];
query: string;
}
const customersState = signalState<CustomersState>({
customers: [],
query: '',
});
// (property) customers: Signal<Customer[]>
// customersState.customers
// (property) query: Signal<string>
// customersState.query
import { signalStore, withState } from '@ngrx/signals';
interface CustomersState {
customers: Customer[];
query: string;
}
const CustomersStore = signalStore(
withState<CustomersState>({
customers: [],
query: '',
}),
);
@Component({...})
class Customers {
private store = inject(CustomersStore);
constructor() {
// (property) customers: Signal<Customer[]>
// this.store.customers
// (property) query: Signal<string>
// this.store.query
}
}
This is great because consumers (for example, a component template or an effect) can subscribe to a specific producer (the signal for a given property) instead of the full state object, which improves performance. However, sometimes you either can’t or don’t want to adopt NgRx Signals directly, but you still want fine‑grained signals derived from a single initial state object.
Consider a simple service with state; here you can only observe changes on the whole state (the user object):
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserStore {
private readonly _user = signal({
name: 'Bob',
address: {
street: '',
zipCode: '',
city: '',
},
note: '',
title: '',
salary: 0,
});
readonly user = this._user.asReadonly();
...
}
Of course, you can manually create computed signals for each property, but that becomes cumbersome:
import { computed, Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserStore {
private readonly _user = signal({
name: 'Bob',
address: {
street: '',
zipCode: '',
city: '',
},
note: '',
title: '',
salary: 0,
});
readonly name = computed(() => this._user().name);
readonly address = computed(() => this._user().address);
// and so on...
}
Instead, you can use the deepComputed utility to create a signal-based representation of the initial state object:
import { Injectable, signal } from '@angular/core';
import { deepComputed } from '@ngrx/signals';
@Injectable({ providedIn: 'root' })
export class UserStore {
private readonly _user = signal({
name: 'Bob',
address: {
street: '',
zipCode: '',
city: '',
},
note: '',
title: '',
salary: 0,
});
readonly user = deepComputed(() => this._user());
...
}
@Component({...})
class User {
private store = inject(UserStore);
constructor() {
// (property) name: Signal<string>
// this.store.user.name
// (property) address: DeepSignal<{
// street: string;
// zipCode: string;
// city: string;
// }>;
// this.store.user.address
// (property) street: Signal<string>
// this.store.user.address.street
}
}
Another scenario where deepComputed is useful is when you already have a well-established state management solution that does not rely on signals, such as ComponentStore. You may not want to refactor to SignalStore but still want to benefit from representing state with signals. You can use selectSignal for each state slice, but similar to manually creating computed signals, it becomes verbose:
import { ComponentStore } from '@ngrx/component-store';
export interface PhotoState {
photos: Photo[];
search: string;
page: number;
pages: number;
loading: boolean;
error: unknown;
}
@Injectable()
export class PhotoStore extends ComponentStore<PhotoState> {
...
readonly photos = this.selectSignal((s) => s.photos);
readonly search = this.selectSignal((s) => s.search);
// and so on...
}
Here again, the deepComputed utility helps. You can select the whole state and then use deepComputed to turn it into an object with fine-grained signals:
import { ComponentStore } from '@ngrx/component-store';
import { deepComputed } from '@ngrx/signals';
export interface PhotoState {
photos: Photo[];
search: string;
page: number;
pages: number;
loading: boolean;
error: unknown;
}
@Injectable()
export class PhotoStore extends ComponentStore<PhotoState> {
...
private readonly _vm = this.selectSignal((state) => ({
...state,
endOfPage: state.page === state.pages,
}));
readonly vm = deepComputed(() => this._vm());
}
@Component({...})
class Photos {
private store = inject(PhotoStore);
constructor() {
// (property) photos: Signal<Photo[]>
// this.store.vm.photos
// (property) search: Signal<string>
// this.store.vm.search
}
}
Last but not least, let us revisit the CustomersStore signal store and add a computed signal:
import { signalStore, withComputed, withState } from '@ngrx/signals';
import { computed } from '@angular/core';
const CustomersStore = signalStore(
withState<CustomersState>({
customers: [],
query: '',
}),
withComputed(({ customers }) => ({
statistics: computed(() => ({
totalCustomers: customers().length,
regularCustomers: customers().filter((c) => c.status === 'regular')
.length,
premiumCustomers: customers().filter((c) => c.status === 'premium')
.length,
vipCustomers: customers().filter((c) => c.status === 'vip').length,
})),
})),
);
@Component({...})
class Customers {
private store = inject(CustomersStore);
constructor() {
// (property) statistics: Signal<{
// totalCustomers: number;
// regularCustomers: number;
// premiumCustomers: number;
// vipCustomers: number;
// }>
// this.store.statistics
// Property totalCustomers does not exist on type:
// this.store.statistics.totalCustomers ❌
}
}
Unlike signals from state, computed signals do not follow the “signal per property” model. Here, you can only consume the statistics signal as a whole. To align it with state-based signals, you can wrap the computed result with deepComputed so that each property (like totalCustomers) becomes its own signal:
import { deepComputed, signalStore, withComputed, withState } from '@ngrx/signals';
const CustomersStore = signalStore(
withState<CustomersState>({
customers: [],
query: '',
}),
withComputed(({ customers }) => ({
statistics: deepComputed(() => ({ 🚨🚨🚨
totalCustomers: customers().length,
regularCustomers: customers().filter((c) => c.status === 'regular')
.length,
premiumCustomers: customers().filter((c) => c.status === 'premium')
.length,
vipCustomers: customers().filter((c) => c.status === 'vip').length,
})),
})),
);
@Component({...})
class Customers {
private store = inject(CustomersStore);
constructor() {
// (property) totalCustomers: Signal<number>
// this.store.statistics.totalCustomers ✅
}
}
Although, the deepComputed helper is tree‑shakeable, as always you should weigh the trade‑off of adding yet another dependency to your project.
I hope you liked the “golden nugget” 💎 , thanks for reading! 🙂
메타데이터
- post_id
- 5ea017a90741
- slug
- ngrx-deepcomputed-5ea017a90741
- url
- https://medium.com/javascript-everyday/ngrx-deepcomputed-5ea017a90741
- canonical_url
- https://medium.com/javascript-everyday/ngrx-deepcomputed-5ea017a90741
- author_url
- https://medium.com/@wojtrawi
- status
- ok
- fetched_at
- 2026-06-14 11:28:49