Managing state in Angular
About this article
Building single-page applications inevitably brings us face to face with state—there’s no avoiding it. The ecosystem offers plenty of state management solutions, and with those solutions come plenty of strong opinions. When seasoned developers get involved, those opinions tend to harden, often sparking lively debates.
This article isn’t meant to pick a winner among libraries. Its purpose is to help you understand what state really is and how to think about it. While every example here is written in Angular, the concepts themselves are framework-agnostic.
Throughout this piece, we’ll explore the various categories of state that exist in our applications and identify where that state tends to reside.
What is state?
State boils down to everything that shapes the UI our users interact with. It might determine whether a button appears on screen, hold the outcome of a click on that button, or contain an Array of users fetched from an API. State can be scattered across many parts of our codebase. Some of it is tightly scoped to a single component, while other pieces are shared among multiple areas. One slice of state could exist as a singleton, whereas another might only live as long as a particular component that could be destroyed in an instant.
This sheer diversity—what state represents, how long it persists, and where it originates—creates the complexity we have to deal with.
What is state management?
State management refers to the practice of creating, modifying, removing, and retrieving state within an application. When we deal with deeply nested data structures and need to touch a value buried several levels down, things can get messy. That’s where state management libraries come in—they offer a Store to streamline these operations and cut through the clutter. A word of caution: we shouldn’t lean on these libraries so heavily that they end up introducing more complexity than they remove.
Reactive state
Pairing state management with reactive programming can be an effective strategy for crafting single-page applications. Whether you’re working with Angular, Vue, or React, merging these two ideas can make your apps more predictable.
So how does state relate to reactive programming? Since a piece of state evolves over time, we’re essentially waiting for updates to happen. That inherently makes it asynchronous.
Consider this scenario:
// false------true-----false---true...
sidebarCollapsed$ = this.state.sidebarCollapsed$
The sidebarCollapsed$ stream begins with an initial value of false, yet it later shifts to true and continues from there. What’s more, this stream persists over time. In Angular, such state can be tapped into effortlessly through the async pipe:
<my-awesome-sidebar *ngIf="sidebarCollapsed$|async">
</my-awesome-sidebar>
The async pipe subscribes to sidebarCollapsed$, hands it over to the component, flags it for change detection, and takes care of unsubscribing automatically once the component is destroyed. Storing state with an observer pattern is convenient because we can react to updates. Plus, it integrates smoothly with Angular, in case that wasn’t obvious.
Two options exist: using a BehaviorSubject directly or relying on state management libraries built around Observables. Several solid choices support Observables:
Immutability and Unidirectional data flow
I’ve got two key guidelines worth adhering to before we get deeper into state. First, keep things immutable: avoid altering data directly unless you produce a fresh reference to that object. Direct mutation leads to unpredictable behavior and difficult bug hunts. Sticking to immutable patterns also unlocks performance advantages such as Angular’s ChangeDetection.OnPush or React’s PureComponent.
With TypeScript, we can force the compiler to raise errors if we attempt data mutation.
type Foo = {
readonly bar: string;
readonly baz: number;
}
let first = {bar: 'test', baz: 1};
first.bar = 'test2'; // compilation error
first = {...first, bar: 'test2'}; // success
Within the earlier sample, the first instance was replaced by a completely fresh object carrying a modified bar field.
For arrays, a similar approach applies:
let arr = ['Brecht', 'Kwinten'];
arr.push('John'); // BAD: arr is mutated
arr = [...arr, 'John']; // Good, arr gets new reference
The Array prototype offers useful methods for enforcing immutability, such as map() and filter(). Those helpers, however, fall outside the scope of this article.
The second principle is Unidirectional data flow. In essence, applying this rule means avoiding two-way data binding for state. Only the component that truly owns a given slice of state has the authority to change it, and it must do so in an immutable manner.
Both principles are strongly reinforced by the Redux pattern.
Which state types exist?
Router state
This one often slips under the radar, yet it ranks among the most critical state in any web app. Incorporating state directly into the route provides several benefits:
- Browser back and forward buttons work seamlessly
- Bookmarking the state is straightforward
- The URL can be shared via copy-paste with other users
- No manual handling is required—the state persists within the route itself
Tip: Rather than toggling a userDetailModalVisible flag, why not capitalize on all these advantages by tying the modal to a users/:userId route?
With a child router-outlet in Angular, achieving this becomes quite simple, as demonstrated in the snippet below.
<table>
<!--contains users -->
</table>
<router-outlet>
<!-- user detail modal rendered in here -->
</router-outlet>
Component state
Each component may hold its own state. This state can be utilized internally or passed down to presentational child components.
For instance, if an ItemComponent defines a property selectedItems as an array of ids, and that array is only referenced within its own subtree, it falls under component state.
It is owned by that component, so the component has full responsibility for it. Child components may access that state, but they should never change it directly. Instead, they inform the owning parent, which then updates the state in an immutable fashion. You can learn more about smart versus dumb components right here.
In my experience, I steer clear of state management libraries for component state, since managing it is entirely up to the component itself. Still, there are valid justifications for bringing in such frameworks for this purpose:
- When handling the state gets intricate
- For implementing optimistic updates
- When you need real-time features
If managing that component state starts becoming tricky, but adopting a full-fledged state framework isn't on the table yet, you can incorporate a reducer directly within the component.
Persisted state
Persisted state refers to information that stays intact as the user switches between views. This can be something like remembering if a sidebar is open or closed, or when a user navigates away from and back to a data table with many filters that should be re-applied. Another situation is multi-step wizards, where the input from each step must be saved so the user can go back and forth, and the final step results from all earlier ones.
This kind of state usually calls for a state management framework. However, if you prefer to avoid an extra library, an Angular service—scoped as a singleton across the entire app—can do the job. If that service grows cumbersome or you face a substantial volume of state, then migrating to a state management framework becomes a sensible option.
Shared state
Shared state concerns data that must be accessible from various sections of the application, namely across different smart components. Consequently, the single source of truth for this state belongs at a layer above the components that depend on it.
You can handle shared state with state management solutions such as Redux, Ngrx, Akita, Ngxs, among others, though a lightweight approach will work fine for simpler cases.
Suppose we need a shared Observable of an Array of countries across the whole app. In Angular, a CountryService could fetch those countries from the API one time and then distribute them app-wide.
The RxJS shareReplay operator is just the tool for that.
export class CountryService {
...
countries$ = this.httpClient.get('countries').pipe(shareReplay(1));
}
Seems simple enough—just a single line of code! However, this doesn’t mean we need a state management framework, even if such tools bring their own perks.
Some developers prefer storing all their reference data in a Redux store, which is perfectly fine. Still, it’s important to recognize that we aren’t obligated to do so.
My personal preference leans heavily on the KISS principle (Keep It Simple Stupid), which makes me advocate for this simpler route quite often.
Consider how many lines of code we cut out with this method.
Don’t forget that every line we add requires not just initial writing but ongoing upkeep as well.
Which state requires management?
Having established what state is, we must figure out which parts of it need management and where that management should happen—inside a component, a singleton service, or a framework like a Store?
This is where opinions start to diverge sharply. My advice is to pick what suits you and your team, and to give it real thought. With that said, here are my own personal, opinionated recommendations:
- I steer clear of state management frameworks whenever I can. RxJS already gives us a ton of functionality, and I’m a big believer in KISS.
- I avoid using state management frameworks for cross-component communication, since I see state and communication as separate concerns.
- If a component can manage its own state without excessive complexity, I let that component take charge.
- Reference data, such as a list of countries, is exposed through a service that leverages the
shareReplayoperator. - For a
getByIdAPI response, I don’t store it unless another part of the app consumes that state beyond the component that initiated the request. - I place a facade between my smart components and my stores/services to ease future refactoring.
On the other hand, a widely held view advocates for putting nearly everything into the store, which offers these benefits:
- Developer tools show the complete flow of code
- Uniformity in patterns
- Access to memoized selectors
- Better fit for realtime scenarios
- Simpler optimistic updates
Still, there are considerable drawbacks as well:
- Huge volume of boilerplate: larger bundles, greater maintenance burden, and more dev time. For instance, applying the full Ngrx pattern to the
countries$example would require creating anaction,actiontype,effect, and areducer. - Creates a strong dependency that’s very difficult to replace later
- Adds overall complexity
- The user’s view can become out of sync with the backend
- Cache invalidation: adding a
currentUserToEditto the store means we must remove it upon navigation away - Prevents using the
asyncpipe to cancel pending XHR requests - It results in a sort of distributed monolith
Wrapping up
State management is a subject full of debate and personal preference. There’s no absolute right or wrong; stick with what works for you and your team. Great libraries certainly exist, so make use of them if they provide value, but take a moment to consider before adopting one. After all, this article aims to encourage thoughtful assessment of state management rather than jumping straight to the easiest fix.
Special thanks
Acknowledgment goes out to the excellent reviewers:

•