
Upgrade Angular 12–15 to Angular 20 Without Breakage: State Management, RxJS, and Change Detection
A field-tested migration plan using Signals, NgRx SignalStore, and RxJS interop—upgrade to Angular 20+ without freezing delivery or spiking render counts.
“Upgrade the framework first, then make Signals pay for themselves on the hot paths. Measure every step.”Back to all posts
Scene from the front lines: why your 12–15 dashboard jitters on ng20
What I see in real upgrades
I’ve upgraded employee tracking systems, airline kiosks, and telecom analytics dashboards through multiple Angular majors. The dangerous part isn’t ng update—it’s the ripple effects in state, RxJS, and change detection. On a telecom analytics app we moved from Angular 11 to 20 with Signals and cut INP 68%, but only after fixing three classes of breakage: NgRx selector churn, RxJS cache misuse, and zone-triggered renders. If you need an Angular consultant who’s done this at Fortune 100 scale, this guide is the exact playbook I use.
Dashboard cards double-render after route changes
Selectors fire too often and async pipes redraw charts
RxJS shareReplay caches don’t refCount and leak
Forms lose types post-upgrade and break reducers
Why this migration matters for 20+
2025 reality for Angular teams
As companies plan 2025 Angular roadmaps, the 12–15 to 20 jump is where you pay down technical debt and align with Signals-first patterns. Done right, you’ll get fewer renders, faster dashboards, and simpler state. Done wrong, you’ll ship more code and slower UX.
Signals are first-class; standalone APIs are table stakes
NgRx offers SignalStore and signal interop
SSR/hydration and tighter TypeScript rules surface hidden bugs
Step-by-step: 12–15 to 20 without pausing delivery
# 1) Create a safe branch
git checkout -b feat/upgrade-ng20
# 2) Framework + tooling (Nx-aware)
npx nx migrate @angular/cli@20 @angular/core@20
npx nx migrate @ngrx/store@latest @ngrx/effects@latest @ngrx/entity@latest
pnpm install
npx nx migrate --run-migrations
# 3) Lock CI toolchain (example GitHub Actions)# .github/workflows/ci.yml (snippet)
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node: [20]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node }}, cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm nx run-many -t build,test,lint -c ci1) Create the runway
I use Nx to stage upgrades and keep features shipping. Each slice has a flag to flip at runtime.
Branch per slice (shell, shared libs, feature areas)
Pin Node/PNPM/TS in CI to avoid drift
Run migrations locally and in CI the same way
2) Run framework + workspace migrations
Prefer Nx migrations so you can review changes. Keep strict mode on to flush risky types before Signals work.
Angular CLI/Core to 20
NgRx to latest
TypeScript to a supported version with strict on
Commands I actually run
State management: breaking changes and a safe path with Signals
// user.selectors.ts (legacy stays intact)
export const selectUsers = createSelector(selectUserState, s => s.list);
export const selectActiveTeamId = createSelector(selectTeamState, s => s.activeId);
// users.component.ts (Angular 20)
import { toSignal, computed, effect, signal, input } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectUsers, selectActiveTeamId } from '../state';
@Component({
selector: 'ux-users',
templateUrl: './users.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true
})
export class UsersComponent {
// reactive input using signals
filters = input<{ q?: string }>();
private users$ = this.store.select(selectUsers);
private teamId$ = this.store.select(selectActiveTeamId);
users = toSignal(this.users$, { initialValue: [] });
teamId = toSignal(this.teamId$, { initialValue: null });
// derived state as computed
visible = computed(() => {
const q = this.filters()?.q?.toLowerCase() ?? '';
return this.users().filter(u => u.teamId === this.teamId() && u.name.toLowerCase().includes(q));
});
constructor(private store: Store) {
// optional side-effect for analytics
effect(() => {
track('users_visible_count', { count: this.visible().length }); // GA4/Firebase
});
}
}// users.store.ts (NgRx SignalStore composing server + selection)
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
interface UsersState {
list: User[];
loading: boolean;
}
const initialState: UsersState = { list: [], loading: false };
export const UsersStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ list }) => ({
total: computed(() => list().length),
})),
withMethods((store) => ({
setLoading(loading: boolean) { patchState(store, { loading }); },
setUsers(list: User[]) { patchState(store, { list }); }
}))
);
// Feature components can consume UsersStore signals alongside legacy NgRx selectors.Migrate NgRx incrementally (don’t rewrite)
Your existing store stays; Signals ride alongside. This removes risk and lets you measure ROI per component.
Upgrade NgRx to latest and keep reducers/selectors
Adopt createFeature/createActionGroup if you’re still on v12 APIs
Introduce SignalStore for new features while you stabilize legacy
Bridge selectors to Signals
This trims change detection work without tearing out NgRx.
Use toSignal on hot selectors
Memoize derived computed signals in components
Prefer signal inputs over @Input() + setters
Code: selector to Signal + computed
Code: a small SignalStore that composes NgRx data
RxJS migration landmines to fix in CI
// BEFORE (old signatures & stale cache risk)
const data$ = http.get<Item[]>(url).pipe(
switchMap(items => combineLatest(items.map(i => loadDetails$(i.id, (a, b) => ({ a, b }))))), // resultSelector misuse
shareReplay(1) // no refCount, stays subscribed forever
);
// AFTER
const data$ = http.get<Item[]>(url).pipe(
switchMap(items => combineLatest(items.map(i => loadDetails$(i.id)))),
map(detailsArr => detailsArr.map(d => ({ a: d.a, b: d.b }))),
shareReplay({ bufferSize: 1, refCount: true })
);
// toPromise removal
async function loadOnce() {
const v = await firstValueFrom(data$);
return v;
}What breaks moving from older code to modern RxJS
Most Angular 20 projects stick with RxJS 7.8.x; some pilots use RxJS 8. Either way, clean these up in CI before Signals work, or you’ll misattribute performance issues.
toPromise was removed; use firstValueFrom/lastValueFrom
Deprecated resultSelector overloads are gone
shareReplay(1) without refCount can leak and stale-cache
Type inference changes reveal bad any usage
Fixes I apply repeatedly
Change detection: cut renders with Signals and OnPush
<!-- BEFORE -->
<div *ngFor="let u of users$ | async">{{ u.name }}</div>
<!-- AFTER: bridge once, compute locally -->
<div *ngFor="let u of visible()">{{ u.name }}</div>// App shell: ensure OnPush everywhere and avoid manual detectChanges
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
selector: 'app-shell',
template: `<router-outlet />`
})
export class AppShell {}Practical targets
In an airport kiosk project, Signals + input() let us isolate device status updates (printers, barcode scanners) without repainting the whole view. Same trick works on dashboards built with PrimeNG or Angular Material.
High-churn dashboards (charts, tables, counters)
Large forms where validation thrashes
Kiosk screens with device polling
Template changes that pay off
Measure it
At AngularUX I push flame charts and render counts into CI to block regressions. That’s how we keep gitPlumbers at 99.98% uptime across modernizations.
Angular DevTools: Profiler -> record and count renders
Core Web Vitals: watch INP and LCP movements
GA4/BigQuery: custom event signals_render_count by route
When to Hire an Angular Developer for Legacy Rescue
Good moments to bring in help
If you need a remote Angular developer to steady the codebase, I can audit state/RxJS/change detection and ship a plan within a week. We’ll protect delivery while upgrading. See how we rescue chaotic code at gitPlumbers (70% velocity lift).
Missed SLAs due to upgrade blockers or flaky tests
Leadership wants Signals ROI with proof, not vibes
You need a slice-by-slice plan that won’t pause feature work
End-to-end example: upgrading a dashboard slice
// charts/traffic-card.component.ts
@Component({
selector: 'ux-traffic-card',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ux-chart [data]="series()" [loading]="loading()"></ux-chart>
`
})
export class TrafficCardComponent {
private traffic$ = this.store.select(selectTrafficByRegion);
private loading$ = this.store.select(selectTrafficLoading);
traffic = toSignal(this.traffic$, { initialValue: [] });
loading = toSignal(this.loading$, { initialValue: false });
series = computed(() => transformToHighcharts(this.traffic()));
constructor(private store: Store) {}
}What we change first
In a telecom analytics dashboard, this cut renders on critical cards by 86% and stabilized WebSocket updates.
Module -> standalone for the slice
Wrap three hot selectors with toSignal
Replace two async pipes feeding Highcharts with computed signals
Code sketch
What to instrument next to prove the upgrade
Telemetry hooks
Executives don’t want “it feels faster.” Show render counts dropping, INP improving, and selector churn reduced. Attach the numbers to PRs.
Log render_count, change_detection_cycles, and selector_hits
Attach commit SHAs and feature flags to GA4 events
BigQuery dashboard comparing pre/post per route
FAQs for upgrades and engagements
Key takeaways
- Treat 12–15 to 20 as two tracks: framework/tooling upgrades first, then Signals adoption behind flags.
- Bridge Observables to Signals with toSignal and NgRx SignalStore before deleting NgRx or async pipes.
- Fix RxJS landmines (resultSelector removal, shareReplay config, toPromise removal) early in CI.
- Cut render counts by moving high-churn streams to Signals + computed and using input() for reactive inputs.
- Prove the win: track render counts and INP/LCP in CI with Angular DevTools traces and GA4 events.
- Use feature flags and Nx projects to ship the upgrade in slices without pausing delivery.
Implementation checklist
- Create a migration branch and run Angular/Nx migrations; lock Node/TS versions in CI.
- Upgrade NgRx to latest; introduce SignalStore for new features—don’t rewrite everything.
- Wrap legacy selectors with toSignal; replace only hot paths/components first.
- Remove deprecated RxJS resultSelector overloads; configure shareReplay with refCount.
- Switch components to ChangeDetectionStrategy.OnPush and adopt signal inputs.
- Instrument flame charts and render count metrics; compare before/after in PRs.
Questions we hear from teams
- How long does a typical Angular 12–15 to 20 upgrade take?
- For a medium dashboard, 4–8 weeks end-to-end: 1 week assessment, 1–2 weeks framework/NgRx/RxJS cleanup, then 2–4 weeks Signals adoption on hot paths with CI guardrails. We keep features shipping with flags.
- Do we need to replace NgRx with Signals?
- No. Keep NgRx for global state and adopt Signals/SignalStore where it simplifies components. Bridge with toSignal and migrate selectively. Many teams run NgRx selectors and SignalStore side-by-side successfully.
- What breaks most often with RxJS during upgrades?
- Deprecated resultSelector overloads, toPromise removal, and shareReplay misconfiguration. Fix with map, firstValueFrom/lastValueFrom, and shareReplay({ bufferSize: 1, refCount: true }). Tighten TypeScript to catch hidden issues.
- What does an Angular consultant actually deliver here?
- A migration plan, PR-ready changes, CI upgrades, and instrumentation showing render count, INP/LCP, and selector churn improvement. I also coach your team to adopt Signals safely and document patterns recruiters can inspect.
- How much does it cost to hire an Angular developer for this upgrade?
- Scoped upgrades with telemetry typically start at a few weeks of contracting. I offer fixed-scope assessments and milestone-based delivery. Discovery call in 48 hours; assessment within a week; then sprint-based execution.
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