Angular 20. I need to store a list of settings in ...
Created on: December 14, 2025
Created on: December 14, 2025
Angular 20. I need to store a list of settings in an NgRx signal store. The interface representing the setting is the following:
interface Setting {
SiteSubdomain: string;
Name: string;
Value: string;
Description: string;
SettingId: SettingId;
}
The store should record timestamped settings:
interface TimestampedSetting {
timeStamp: Date;
setting: Setting;
}
The store acts as a cache, with an expiration period of 10 minutes per setting, hence it storing TimestampedSetting objects
The store should expose a method that receives a list of SettingId ids, it checks the store if the corresponding settings are available (in store and not expired) and it returns a list of SettingId s that are not available.
The signal store should expose a method that allows storing a list of Setting objects. This method should transform the list of Setting objects into one of TimestampedSetting, patching in the current timestamp into all involved settings.
The signal store should expose a method that allows retrieving a setting via its SettingId.
Please provide a SignalStore implementation for this usecase.
Below is a concise NgRx Signal Store that fulfils the requirements.
It uses the “functional-store” API (signalStore) that ships with NgRx v17 and later (and therefore works in Angular 20).
ts// settings.store.ts --------------------------------------------------------- import { signalStore, withState } from '@ngrx/signals'; import { inject } from '@angular/core'; /* -------------------------------------------------------------------------- */ /* Types */ /* -------------------------------------------------------------------------- */ export type SettingId = string; // <-- change if your ID is not a string export interface Setting { SiteSubdomain: string; Name: string; Value: string; Description: string; SettingId: SettingId; } export interface TimestampedSetting { timeStamp: Date; setting: Setting; } /* -------------------------------------------------------------------------- */ /* Constants */ /* -------------------------------------------------------------------------- */ const TEN_MINUTES_MS = 10 * 60 * 1_000; /* -------------------------------------------------------------------------- */ /* Helpers */ /* -------------------------------------------------------------------------- */ function isExpired(ts: Date): boolean { return Date.now() - ts.getTime() > TEN_MINUTES_MS; } /* -------------------------------------------------------------------------- */ /* Store */ /* -------------------------------------------------------------------------- */ export const SettingsStore = signalStore( { providedIn: 'root' }, /* ---------- 1. State: map(id -> TimestampedSetting) ------------------ */ withState<{ entities: Record<SettingId, TimestampedSetting> }>({ entities: {}, }), /* ---------- 2. Updater: cache incoming settings ----------------------- */ (store) => ({ /** Upsert a list of Setting objects (they are stamped automatically). */ upsertSettings(settings: Setting[]) { store.update((state) => { const now = new Date(); const updated: Record<SettingId, TimestampedSetting> = { ...state.entities }; for (const s of settings) { updated[s.SettingId] = { timeStamp: now, setting: s, }; } return { entities: updated }; }); }, }), /* ---------- 3. Selectors + methods ------------------------------------ */ (store) => ({ /** * @returns the cached setting (undefined if missing or expired) */ selectSetting(id: SettingId) { const entry = store.state().entities[id]; if (!entry || isExpired(entry.timeStamp)) return undefined; return entry.setting; }, /** * Given a list of ids, returns the subset that is * ① missing from the cache OR ② expired (>10 min old). */ missingSettingIds(ids: SettingId[]): SettingId[] { const { entities } = store.state(); return ids.filter((id) => { const hit = entities[id]; return !hit || isExpired(hit.timeStamp); }); }, }) );
ts@Component({...}) export class MyCmp { private settingsStore = inject(SettingsStore); ngOnInit() { // 1️⃣ Ask the cache which IDs we still need from the server const desiredIds = ['S1', 'S2', 'S3']; const notInCache = this.settingsStore.missingSettingIds(desiredIds); // 2️⃣ Fetch those from your API … // (pseudo code) // this.api.getSettings(notInCache).subscribe(fetched => { // this.settingsStore.upsertSettings(fetched); // }); // 3️⃣ Read a single setting anywhere you need it const maybeSetting = this.settingsStore.selectSetting('S1'); } }
isExpired, ensuring that every read correctly invalidates stale entries.upsertSettings creates a new entities map, so Angular’s change detection picks it up instantly.upsert*, missingIds*, select*), matching the cache nature of the store.interface TimestampedSetting extends Setting {
timeStamp: Date;
}
What if I use the above structure and I try to go with withEntities?
Here’s a compact yet fully-typed Signal Store that uses withEntities with your new
TimestampedSetting model:
ts// settings.store.ts --------------------------------------------------------- import { inject } from '@angular/core'; import { signalStore, patchState, type, // tiny helper to reference a generic type } from '@ngrx/signals'; import { withEntities, entityConfig, upsertEntities, } from '@ngrx/signals/entities'; /* -------------------------------------------------------------------------- */ /* Models */ /* -------------------------------------------------------------------------- */ export type SettingId = string; export interface Setting { SiteSubdomain: string; Name: string; Value: string; Description: string; SettingId: SettingId; } export interface TimestampedSetting extends Setting { timeStamp: Date; } /* -------------------------------------------------------------------------- */ /* Entity configuration */ /* -------------------------------------------------------------------------- */ const TEN_MINUTES = 10 * 60 * 1_000; const settingEntity = entityConfig({ entity: type<TimestampedSetting>(), selectId: (s: TimestampedSetting) => s.SettingId, // use SettingId as PK }); /* -------------------------------------------------------------------------- */ /* Private helpers */ /* -------------------------------------------------------------------------- */ function isExpired(d: Date): boolean { return Date.now() - d.getTime() > TEN_MINUTES; } /* -------------------------------------------------------------------------- */ /* Store */ /* -------------------------------------------------------------------------- */ export const SettingsStore = signalStore( { providedIn: 'root' }, // Adds ids(), entityMap(), entities(), addEntities(), upsertEntities()… withEntities(settingEntity), ([ngrx.io](https://ngrx.io/guide/signals/signal-store/entity-management?utm_source=chatgpt.com)) // Custom cache-specific API withMethods((store) => ({ /** * Save/refresh settings in a single shot. * All items get the *same* “now” timestamp so they expire together. */ upsertSettings(raw: Setting[]): void { const now = new Date(); const stamped = raw.map<TimestampedSetting>((s) => ({ ...s, timeStamp: now })); // tree-shakeable updater provided by @ngrx/signals/entities patchState(store, upsertEntities(stamped, settingEntity)); ([ngrx.io](https://ngrx.io/api/signals/entities/upsertEntities?utm_source=chatgpt.com)) }, /** * From a list of IDs, return those that are **missing OR stale (>10 min)**. * Ideal for figuring out which settings still need to be requested. */ missingSettingIds(ids: SettingId[]): SettingId[] { const map = store.entityMap(); // Signal ➜ plain object return ids.filter((id) => { const hit = map[id]; return !hit || isExpired(hit.timeStamp); }); }, /** * Get the current (non-expired) value of a single setting, * or `undefined` if absent / stale. */ getSetting(id: SettingId): Setting | undefined { const hit = store.entityMap()[id]; return hit && !isExpired(hit.timeStamp) ? hit : undefined; }, })) );
| Aspect | Decision |
|---|---|
| Normalisation | withEntities gives constant-time look-ups and auto-generated helpers. |
| Custom PK | entityConfig + selectId lets us keep SettingId without duplicating it as id. |
| Expiration | Centralised isExpired check guarantees every read respects the 10-minute TTL. |
| Tiny API surface | Three imperative methods (upsertSettings, missingSettingIds, getSetting) match the cache’s life-cycle exactly—nothing more, nothing less. |
Use it like so:
tsconst desired = ['theme', 'locale']; const notCached = settingsStore.missingSettingIds(desired); if (notCached.length) { httpClient .post<Setting[]>('/api/settings/query', { ids: notCached }) .subscribe(fresh => settingsStore.upsertSettings(fresh)); } const theme = settingsStore.getSetting('theme');
This keeps your cache lean, timestamped, and automatically evicts stale entries after ten minutes.