State in enterprise frontends: ownership before libraries
State management in enterprise and React apps: name the kind of state, give it one owner, derive the rest. , , URL and form state.

A support agent opens the link a customer sent: an account page, filtered on one category. The page shows all categories, because the filter lived in a component field and not in the URL. Nobody wrote a bug. The state was stored as the wrong kind.
That is the state problem in enterprise apps. The library choice comes last. First you decide what kind each piece of state is, who owns it, and how long it lives.
Five kinds of state
- Server data. Accounts, transactions. The backend owns it; the frontend holds a copy that can be stale (part 3).
- UI state. Which panel is open. Owned by one component, gone when it is gone.
- Form state. A draft, its validation, whether it is dirty.
- URL state. Filters, tabs, the selected record. Anything a link, a support ticket or the back button must restore.
- Session state. The user and their roles. Truly global.
A common state bug is one kind stored as another: server data copied into a component, a filter in memory that belongs in the URL, roles cached in a store nobody refreshes.
The URL is state
If a person might share it, bookmark it or go back to it, it belongs in the URL. In Angular, withComponentInputBinding binds route parameters to component inputs, and by default the router sets an input to undefined when the value is missing from the route, which "ensures that stale data is not retained". Query parameters bind the same way, so a filter can live there too.
One store per domain, one global store at most
I would give each domain its own store: accounts, cards, requests. A team owns its store the way it owns its library. A single global store looks tidy on day one and turns into the place where every team's state meets every other team's bugs.
What stays global is what is cross-cutting by nature: the session, the permissions, maybe . Everything else stays close to the feature that owns it.
Derive, do not duplicate
A total balance is not state. It is a function of the accounts. Angular says it directly: "Avoid using effects for propagation of state changes", use instead. React says the same in its own words: "When something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering."
Every copy of a value is a second owner. Two owners will disagree.
Long regulated forms
A loan application or a patient intake form is state with rules: drafts that survive a lunch break, validation that mirrors the server's, and so nobody loses an hour of input. The server still validates; the client check is for the person typing.
For new code, Angular's sync the form model with its fields automatically. The Angular team is careful here: they "work best in new applications built with signals", and "reactive forms remain a solid choice" in an existing app that uses them. In a ten-year codebase, that is the right advice. Do not migrate forms for fashion.
Signals or RxJS
My rule: signals for state (a value now), RxJS for events over time (keystrokes, , retries). The bridge exists: toSignal turns an Observable into a signal and "behaves similarly to the async pipe in templates". Signals also fit Angular, where "updating a signal that's read in a template" is one of the notifications that schedules .
On Atlas Bank
AccountsStore in libs/data-access/banking keeps its state private and hands out read-only signals and :
private readonly accountsState = signal<readonly Account[]>([]);
private readonly transactionsState = signal<readonly Transaction[]>([]);
private readonly beneficiariesState = signal<readonly Beneficiary[]>([]);
private readonly loadingState = signal(false);
private readonly loadedState = signal(false);
private inFlight?: Promise<void>;
readonly accounts = this.accountsState.asReadonly();
readonly transactions = this.transactionsState.asReadonly();
readonly beneficiaries = this.beneficiariesState.asReadonly();
readonly loading = this.loadingState.asReadonly();
readonly loaded = this.loadedState.asReadonly();
readonly totalBalanceMad = computed(() =>
this.accountsState()
.filter((a) => a.status !== 'closed')
.reduce((sum, a) => sum + a.balanceMad, 0),
);asReadonly() exposes a value "without allowing them to modify it directly", so the store is the only owner. The account detail page gets its id from the route through withComponentInputBinding and derives the filtered list with computed. Its category filter, though, is a component signal. Fine for a demo; in a bank's support flow I would move it to a query parameter.
The legacy transfer screen shows the old shape: plain fields, manual subscriptions, and markForCheck() after them. The repo's AGENTS.md calls that "a smell to migrate, not a pattern to copy".
What I left out, and why
- , , , comparisons. A library tour. Ownership decides first; any of them can hold the result.
- as a concept. Implied by read-only signals and derived values.
- on the client. Rare in a frontend, a backend shape.
- . React library vocabulary for the same idea as "derive, do not duplicate".
On Monday
- Pick one screen and label every piece of state with its kind: server, UI, form, URL, session.
- Find one value stored twice. Make one of them a
computed(or a calculation during render). - Find one filter that a support ticket cannot reproduce. Move it to the URL.
Next: Part 3, on networking and caching.
For your coding agent
The rules of this article, written for an agent. Paste them into your AGENTS.md, save them as a skill, or point your agent at the file.
# State rules for enterprise apps Source: https://tt-tech.io/blog/enterprise-frontend-state-ownership For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ. ## Rules 1. Before you add state, name its kind: server data, UI state, form state, URL state or session state. Store it as that kind. 2. Put anything a link, a bookmark, a support ticket or the back button must restore in the URL: filters, tabs, the selected record. In Angular, bind route and query parameters to inputs with `withComponentInputBinding`, and expect a missing parameter to arrive as `undefined`. 3. Server data belongs to the backend. Never copy it into a component. Read it from the store of its domain. 4. Give each domain its own store (accounts, cards, requests), owned by the team that owns the domain. Never add a single global store for everything. 5. Keep global only what is cross-cutting by nature: the session, the permissions, maybe feature flags. Do not cache roles in a store nobody refreshes. 6. Keep UI state (which panel is open) in the component that owns it. 7. Derive, do not duplicate. If a value can be calculated from state, use `computed` in Angular or calculate it during render in React. Never store it as a second copy. 8. Never use `effect` to propagate state changes. Use `computed` instead. 9. Keep a store's writable state private. Expose read-only signals with `asReadonly()` and derived values, so the store is the only owner. 10. In long regulated forms, keep drafts, mirror the server's validation and track dirty state. The server still validates; the client check is for the person typing. 11. Use Signal Forms in new code built with signals. Keep reactive forms in an existing app that uses them. Do not migrate forms for fashion. 12. Use signals for state (a value now) and RxJS for events over time (keystrokes, websockets, retries). Bridge them with `toSignal`. ## Before you say done - For every new piece of state in the diff, list its kind and its owner in the pull request. - Search the diff for `effect(` calls that set other state, and for stored values that could be a `computed` or a calculation during render. - For every new filter, tab or selection, confirm it lives in the URL, or say why not.
Sources
- Angular, Other routing tasks, docs
- Angular, Side effects for non-reactives APIs, docs
- React, You Might Not Need an Effect, docs
- Angular, Forms with Angular Signals, docs
- Angular, RxJS interop with Angular signals, docs
- Angular, Angular without ZoneJS (Zoneless), docs
- Angular, Angular Signals, docs