You’ve probably faced this situation before – you’re adding a new feature to your app, and you realize you need the same logic you already wrote somewhere else. Copy-pasting feels quick, but it soon leads to duplicated code and harder maintenance. In state management, the way to avoid this problem is with custom store features. They allow you to define shared functionality once and then apply it seamlessly to multiple stores.
To see how this works in practice, imagine building an Angular app to track players and teams in a competitive game.
You’ll need:
- Player store – stores player’s name and role (mid, jungle, top, support, adc).
- Team store – stores the team name and number of wins.
- Shared feature – both players and teams have a rank (bronze, silver, gold…).
Instead of duplicating the rank logic in both stores, we can pull it out into a custom feature and just reuse it.
Create the shared feature
// with-rank.feature.ts
import { signalStoreFeature, withState, withComputed } from '@ngrx/signals';
import { computed, Signal } from '@angular/core';
export type Rank = 'bronze' | 'silver' | 'gold';
export interface RankState {
rank: Rank;
}
// reusable feature
export function withRank() {
return signalStoreFeature(
withState<RankState>({ rank: 'bronze' }),
withComputed(({ rank }) => ({
isGold: computed(() => rank() === 'gold'),
}))
);
}
// helper functions to patch state easily
export function setGold(): RankState {
return { rank: 'gold' };
}
export function setSilver(): RankState {
return { rank: 'silver' };
}
export function setBronze(): RankState {
return { rank: 'bronze' };
}
Here’s what we got:
- rank state
- Helpers like
setGold
,setSilver
andsetBronze
to update state. - A computed property
isGold
we can use in templates.
Player store
// player.store.ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
import { withRank, setGold } from './with-rank.feature';
type Role = 'mid' | 'jungle' | 'top' | 'adc' | 'support';
export interface PlayerState {
name: string;
role: Role;
}
export const PlayerStore = signalStore(
withState<PlayerState>({ name: 'Faker', role: 'mid' }),
withRank(),
withMethods((store) => ({
promoteToGold() {
patchState(store, setGold());
}
}))
);
Our player now has:
- name,
- role,
- and thanks to
withRank
→ also a rank.
Team store
// team.store.ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
import { withRank, setGold } from './with-rank.feature';
export interface TeamState {
teamName: string;
wins: number;
}
export const TeamStore = signalStore(
withState<TeamState>({ teamName: 'T1', wins: 0 }),
withRank(),
withMethods((store) => ({
promoteTeamToGold() {
patchState(store, setGold());
}
}))
);
The team has:
- teamName,
- wins,
- and again, a rank.
Using the PlayerStore
in a component
// player-card.component.ts
import { Component, inject } from '@angular/core';
import { PlayerStore } from './player.store';
@Component({
selector: 'app-player-card',
template: `
<div class="card">
<h2>{{ playerStore.name() }} — {{ playerStore.role() }}</h2>
<p>Rank: <strong>{{ playerStore.rank() }}</strong></p>
@if (playerStore.isGold()) {
<p>🏅 This player is Gold!</p>
}
<button (click)="playerStore.promoteToGold()">Promote to Gold</button>
</div>
`,
})
export class PlayerCardComponent {
playerStore = inject(PlayerStore);
}
Using the TeamStore
in a component
// team-card.component.ts
import { Component, inject } from '@angular/core';
import { TeamStore } from './team.store';
@Component({
selector: 'app-team-card',
template: `
<div class="card">
<h2>{{ teamStore.teamName() }}</h2>
<p>Wins: {{ teamStore.wins() }}</p>
<p>Rank: <strong>{{ teamStore.rank() }}</strong></p>
@if (teamStore.isGold()) {
<p>🏆 This team is Gold!</p>
}
<button (click)="teamStore.promoteTeamToGold()">Promote Team to Gold</button>
</div>
`,
})
export class TeamCardComponent {
teamStore = inject(TeamStore);
}
Here, we inject our PlayerStore
or TeamStore
, display the relevant info, and allow a one-click promotion to gold (if only ranking up in games was this easy 😂).
What did we gain?
By extracting rank into a custom feature, we avoided duplicating logic across multiple stores. Both players and teams share the same feature, and we can extend it in the future if needed.
Think of custom store features like power-ups in a game, you create them once and then use them wherever you need that extra boost.
✅ Cleaner code
✅ Reusable state & methods
✅ Less duplication
And the best part: your stores stay lean and focused on their specific responsibilities.
Top comments (0)