Angular 20. I need to store a list of settings in ...

Created on: December 14, 2025

Answered using O3 by Chat01

Question

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.

Thought

Creating an Angular store with NgRx Signals

I’m working on providing a code snippet for an Angular store using NgRx Signals. This example doesn’t require browsing since Angular 20 hasn’t been released yet, but we can follow current patterns from v17. I’ll define methods for managing settings and include a 10-minute expiration, as well as use the signalStore and withState decorators. Let's craft this code now.

Thought for 11s

Answer

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); }); }, }) );

How to use it

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'); } }

Why this design?

  • Expiration logic is centralised in isExpired, ensuring that every read correctly invalidates stale entries.
  • Immutable state: upsertSettings creates a new entities map, so Angular’s change detection picks it up instantly.
  • No unnecessary computed signals – the API surface is fully imperative (upsert*, missingIds*, select*), matching the cache nature of the store.

Question

interface TimestampedSetting extends Setting {
timeStamp: Date;
}

What if I use the above structure and I try to go with withEntities?

Thought

Implementing store and handling edge cases

I’ve looked at the docs and can rely on them for entityConfig. For the store example, I’ll ensure to cite references for withEntities and entityConfig to back up any claims. We also need to account for edge cases. For example, store.entityMap is a Signal, and any call to upsertSettings should use patchState, but the missingSettingIds might behave differently when called inside the store.

Finalizing store code implementation

I’ll craft the final code for the setting store using signalStore and integrate NgRx’s utilities like withEntities and entityConfig. The upsertSettings method ensures settings are added with new timestamps. I’m making sure that the cache expiration is handled with proper TTL logic, ensuring relevant methods like isExpired are applied.

Thought for 4m 34s

Answer

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; }, })) );

Why this version?

AspectDecision
NormalisationwithEntities gives constant-time look-ups and auto-generated helpers.
Custom PKentityConfig + selectId lets us keep SettingId without duplicating it as id.
ExpirationCentralised isExpired check guarantees every read respects the 10-minute TTL.
Tiny API surfaceThree imperative methods (upsertSettings, missingSettingIds, getSetting) match the cache’s life-cycle exactly—nothing more, nothing less.

Use it like so:

ts
const 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.

Share this Q&A