
Rescue Legacy AngularJS/9–14 State Without a Rewrite: Signals Adapters, Facades, and a Strangler Path That Ships
Modernize state to Angular 20+ Signals and SignalStore while features keep shipping—no big-bang rewrite required.
Don’t rewrite—strangle. Wrap legacy state with Signals today, replace slices with SignalStore tomorrow, and keep shipping the whole time.Back to all posts
The Jittery Dashboard Moment
What I see in the field
I’m Matthew Charlton. I’ve rescued AngularJS and Angular 9–14 apps in aviation, media, telecom, insurance, IoT, and fintech. The pattern is familiar: a KPI dashboard jitters, a modal opens 300ms late, memory creeps after every route change. Teams want Signals but can’t stop shipping features. Good news: you don’t need a rewrite. You need a Signals adapter layer, a facade, and a strangler plan that you can defend in CI.
Runaway change detection on every keystroke
Subscriptions orphaned in feature flags
Selectors feeding components and pipes directly
Homegrown stores + NgRx + Subjects all in one app
Why Angular 12 Apps Break During Signals Migration
Risk areas to avoid
Signals shift your mental model from streams to pull-based computation. If you rip out NgRx or legacy subjects too quickly, you’ll break effect chains, selectors, or memoization. The workable path is incremental: wrap legacy in read-only signals, move mutations behind a facade, then replace slices with SignalStore when tests and telemetry say it’s safe.
Eagerly replacing NgRx with SignalStore across the app
Mixing mutable shared services with computed signals
Tight coupling to zone.js-based side effects
Skipping telemetry, so regressions hide until prod
The Incremental Plan: Adapters, Facades, and a Strangler Fig
// Step 2–3: Signals facade over legacy NgRx selectors and subjects
import { Injectable, computed, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Store } from '@ngrx/store';
import { selectUsers, selectSelectedUserId } from './users.selectors';
import * as UsersActions from './users.actions';
@Injectable({ providedIn: 'root' })
export class UsersFacade {
private store = inject(Store);
// Read-only signals wrapping existing selectors
readonly users = toSignal(this.store.select(selectUsers), { initialValue: [] as ReadonlyArray<User> });
readonly selectedUserId = toSignal(this.store.select(selectSelectedUserId), { initialValue: null as string | null });
// Derived state with computed
readonly selectedUser = computed(() => this.users().find(u => u.id === this.selectedUserId()) ?? null);
// Write methods keep legacy actions intact for now
selectUser(id: string) {
this.store.dispatch(UsersActions.selectUser({ id }));
}
loadUsers() {
this.store.dispatch(UsersActions.loadUsers());
}
}// Step 4: Replace one slice with @ngrx/signals SignalStore
import { signalStore, withState, withMethods, patchState, withComputed } from '@ngrx/signals';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface UsersState {
users: ReadonlyArray<User>;
loading: boolean;
error?: string;
selectedUserId?: string | null;
}
const initialState: UsersState = { users: [], loading: false, selectedUserId: null };
export const UsersStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ users, selectedUserId }) => ({
selectedUser: () => users().find(u => u.id === selectedUserId()) ?? null,
hasError: () => !!users() && !!selectedUserId(),
})),
withMethods((state) => {
const http = inject(HttpClient);
return {
async loadUsers() {
patchState(state, { loading: true, error: undefined });
try {
const users = await http.get<User[]>('/api/users').toPromise();
patchState(state, { users: users ?? [], loading: false });
} catch (e: any) {
patchState(state, { loading: false, error: e?.message ?? 'Load failed' });
}
},
selectUser(id: string | null) { patchState(state, { selectedUserId: id }); }
};
})
);<!-- Step 3–4: Component stays simple, Signals do the heavy lifting -->
<section *ngIf="usersStore.loading(); else ready">
<p-progressSpinner></p-progressSpinner>
</section>
<ng-template #ready>
<p-table [value]="usersStore.users()" selectionMode="single" [(selection)]="sel" (onRowSelect)="usersStore.selectUser(sel?.id)">
<ng-template pTemplate="header">
<tr><th>Name</th><th>Role</th></tr>
</ng-template>
<ng-template pTemplate="body" let-user>
<tr><td>{{ user.name }}</td><td>{{ user.role }}</td></tr>
</ng-template>
</p-table>
</ng-template>Step 1: Baseline and budget your wins
Before touching code, capture baselines. On a telecom ads analytics dashboard, we cut interactive latency by 28% just by stopping wasteful zone churn with Signals. If you need to hire an Angular developer to run a fast assessment, I can deliver a state audit within a week.
Angular DevTools: profile change detection and component updates
Core Web Vitals: INP and LCP in Lighthouse CI
Memory: Chrome heap snapshots on route churn
Business KPIs: task success and time-to-insight
Step 2: Wrap legacy selectors and subjects in read-only Signals
This step delivers immediate wins with almost zero risk. Components consume signals; legacy pipelines keep working behind the scenes.
Keep NgRx selectors and service subjects; expose toSignal wrappers
No mutations yet—preserve existing dispatch/next calls
Use computed() for derived state to simplify components
Step 3: Introduce a Signals Facade
The facade is your seam. It hides whether the source is NgRx, a Subject, Firebase, or a SignalStore slice.
Centralize reads (signals) and writes (methods)
Guard with feature flags and contract tests
Route mutations to legacy actions/effects until replaced
Step 4: Strangler-replace one slice with SignalStore
Ship one slice at a time: users, sessions, preferences, or a single report. In a broadcast network VPS scheduler, we moved just the calendar slice to SignalStore in two weeks while features kept shipping.
Clone state shape; keep action names for telemetry continuity
Migrate effects to methods using inject(HttpClient)
Delete legacy slice when dashboards and tests go green
Step 5: CI and telemetry guardrails
Guard the migration. When we stabilized an employee tracking and payment system, CI caught a regression where a computed signal looped a date pipe; we fixed it before prod.
Nx affected targets + Cypress canaries
Lighthouse CI budgets for INP/LCP
OpenTelemetry/GA4 events for state transitions
Firebase Logs for error taxonomy
Bridging AngularJS or Mixed Services to Signals
If you still have AngularJS pieces
You don’t need to drag AngularJS into your Angular 20 app. Keep it isolated and surface the minimum contract as signals.
Expose $rootScope events as RxJS and wrap with toSignal
Use an upgrade adapter at the boundary, not inside components
Prefer read-only signals until you strangle the service
Example: $rootScope to signal
// AngularJS -> RxJS -> Angular Signal
import { toSignal } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
function fromRootScope($rootScope: angular.IRootScopeService, event: string): Observable<any> {
return new Observable(obs => {
const off = $rootScope.$on(event, (_, data) => obs.next(data));
return () => off();
});
}
export function rootScopeEventSignal($rootScope: angular.IRootScopeService, event: string) {
return toSignal(fromRootScope($rootScope, event), { initialValue: null });
}How an Angular Consultant Approaches Signals Migration
My 6-step engagement
In an airline kiosk project, we used Docker-based hardware simulation and offline-tolerant flows. The same adapter/facade strategy let us modernize device state (printers, scanners, card readers) to Signals without blocking field deployments.
Discovery (48 hours): repo scan, dependency map, risk register
Assessment (1 week): state topology, event schema, rollout plan
Pilot (2 weeks): signals facade + 1 SignalStore slice
Expand (2–6 weeks): module-by-module strangler replacement
Harden: telemetry hooks, error taxonomy, CI budgets
Knowledge transfer: patterns, playbooks, and guardrails
When to Hire an Angular Developer for Legacy Rescue
Signals you need help now
If this reads like your week, hire an Angular expert who has shipped this path before. I bring Nx monorepo discipline, PrimeNG/Material theming, WebSocket telemetry patterns, and Firebase-backed canaries that make risky changes safe.
Critical paths cross NgRx, subjects, and AngularJS services
Feature velocity is high but regressions keep slipping into prod
Core Web Vitals (INP) and memory trends worsen after releases
You need a zero-downtime plan while upgrading Angular versions
Production Safety: Telemetry, CI, and Contract Tests
Contract test a slice before and after
Typed events let you compare behavior across the strangler boundary. On IntegrityLens (our AI-powered verification system), we used the same tactic to validate streaming state—MTTR dropped by 35%.
Define typed event schemas for state transitions
Assert same outputs for same inputs pre/post migration
Log to Firebase with correlation IDs for MTTR cuts
Nx + GitHub Actions
name: ci
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- run: pnpm install --frozen-lockfile
- run: pnpm nx affected -t lint,test,build --parallel=3
- run: pnpm nx run web:ci-lh # Lighthouse budgets for INP/LCP
- run: pnpm nx run e2e:cy:run # Cypress canariesInstrument with Angular DevTools and GA4
Ship with numbers. In a telecom analytics platform, this discipline let us keep a 99.98% uptime while modernizing state, similar to how gitPlumbers helps teams stabilize and rescue chaotic codebases.
Count component updates before/after
Track interaction-to-next-paint (INP) by route
Graph state errors by taxonomy in Firebase Logs
Mini Case Study: From Dashboard Jitters to SignalStore
Context and constraints
We started by wrapping selectors in signals and moved the heaviest charting slice (D3/Highcharts) into SignalStore. Data virtualization and typed WebSocket events fed signals directly; components dropped 30–50% of change detection cycles, and INP improved from 220ms to 130ms across key dashboards.
Angular 12, NgRx + ad-hoc subjects, PrimeNG tables/charts
Release train every two weeks; no feature freeze allowed
Multiple tenants with role-based views
Results you can take to leadership
This is the kind of outcome that makes budget conversations easier as companies plan 2025 Angular roadmaps.
-40% component updates on hot routes
-90% subscription code in components
+25% faster perceived interactions (INP)
No missed releases; zero-downtime rollout
Practical Code Patterns You Can Drop In Today
A thin signals adapter for any observable
import { Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';
export function asROSignal<T>(source$: Observable<T>, initialValue: T): Signal<T> {
return toSignal(source$, { initialValue });
}Guarded feature flag around the strangled slice
// env flag or Firebase Remote Config
@if (env.useSignalStoreUsers) {
usersStore.loadUsers();
} @else {
usersFacade.loadUsers();
}Error taxonomy hook
export type UserError =
| { kind: 'network'; path: string; status: number }
| { kind: 'parse'; path: string; message: string }
| { kind: 'auth'; message: string };
function logUserError(e: UserError) {
// send to Firebase Logs / OpenTelemetry
}Final Checklist and What to Instrument Next
Ship this week
Measure before and after. If the numbers don’t move, we adjust—no rewrites, just surgical improvements.
Wrap 2–3 selectors/subjects with signals in your hottest route
Add a facade with read methods + write methods (legacy under the hood)
Pick one slice to strangle with SignalStore; guard with a flag
Add INP/LCP budgets and a Cypress canary test in Nx CI
Instrument next
For real-time telemetry pipelines, I use typed event schemas, exponential retry, and backoff strategies with graceful UI states—these patterns carried our insurance telematics dashboards to production without drama.
Typed WebSocket events for real-time dashboards
GA4 engagement time per route after Signals adoption
Error taxonomy heatmap in Firebase Logs
Key takeaways
- You can modernize state to Signals without pausing feature work using adapters and facades.
- Start with read-only Signals over existing NgRx/subjects, then strangler‑replace slices with SignalStore.
- Guard the migration with Nx CI, contract tests, and production telemetry to avoid regressions.
- Measure wins: fewer change detection runs, lower INP, less memory churn, simpler components.
- Hire an Angular expert when critical flows span multiple legacy patterns or timelines are tight.
Implementation checklist
- Baseline metrics: Angular DevTools timings, INP/LCP, memory snapshots.
- Introduce a Signals facade over existing store/selectors/services.
- Migrate one slice with @ngrx/signals SignalStore; keep actions compatible.
- Instrument state transitions (OpenTelemetry/GA4, Firebase Logs).
- Add contract tests for events/state; protect with Nx CI + Cypress canaries.
- Repeat per feature module; remove strangled legacy bits behind flags.
Questions we hear from teams
- How much does it cost to hire an Angular developer for a Signals migration?
- It depends on scope, but typical discovery + pilot runs 2–4 weeks. I offer fixed‑fee assessments and sprint‑based delivery. You get a plan, a pilot slice in SignalStore, CI guardrails, and metrics leaders can trust.
- How long does a legacy Angular 9–14 to Signals migration take?
- A pilot slice lands in 2 weeks. Full migrations roll out module by module over 4–12 weeks, depending on complexity, testing maturity, and release cadence.
- Do we need to replace NgRx completely?
- No. Many teams keep NgRx for some flows and use SignalStore where it simplifies code. The facade lets you mix safely and move at your own pace.
- Will this break production or require a feature freeze?
- No. We run a strangler plan with feature flags, Nx CI, Cypress canaries, and telemetry. Rollouts are incremental and reversible.
- What’s included in a typical engagement?
- Repo assessment, state topology map, event schema, Signals facade, one SignalStore slice, CI/telemetry hardening, and knowledge transfer. Discovery call within 48 hours; assessment delivered within one week.
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