
Documenting Signals State in Angular 20+: Derived Selectors, Mutators, and Analytics Hooks Hiring Teams Can Trust
A practical documentation standard for SignalStore—selectors, mutators, and analytics—so recruiters, directors, and senior engineers can review your Angular 20+ repo in minutes.
If reviewers can’t find your selectors, mutators, and analytics events in under five minutes, your Signals migration will look risky—no matter how clean the code is.Back to all posts
I’ve sat on both sides of the table—building enterprise dashboards and reviewing repos for Fortune 100 teams who need a senior Angular engineer now. The fastest way to earn trust in a review is simple, boring documentation that maps exactly to your Signals-based state: derived selectors, mutators, and analytics hooks.
Below is the documentation standard I use across AngularUX projects (telecom analytics, airport kiosks, insurance telematics, media schedulers). It’s optimized for Angular 20+, SignalStore, Nx monorepos, PrimeNG UIs, and Firebase analytics/telemetry. Your reviewers should be able to answer: What state exists? How is it derived? What changes it? What gets tracked?
The Five-Minute Review Your Repo Gets
As companies plan 2025 Angular roadmaps, hiring managers ask me one thing: Can a remote Angular developer stabilize and extend this codebase without surprises? Clear state documentation answers yes before the call.
Real scene from enterprise reviews
Hiring panels don’t have time to spelunk your state. They skim your feature-level docs and one or two stores. If selectors, mutators, and analytics hooks are obvious and typed, you’ll look like an Angular expert in five minutes. If they’re vibe-coded, you’ll be explaining during a budget freeze.
Telecom analytics: jittery dashboard traced to undocumented selectors recomputing on every tick.
Airline kiosk: offline flows broke because mutators weren’t idempotent—no preconditions documented.
Entertainment payroll: finance asked for an audit trail; analytics hooks were scattered across components.
Why Document Derived Selectors, Mutators, and Analytics in Angular 20+
Signals raise the bar
Signals + SignalStore make state predictable, but the cost of a missing invariant is higher because change detection is leaner and you can push a lot more updates. Documentation is the throttle that keeps real-time apps smooth and provable.
Derived signals can hide expensive computation.
Mutators decide data quality and auditability.
Analytics define your truth for KPIs and growth.
What reviewers look for
I use Angular DevTools flame charts, Core Web Vitals, and Firebase Logs to back claims. If you want to hire an Angular developer quickly, show these receipts in the repo.
Naming consistency: nouns for selectors, verbs for mutators.
Typed analytics events with examples.
Performance and accessibility notes (AA, SSR caveats).
A Documentation Standard that Mirrors SignalStore
Folder and README structure
Keep the docs where reviewers look first: next to the store. If you also use NgRx for WebSocket streams in dashboards, note the boundary: SignalStore for in-memory UI state, NgRx for server events.
Nx lib per feature: libs/cart/data-access
README.md sections: State, Selectors, Mutators, Analytics, Performance
ADR: Why Signals/SignalStore, not NgRx reducers/effects (or how they interop)
TSDoc conventions
Docs live above the code they describe. A one-page README summarizes; TSDoc provides the source of truth for automation.
@selector name – purpose, inputs, complexity
@mutator name – preconditions, side-effects, idempotency
@analytics event – schema and sample payload
SignalStore example with documented selectors, mutators, and analytics
import { computed, effect, inject, signal } from '@angular/core';
import { signalStore, withState, withComputed, withMethods, withHooks } from '@ngrx/signals';
import { Analytics } from '@angular/fire/analytics';
// --- Types ---
export type CartItem = { id: string; price: number; qty: number };
export type CartState = {
items: Record<string, CartItem>;
discountPct: number; // 0..100
lastUpdated: number;
};
type AnalyticsEvent =
| { name: 'cart_item_added'; itemId: string; qty: number; price: number }
| { name: 'cart_discount_applied'; pct: number }
| { name: 'cart_checked_out'; itemCount: number; total: number };
function track(analytics: Analytics, ev: AnalyticsEvent): void {
// Centralized analytics hook. Replace with GA4/logEvent wrapper as needed.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(analytics as any).logEvent(ev.name, { ...ev });
}
const initialState: CartState = { items: {}, discountPct: 0, lastUpdated: Date.now() };
export const CartStore = signalStore(
withState(initialState),
// --- Selectors ---
withComputed((s) => ({
/**
* @selector itemCount
* Total units in cart.
* Inputs: s.items
* Complexity: O(n) over values(items)
*/
itemCount: computed(() => Object.values(s.items()).reduce((a, c) => a + c.qty, 0)),
/**
* @selector subtotal
* Sum of price*qty without discount. Pure derived signal.
* Inputs: s.items
* Complexity: O(n)
*/
subtotal: computed(() => Object.values(s.items()).reduce((a, c) => a + c.price * c.qty, 0)),
/**
* @selector total
* subtotal minus percentage discount. Clamped to >= 0.
* Inputs: subtotal, s.discountPct
* Note: Keep math deterministic for analytics parity.
*/
total: computed(() => {
const raw = s.subtotal();
const pct = Math.max(0, Math.min(100, s.discountPct()));
return Math.max(0, Math.round(raw * (1 - pct / 100)));
}),
})),
// --- Mutators ---
withMethods((s, analytics = inject(Analytics)) => ({
/**
* @mutator addItem
* Preconditions: qty > 0, price >= 0.
* Idempotency: additive; repeated calls accumulate qty.
* Side-effects: updates lastUpdated; logs 'cart_item_added'.
*/
addItem(id: string, price: number, qty = 1): void {
if (qty <= 0 || price < 0) return;
const prev = s.items()[id];
const next: CartItem = prev
? { ...prev, qty: prev.qty + qty, price }
: { id, qty, price };
s.items.mutate((map) => (map[id] = next));
s.lastUpdated.set(Date.now());
track(analytics, { name: 'cart_item_added', itemId: id, qty, price });
},
/**
* @mutator applyDiscount
* Preconditions: 0 <= pct <= 100. Rounds to integer.
* Idempotency: setting same pct twice is a no-op.
* Side-effects: updates lastUpdated; logs 'cart_discount_applied'.
*/
applyDiscount(pct: number): void {
const clamped = Math.max(0, Math.min(100, Math.round(pct)));
if (clamped === s.discountPct()) return;
s.discountPct.set(clamped);
s.lastUpdated.set(Date.now());
track(analytics, { name: 'cart_discount_applied', pct: clamped });
},
/**
* @mutator checkout
* Preconditions: itemCount > 0.
* Side-effects: logs 'cart_checked_out' and clears state.
*/
checkout(): void {
const count = s.itemCount();
if (count === 0) return;
track(analytics, { name: 'cart_checked_out', itemCount: count, total: s.total() });
s.items.set({});
s.discountPct.set(0);
s.lastUpdated.set(Date.now());
},
})),
// --- Lifecycle/Analytics Hooks ---
withHooks({
onInit(store) {
// Example: derive a performance mark for heavy selectors
effect(() => {
// This effect subscribes to total to simulate heavy recompute logging
void store.total();
performance.mark('cart:total:recomputed');
});
},
})
);Component usage with docs in place
@Component({
selector: 'cart-summary',
template: `
<p-panel header='Summary'>
<div>Total items: {{ store.itemCount() }}</div>
<div>Subtotal: {{ store.subtotal() | currency:'USD' }}</div>
<div>Total: {{ store.total() | currency:'USD' }}</div>
<button pButton label='Checkout' (click)='store.checkout()' [disabled]='store.itemCount()===0'></button>
</p-panel>
`,
standalone: true,
imports: [PanelModule, ButtonModule],
})
export class CartSummaryComponent { constructor(public store: CartStore) {} }PrimeNG keeps markup terse; reviewers see exactly which selectors and mutators are used without spelunking.
Repository Artifacts that Win Reviews
These artifacts show discipline without ceremony. On telecom analytics, this template cut onboarding from days to hours. On airport kiosks, we documented scanner/printer mutators and offline invariants—support could finally reproduce defects in Docker CI.
Feature README template
# Cart Store (libs/cart/data-access)
## State
- items: Record<string, CartItem>
- discountPct: number (0..100)
## Selectors
- itemCount: total units (O(n))
- subtotal: sum price*qty (O(n))
- total: subtotal minus discount (rounded, >= 0)
## Mutators
- addItem(id, price, qty=1): additive, logs cart_item_added
- applyDiscount(pct): clamped 0..100, logs cart_discount_applied
- checkout(): logs cart_checked_out, clears state
## Analytics Events
- cart_item_added { itemId, qty, price }
- cart_discount_applied { pct }
- cart_checked_out { itemCount, total }
## Performance Notes
- Verify itemCount/subtotal/total recompute cost in Angular DevTools.
- Flame charts stable at <1ms/frame on 1k items (see /perf/cases.md).Typedoc/Compodoc and CI guardrail
# .github/workflows/docs-check.yaml
name: docs-check
on: [pull_request]
jobs:
typedoc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx typedoc --json docs/typedoc.json
- run: git diff --exit-code docs || (echo 'Docs changed. Update README/ADR.' && exit 1)Generate docs; snapshot in CI; fail when store API changes without README update.
Nx tagging and ownership
// libs/cart/data-access/project.json
{
"tags": ["type:state", "scope:cart"]
}# CODEOWNERS
/libs/cart/data-access/* @frontend-leads @analytics-teamTag data-access libs as 'state'; CODEOWNERS require review for mutator changes.
Test as living documentation
// cart.store.spec.ts
it('applyDiscount clamps and is idempotent', () => {
const s = new CartStore();
s.applyDiscount(105);
expect(s.discountPct()).toBe(100);
s.applyDiscount(100);
expect(s.discountPct()).toBe(100); // idempotent
});
it('analytics events are well-typed', () => {
// compile-time: track({ name: 'cart_item_added', itemId: 'x' }); // TS error: missing qty/price
});How an Angular Consultant Documents Signals
Engagement pattern (what I do)
If you need to hire an Angular developer with Fortune 100 experience, I audit your top-3 stores, implement this template, and leave you with CI guardrails. See how I stabilize chaotic repos with gitPlumbers (70% velocity lift, 99.98% uptime) and real Angular apps like IntegrityLens (12k+ interviews) and SageStepper (320 communities, +28% lift).
Discovery within 48 hours; store audit delivered in 1 week.
Instrument telemetry and DevTools marks before changing code.
Snapshot docs in CI; add READMEs and TSDoc as PRs.
When to bring me in
I’ve rescued AngularJS → Angular migrations, removed zone.js footguns, and enforced TypeScript strictness without freezing delivery. As a remote Angular contractor, I’m available for targeted fixes or a full documentation and statecraft pass.
Selectors recompute too often or are untraced.
Mutators aren’t idempotent or lack preconditions.
Analytics events aren’t typed, dashboards disagree with finance.
Concise Takeaways and Next Steps
- Document selectors, mutators, and analytics hooks next to the store with TSDoc and a one-page README.
- Type your analytics events and centralize logging; include examples in docs and tests.
- Add CI guardrails to catch undocumented state changes; verify performance with Angular DevTools and telemetry.
Ready to review your repo or plan a Signals rollout? Let’s talk.
FAQs
More questions are below for hiring and process details.
How long does this take?
Most teams get a first pass across 2–3 critical stores in 1 week; full repo standards in 2–4 weeks. For upgrades plus documentation, plan 4–8 weeks depending on risk and test coverage.
Do we need NgRx and SignalStore?
Often both. I keep NgRx for server streams and cross-app events; SignalStore for component-level and derived UI state. The docs template applies to either.
What about SSR/PrimeNG/AA accessibility?
Document SSR constraints, keyboard interactions, and ARIA expectations in the README. Add a11y notes per mutator if it changes focus or announces updates. PrimeNG works well with clear ownership.
Key takeaways
- Write a one-page README per feature store listing selectors, mutators, and analytics events with purpose, inputs, and invariants.
- Use TSDoc tags (@selector, @mutator, @analytics) directly on computed signals and methods; keep pre/post-conditions close to code.
- Adopt SignalStore with withComputed, withMethods, and withHooks so the documentation structure mirrors the code structure.
- Type your analytics events with a discriminated union and log via a single track function; include event samples in README.
- Automate doc drift checks in CI (Typedoc/Compodoc generation + snapshot); fail PRs when selectors/mutators change without docs.
Implementation checklist
- Create a feature README template with Selectors, Mutators, Analytics sections.
- Name selectors with a noun (e.g., totalDue), mutators with an imperative verb (e.g., applyDiscount).
- Add TSDoc with @selector/@mutator/@analytics and pre/post-conditions to each item.
- Model analytics events as a typed union and expose a track function.
- Generate docs with Typedoc and snapshot them in CI to prevent drift.
- Link each selector/mutator to a usage example (component or effect).
- Instrument performance marks for heavy selectors; verify with Angular DevTools.
- Include an ADR that states why Signals/SignalStore are used over alternatives.
Questions we hear from teams
- How much does it cost to hire an Angular developer for this documentation pass?
- Scoped audits start at a fixed fee; typical 2–4 week engagements are fixed-price with outcomes (docs, tests, CI guardrails). Get a quote after a 30–45 minute repo walkthrough.
- What does an Angular consultant actually deliver here?
- Feature READMEs, TSDoc on selectors/mutators/hooks, typed analytics events, CI doc checks, and a short ADR. Optional: performance marks, Angular DevTools capture, and Firebase Analytics wiring.
- How long does an Angular upgrade plus documentation take?
- Most Angular 14–20 upgrades with docs take 4–8 weeks depending on size, third-party libraries, and test maturity. We ship incrementally with feature flags and CI guardrails to avoid production risk.
- Can you work with Nx, Firebase, and PrimeNG?
- Yes. My standard stack is Angular 20+, Nx monorepo, Firebase Hosting/Functions/Analytics, and PrimeNG or Material. I also integrate D3/Highcharts for data viz and Docker for kiosk simulation.
- What’s involved in a typical engagement?
- Discovery within 48 hours, repo audit in 1 week, prioritized fixes and documentation templates, and CI guardrails. I work remote as a contractor or consultant and coordinate with your dev/analytics teams.
Ready to level up your Angular experience?
Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.
NG Wave
Angular Component Library
A comprehensive collection of 110+ animated, interactive, and customizable Angular components. Converted from React Bits with full feature parity, built with Angular Signals, GSAP animations, and Three.js for stunning visual effects.
Explore Components