object-recipes: Angular + NgRx example
May 1, 2026 ยท View on GitHub
This example was created on Angular v20.3.0, and NgRx v20.0.1.
/* src/state/person.ts */
import { createReducer, createAction, on, props } from '@ngrx/store';
import { entity, recipeSafe, Recipe, Shape, ShapeSafe } from 'object-recipes';
export const personEntity = entity({
name: 'John Doe',
age: 20,
address: {
street: 'Some street 99',
zip: 1000,
country: 'Some country',
},
});
export type Person = ShapeSafe<typeof personEntity>;
export const setNameAgeAction =
(
values: Partial<Pick<Shape<typeof personEntity>, 'name' | 'age'>>
): Recipe<typeof personEntity> =>
(entity) =>
entity.set(values);
export const setAddressAction =
(
values: Partial<Shape<typeof personEntity>['address']>
): Recipe<typeof personEntity> =>
(entity) =>
entity.set({
address: { ...entity.get().address, ...values },
});
export const action = createAction(
'[personEntity] recipe',
props<{ recipe: Recipe<typeof personEntity> }>()
);
export const personStoreReducer = createReducer(
personEntity.getSafe(),
on(action, (state, { recipe }) => recipeSafe(recipe)(state))
);
/* src/state/index.ts */
import { provideStore, provideState } from '@ngrx/store';
import { Person, personStoreReducer } from './person';
export interface StoreState {
person: Person;
}
export const stateProvider = [
provideStore<StoreState>(),
provideState('person', personStoreReducer),
];
/* src/app/app.config.ts */
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { stateProvider } from './store';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
...stateProvider,
],
};
/* src/person.ts */
/* eslint-disable import/no-deprecated */
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Store } from '@ngrx/store';
import { StoreState } from '../store';
import { action, setNameAgeAction, setAddressAction } from '../store/person';
@Component({
selector: 'person-ngrx-component',
imports: [CommonModule, FormsModule],
template: `
<h1>Person: NgRx</h1>
Hello! {{ name$ | async }}
<br />
{{ address$ | async | json }}
<br /><br />
Name:
<input
type="text"
[ngModel]="name$ | async"
(ngModelChange)="setName($event)"
/><br />
Age:
<input
type="number"
[ngModel]="age$ | async"
(ngModelChange)="setAge($event)"
/><br />
<br />
<br />
<b>Address</b>
<br />
Street:
<input
type="text"
[ngModel]="(address$ | async)?.street"
(ngModelChange)="setAddress({ street: $event })"
/><br />
Zip:
<input
type="text"
[ngModel]="(address$ | async)?.zip"
(ngModelChange)="setAddress({ zip: $event })"
/><br />
Country:
<input
type="text"
[ngModel]="(address$ | async)?.country"
(ngModelChange)="setAddress({ country: $event })"
/><br />
`,
})
export class PersonNgRxComponent {
store: Store<StoreState> = inject(Store<StoreState>);
name$ = this.store.select(({ person }) => person.name);
age$ = this.store.select(({ person }) => {
const age = person.age;
return isNaN(age) ? '' : age.toString();
});
address$ = this.store.select(({ person }) => person.address);
setName(name: string) {
this.store.dispatch(
action({
recipe: setNameAgeAction({ name }),
})
);
}
setAge(age: string) {
this.store.dispatch(
action({
recipe: setNameAgeAction({ age: parseInt(age) }),
})
);
}
setAddress(address: Parameters<typeof setAddressAction>[0]) {
this.store.dispatch(
action({
recipe: setAddressAction(address),
})
);
}
}