If you have shipped a serious Angular app in the last few years, you probably have BehaviorSubjects, selector streams, and NgRx slices everywhere—and no one on the team is excited to refactor them. With Angular 19+, Signals are no longer experimental or “nice to have.” They are stable, performant, and increasingly treated as the default way to model local state. As a result, many Angular teams are actively revisiting long-standing RxJS and NgRx patterns—sometimes rewriting them entirely—while community discussions are filled with the same question: What should Angular state management look like now? Angular 19+ default What should Angular state management look like now? This confusion is understandable. Signals promise less boilerplate and a simpler mental model, but RxJS and NgRx are deeply embedded in real-world applications that cannot simply be replaced overnight. Who is this for? Angular teams with RxJS-heavy components trying to simplify local state. Applications already using NgRx for global or domain state. Tech leads planning refactors, and wondering where Signals fit. Angular teams with RxJS-heavy components trying to simplify local state. Applications already using NgRx for global or domain state. NgRx Tech leads planning refactors, and wondering where Signals fit. Angular’s state management story has gone through several distinct phases. Early Angular relied heavily on RxJS everywhere. As applications grew, NgRx emerged to bring structure, predictability, and discipline to global state. Now, with Signals becoming first-class citizens, the question is no longer whether to use them, but where they belong. whether The correct answer is not “replace everything with Signals.” The real evolution is more nuanced—and more practical. Angular now provides multiple state tools, each optimized for a specific class of problems. The challenge is choosing the right one with intention. 1. Signals, RxJS, and NgRx: How to Decide The biggest mental shift Angular developers need to make is this: State management is about scope and responsibility, not APIs. State management is about scope and responsibility, not APIs. State management is about scope and responsibility, not APIs. Instead of asking “Should I use Signals or RxJS?”, ask: “Should I use Signals or RxJS?” How long does this state live? Who owns it? How many parts of the app depend on it? Is it synchronous or time-based? How long does this state live? Who owns it? How many parts of the app depend on it? Is it synchronous or time-based? A practical rule of thumb Signals → Local, synchronous UI state RxJS → Asynchronous streams and external events NgRx → Global, long-lived domain state with workflows Signals → Local, synchronous UI state RxJS → Asynchronous streams and external events NgRx → Global, long-lived domain state with workflows This rule holds up surprisingly well across real applications. Decision guide If your problem is... You probably want... Because... Toggle state, tabs, modals, filters Signals Direct, synchronous updates with minimal mental overhead Search autocomplete, live input, debouncing RxJS Streams model time, cancellation, and backpressure naturally Cart state shared across pages NgRx Predictable global state with replayable actions Feature flags or permissions NgRx Centralized source of truth with clear ownership HTTP request lifecycle RxJS + Signals RxJS for async, Signals for consuming the result Derived UI state (counts, visibility) Computed Signals Automatic dependency tracking without subscriptions WebSocket or live streaming updates RxJS + Signals Continuous streams feed a simple signals-based UI model If your problem is... You probably want... Because... Toggle state, tabs, modals, filters Signals Direct, synchronous updates with minimal mental overhead Search autocomplete, live input, debouncing RxJS Streams model time, cancellation, and backpressure naturally Cart state shared across pages NgRx Predictable global state with replayable actions Feature flags or permissions NgRx Centralized source of truth with clear ownership HTTP request lifecycle RxJS + Signals RxJS for async, Signals for consuming the result Derived UI state (counts, visibility) Computed Signals Automatic dependency tracking without subscriptions WebSocket or live streaming updates RxJS + Signals Continuous streams feed a simple signals-based UI model If your problem is... You probably want... Because... If your problem is... If your problem is... You probably want... You probably want... Because... Because... Toggle state, tabs, modals, filters Signals Direct, synchronous updates with minimal mental overhead Toggle state, tabs, modals, filters Toggle state, tabs, modals, filters Signals Signals Signals Direct, synchronous updates with minimal mental overhead Direct, synchronous updates with minimal mental overhead Search autocomplete, live input, debouncing RxJS Streams model time, cancellation, and backpressure naturally Search autocomplete, live input, debouncing Search autocomplete, live input, debouncing RxJS RxJS RxJS Streams model time, cancellation, and backpressure naturally Streams model time, cancellation, and backpressure naturally Cart state shared across pages NgRx Predictable global state with replayable actions Cart state shared across pages Cart state shared across pages NgRx NgRx NgRx Predictable global state with replayable actions Predictable global state with replayable actions Feature flags or permissions NgRx Centralized source of truth with clear ownership Feature flags or permissions Feature flags or permissions NgRx NgRx NgRx Centralized source of truth with clear ownership Centralized source of truth with clear ownership HTTP request lifecycle RxJS + Signals RxJS for async, Signals for consuming the result HTTP request lifecycle HTTP request lifecycle RxJS + Signals RxJS + Signals RxJS + Signals RxJS for async, Signals for consuming the result RxJS for async, Signals for consuming the result Derived UI state (counts, visibility) Computed Signals Automatic dependency tracking without subscriptions Derived UI state (counts, visibility) Derived UI state (counts, visibility) Computed Signals Computed Signals Computed Signals Automatic dependency tracking without subscriptions Automatic dependency tracking without subscriptions WebSocket or live streaming updates RxJS + Signals Continuous streams feed a simple signals-based UI model WebSocket or live streaming updates WebSocket or live streaming updates RxJS + Signals RxJS + Signals RxJS + Signals Continuous streams feed a simple signals-based UI model Continuous streams feed a simple signals-based UI model A good heuristic is blast radius: blast radius If a state change affects one component → Signals If it affects multiple features → NgRx If it depends on time, cancellation, or backpressure → RxJS If a state change affects one component → Signals If it affects multiple features → NgRx If it depends on time, cancellation, or backpressure → RxJS 2. A Real-World Migration Slice Most teams are not rewriting their state architecture from scratch. They migrate incrementally, one screen or feature at a time. incrementally The starting point The starting point A mid-sized Angular app had a user management screen using: BehaviorSubject as a local store Selector streams with map async pipe in templates BehaviorSubject as a local store BehaviorSubject Selector streams with map map async pipe in templates async The code was technically correct—but onboarding new developers took time. Understanding the data flow required jumping between streams, operators, and templates. The decision The decision Instead of a full refactor, the team migrated only the UI-level state to Signals: only the UI-level state Selected user Filters Derived counts Selected user Filters Derived counts HTTP, pagination, and error handling stayed in RxJS. Immediate benefits Immediate benefits Components read top-to-bottom like plain TypeScript. No subscriptions to track or clean up. Debugging became trivial (log the signal value). New team members understood the code much faster. Components read top-to-bottom like plain TypeScript. No subscriptions to track or clean up. Debugging became trivial (log the signal value). New team members understood the code much faster. What went wrong What went wrong Initially, the team tried to move HTTP calls into Signals. This caused: Duplicate network requests No cancellation when inputs changed Harder error propagation Duplicate network requests No cancellation when inputs changed Harder error propagation This is a concrete example of an anti-pattern: trying to push debouncing, retries, polling, or other time-based orchestration into Signals instead of keeping it in RxJS. The fix The fix They restored RxJS for HTTP and used Signals only as state holders. state holders Key lesson: Signals simplify state ownership, not asynchronous orchestration. Key lesson: Signals simplify state ownership, not asynchronous orchestration. Key lesson: Key lesson: Signals simplify state ownership, not asynchronous orchestration. This separation—RxJS for time, Signals for state—is the core mental model Angular is pushing toward. Mental model shift: Before—everything is a stream; after—async at the edges, Signals in the core. Before—everything is a stream; after—async at the edges, Signals in the core. 3. A Copy-Pasteable Mini Refactor Before: RxJS-only local store Before: RxJS-only local store users.store.ts users.store.ts // users.store.ts private usersSubject = new BehaviorSubject<User[]>([]); users$ = this.usersSubject.asObservable(); readonly activeUsers$ = this.users$.pipe( map(users => users.filter(u => u.active)) ); loadUsers() { this.http.get<User[]>('/api/users') .subscribe(users => this.usersSubject.next(users)); } // users.store.ts private usersSubject = new BehaviorSubject<User[]>([]); users$ = this.usersSubject.asObservable(); readonly activeUsers$ = this.users$.pipe( map(users => users.filter(u => u.active)) ); loadUsers() { this.http.get<User[]>('/api/users') .subscribe(users => this.usersSubject.next(users)); } users.component.html users.component.html <!-- users.component.html --> <ul> <li *ngFor="let user of activeUsers$ | async"> {{ user.name }} </li> </ul> <!-- users.component.html --> <ul> <li *ngFor="let user of activeUsers$ | async"> {{ user.name }} </li> </ul> Why this becomes painful over time Why this becomes painful over time Even simple state requires streams, operators, and template indirection. The mental cost increases faster than the code size. After: Signals for state, RxJS where it belongs After: Signals for state, RxJS where it belongs users.store.ts users.store.ts // users.store.ts users = signal<User[]>([]); activeUsers = computed(() => this.users().filter(u => u.active) ); loadUsers() { this.http.get<User[]>('/api/users') .subscribe(users => this.users.set(users)); } // users.store.ts users = signal<User[]>([]); activeUsers = computed(() => this.users().filter(u => u.active) ); loadUsers() { this.http.get<User[]>('/api/users') .subscribe(users => this.users.set(users)); } users.component.html users.component.html <!-- users.component.html --> <ul> <li *ngFor="let user of activeUsers()"> {{ user.name }} </li> </ul> <!-- users.component.html --> <ul> <li *ngFor="let user of activeUsers()"> {{ user.name }} </li> </ul> Why this is better Why this is better State is synchronous, dependency-tracked automatically, and directly readable—while RxJS remains responsible for async behavior. Important clarification Important clarification This is not “RxJS vs Signals.” It is RxJS at the boundary, Signals in the core. Example: Live search (RxJS for time, Signals for state) Example: Live search (RxJS for time, Signals for state) A common case where Signals and RxJS complement each other is live search. A signal holds the current query and results. RxJS handles debouncing, cancellation, and HTTP. A signal holds the current query and results. RxJS handles debouncing, cancellation, and HTTP. Conceptually: User input updates a query signal. An RxJS pipeline debounces the query and performs the request. Results are written back into a results signal. User input updates a query signal. An RxJS pipeline debounces the query and performs the request. Results are written back into a results signal. Why this works well Why this works well RxJS manages time and cancellation, while Signals provide a simple, synchronous state model for rendering and derived UI logic. 4. Where NgRx Still Clearly Wins Signals do not replace NgRx. They solve a different problem. NgRx is still the right choice when you need: A single source of truth across routes. Explicit workflows (load → success → failure). Action history and replay. Strong conventions for large teams. Predictable debugging with DevTools. A single source of truth across routes. Explicit workflows (load → success → failure). Action history and replay. Strong conventions for large teams. Predictable debugging with DevTools. Examples where NgRx remains the best option: Authentication and authorization. Shopping carts and checkout flows. Feature flags and entitlement logic. Offline-capable or cached domain data. Authentication and authorization. Shopping carts and checkout flows. Feature flags and entitlement logic. Offline-capable or cached domain data. A common and effective pattern in larger apps is NgRx for domain state, Signals in components. For example, authentication state (user, roles, tokens, refresh lifecycle) lives in an NgRx auth slice, while components consume that state into Signals for local UI decisions such as visibility, layout, and interaction state. NgRx for domain state, Signals in components To bridge the two, teams often use helpers like toSignal / toObservable or a small signal-based store wrapper around selectors, keeping NgRx as the canonical source of truth. toSignal toObservable In fact, Signals often make NgRx more effective—by reducing the amount of state that needs to live there. more 5. Opinionated Do’s and Don’ts Do Do Use Signals for component-local UI state. Use computed signals instead of selector streams when the state is synchronous. Keep RxJS for async workflows, streams, and cancellation. Use NgRx for global, business-critical state. Define clear boundaries between tools. Use Signals for component-local UI state. Use computed signals instead of selector streams when the state is synchronous. Keep RxJS for async workflows, streams, and cancellation. Use NgRx for global, business-critical state. Define clear boundaries between tools. Don’t Don’t Don’t try to replace RxJS entirely. Don’t model time-based problems with Signals. Don’t abandon NgRx just to reduce boilerplate—if it is already modeling real workflows and shared domain state well, keep it. Don’t mix Signals and Observables without intent. Don’t optimize prematurely—optimize for clarity. Don’t try to replace RxJS entirely. Don’t model time-based problems with Signals. Don’t abandon NgRx just to reduce boilerplate—if it is already modeling real workflows and shared domain state well, keep it. Don’t mix Signals and Observables without intent. Don’t optimize prematurely—optimize for clarity. A Simple Mental Model for Modern Angular State Flow Think of modern Angular state as a one-directional pipeline with clear boundaries: a one-directional pipeline Data flow: Data flow: Backend → RxJS stream → Service → Signal store → Component → Template Backend → RxJS stream → Service → Signal store → Component → Template Backend → RxJS stream → Service → Signal store → Component → Template Backend produces data asynchronously. RxJS handles time-based concerns (debounce, retry, cancellation). Services orchestrate data fetching and side effects. Signals store the current, synchronous state of the UI. Components read Signals directly. Templates render without subscriptions or async pipes. Backend produces data asynchronously. Backend RxJS handles time-based concerns (debounce, retry, cancellation). RxJS Services orchestrate data fetching and side effects. Services Signals store the current, synchronous state of the UI. Signals Components read Signals directly. Components Templates render without subscriptions or async pipes. Templates Key idea: RxJS lives at the edges where time exists; Signals live in the core where state is read and derived. Key idea: edges core Migration Strategy in 3 Steps Start with local UI state: Migrate one screen or component at a time. Replace BehaviorSubject based UI state (filters, selection, toggles) with Signals and computed. Avoid shared or cross-route state initially. Keep RxJS at the edges: Leave HTTP, debouncing, polling, and streams in RxJS. Use Signals only to hold and expose the resulting state. Do not model time-based logic with Signals. Revisit NgRx last: After UI state moves to Signals, reassess NgRx usage. What remains should be the true domain state with real workflow or sharing needs. Start with local UI state: Migrate one screen or component at a time. Replace BehaviorSubject based UI state (filters, selection, toggles) with Signals and computed. Avoid shared or cross-route state initially. BehaviorSubject Keep RxJS at the edges: Leave HTTP, debouncing, polling, and streams in RxJS. Use Signals only to hold and expose the resulting state. Do not model time-based logic with Signals. Revisit NgRx last: After UI state moves to Signals, reassess NgRx usage. What remains should be the true domain state with real workflow or sharing needs. Rule of thumb: If state does not need global consistency or time travel, it likely does not belong in NgRx. Rule of thumb: If state does not need global consistency or time travel, it likely does not belong in NgRx. Rule of thumb: Rule of thumb: If state does not need global consistency or time travel, it likely does not belong in NgRx. Conclusion The biggest mistake teams make is treating Signals as a replacement for everything else. The real shift in Angular is not about new APIs—it is about better separation of concerns. better separation of concerns Signals make the local state obvious and readable. RxJS remains unmatched for async and time. NgRx continues to provide structure at scale. Signals make the local state obvious and readable. RxJS remains unmatched for async and time. NgRx continues to provide structure at scale. The future of Angular state management is not fewer tools—it is using each tool where it excels. Teams that adopt this mindset end up with codebases that are easier to reason about, easier to debug, and far more pleasant to maintain—long after the initial refactor is done. using each tool where it excels