Modernize State to Signals in Legacy AngularJS/9–14 Apps Without a Rewrite: Adapters, SignalStore, and Strangler Patterns

Modernize State to Signals in Legacy AngularJS/9–14 Apps Without a Rewrite: Adapters, SignalStore, and Strangler Patterns

A practical, low‑risk path to upgrade state management to Angular 20+ Signals—while legacy UI keeps shipping.

“You don’t need a rewrite to get Signals wins. Wrap what you have, move the hot paths, measure, and keep shipping.”
Back to all posts

If your dashboard jitters when data spikes, you’re not alone. I’ve stabilized employee trackers at a global entertainment company and modernized airport kiosks for a major airline—often with Angular 9–14 code, sometimes AngularJS. Budgets didn’t allow rewrites, but leadership wanted Angular 20 Signals benefits now. Here’s the exact statecraft I use: adapters, SignalStore slices, and a strangler pattern that keeps production calm.

Why Jumping to Signals Without a Rewrite Matters Now

The 2025 reality: shipping beats rewriting

As companies plan 2025 Angular roadmaps, leadership wants tactile UX gains—faster dashboards, stable forms, fewer janky frames—without burning quarters on a rewrite. Signals give us deterministic state, fine-grained reactivity, and simpler mental models. We can graft Signals onto legacy AngularJS/9–14 code and start harvesting wins next sprint.

  • Budgets favor incremental wins

  • Angular 20+ Signals boost UX immediately

  • Rewrites stall delivery for quarters

Where Signals move the needle

On an ads analytics dashboard for a telecom provider, switching the hottest charts to Signals dropped component recalculations by ~38% and raised Lighthouse performance by 12 points—no visual changes, no rewrite. Similar patterns held on a telematics platform: smoother maps and fewer flame chart spikes in Angular DevTools.

  • Deterministic reactivity

  • Lower change detection pressure

  • Simpler local reasoning

The Safe Strangler Plan for Legacy State

Step 1: Inventory and classify state

Tag each state source by: read frequency, write complexity, and UX criticality. In Nx, I drop this as a markdown checklist beside the affected libraries so PRs stay scoped and reviewable.

  • Services with BehaviorSubject

  • NgRx feature stores/selectors

  • AngularJS $rootScope/$broadcast

  • WebSocket/event streams

Step 2: Introduce a Signals Facade (read-only first)

Create a facade that exposes Signals selectors wrapping the current store or services. Components swap to signals-based reads with zero action code changes. This alone trims rerenders and removes accidental async pipe cascades.

  • Use selectSignal()/toSignal()

  • Keep existing actions/effects

  • Don’t change reducers yet

Step 3: Carve critical slices into SignalStore

Move high-churn, UI-centric slices (filters, selection, ephemeral UI state) into SignalStore first. Preserve action/method names to reduce diff size. Cross-cutting or side-effect-heavy flows (auth, websockets) can remain in NgRx until we’re ready.

  • Start with hot paths

  • Keep NgRx for cross-cutting effects

  • Offer identical API shape

Step 4: Flip components incrementally

I enable toggles via Firebase Remote Config or an environment flag. We migrate the container, validate telemetry, then proceed to children. If KPIs regress, roll back by flipping a flag—no code reverts needed.

  • One container at a time

  • Add effect() for imperative syncing

  • Feature-flag the flip

Step 5: Prove it with metrics

I set budgets in CI and emit analytics on key state updates—e.g., how long a filter change takes to settle charts. In one case we cut interaction-to-next-paint from 220ms to 120ms simply by localizing derived state with computed().

  • Angular DevTools signals graph

  • Lighthouse + Core Web Vitals

  • Firebase Performance traces

Signals Adapters That Work with NgRx and Legacy Services

// facade/orders.facade.ts
import { Injectable, computed, inject } from '@angular/core';
import { Store, selectSignal } from '@ngrx/store';
import { toSignal } from '@angular/core/rxjs-interop';
import * as Orders from './orders.selectors';
import * as OrdersActions from './orders.actions';
import { OrdersService } from './orders.service';

@Injectable({ providedIn: 'root' })
export class OrdersFacade {
  private store = inject(Store);
  private svc = inject(OrdersService); // legacy Observable API

  // NgRx → Signals
  orders = selectSignal(this.store, Orders.selectAll);
  loading = selectSignal(this.store, Orders.selectLoading);

  // Observable service → Signal
  svcHealth = toSignal(this.svc.health$(), { initialValue: 'unknown' });

  // Derived with computed()
  openOrders = computed(() => this.orders().filter(o => o.status === 'open'));

  // Preserve dispatch API
  refresh() { this.store.dispatch(OrdersActions.refresh()); }
}

// component usage (Angular 20+ template)
@Component({
  selector: 'orders-table',
  template: `
    <p-progressBar *ngIf="facade.loading()" mode="indeterminate"></p-progressBar>
    <orders-grid [rows]="facade.openOrders()"></orders-grid>
  `
})
export class OrdersTableComponent { constructor(public facade: OrdersFacade) {} }

Facade wrapping NgRx selectors with Signals

This gives you Signals benefits with near-zero surface change. Components still dispatch actions; they just read state via signals.

  • Use selectSignal from NgRx 16+

  • Expose readonly signals

  • Keep dispatch API

From Observable services to Signals

Legacy BehaviorSubjects can be wrapped and gradually replaced. Keep the original service as the single writer while components consume Signals.

  • toSignal for Observables

  • computed for derived slices

  • effect for bridging writes

Introducing SignalStore Slices Without Breaking Features

// stores/filters.store.ts (NgRx SignalStore)
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';

interface FilterState {
  query: string;
  status: 'all' | 'open' | 'closed';
}

const initial: FilterState = { query: '', status: 'all' };

export const FiltersStore = signalStore(
  { providedIn: 'root' },
  withState(initial),
  withComputed(({ query, status }) => ({
    hasQuery: () => query().length > 0,
    isNarrow: () => status() !== 'all'
  })),
  withMethods((store) => ({
    setQuery(query: string) { patchState(store, { query }); },
    setStatus(status: FilterState['status']) { patchState(store, { status }); },
    reset() { patchState(store, initial); }
  }))
);

// filters.facade.ts — preserving old API
@Injectable({ providedIn: 'root' })
export class FiltersFacade {
  constructor(private filters: FiltersStore) {}
  query = this.filters.query; // signal read
  status = this.filters.status; // signal read
  hasQuery = this.filters.hasQuery; // computed signal

  setQuery(v: string) { this.filters.setQuery(v); }
  setStatus(s: 'all' | 'open' | 'closed') { this.filters.setStatus(s); }
  reset() { this.filters.reset(); }
}

Why SignalStore here

I start with filter state, selections, and ephemeral UI. Keep NgRx for server mutations and cross-cutting concerns; migrate later once benefits are clear.

  • Local UI state is a perfect fit

  • Deterministic updates

  • Less boilerplate than reducers

Minimal API churn

If the old API was setFilter(value), keep it. Add internal analytics on mutation to track UX impact—hiring teams love that traceability.

  • Mirror old facade methods

  • Keep input/output shapes

  • Add analytics hooks

AngularJS Hybrids and Microfrontends: How to Bridge

// bridging: from Signal to Observable for AngularJS consumers
import { toObservable } from '@angular/core/rxjs-interop';
import { signal } from '@angular/core';
import { map, startWith } from 'rxjs/operators';

const count = signal(0);
export const count$ = toObservable(count).pipe(
  map(v => ({ value: v })),
  startWith({ value: 0 })
);

// AngularJS controller can $scope.$watch via async pipe or subscribe

// downgrade an Angular component into AngularJS
import { downgradeComponent } from '@angular/upgrade/static';
import { OrdersTableComponent } from './orders-table.component';

angular.module('legacy').directive('ordersTable', downgradeComponent({
  component: OrdersTableComponent
}));

Hybrid with ngUpgrade

For an insurance telematics dashboard, we introduced Angular 20 widgets into an AngularJS shell. Signals powered the widget internals while AngularJS consumed Observables derived from signals—no watcher storms.

  • Downgrade Angular 20 islands

  • Bridge signals via toObservable

  • Keep AngularJS controllers stable

Microfrontend islands

When upgrade is not feasible, I ship self-contained Angular 20+ islands with Signals and a narrow customEvent API. We forward only typed events to the shell.

  • Expose a typed events contract

  • Use Web Components or Module Federation

  • Isolate state leaks

Instrumentation, Guardrails, and A11y Checks

# .github/workflows/ci.yml (excerpt)
name: ci
on: [pull_request]
jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx nx affected -t lint,test,build --parallel=3
      - run: npx lhci autorun --config=./lighthouse-ci.json
      - run: npx cypress run --component --browser chrome

CI budgets and telemetry

I wire Nx in GitHub Actions to run unit, Cypress E2E, Lighthouse, and axe-core on each flagged flip. Bundle budgets prevent regressions. We emit analytics when signals-based filters or grids update to verify latency targets.

  • Nx + GitHub Actions

  • Lighthouse CI and a11y budgets

  • GA4 + Firebase Performance

Angular DevTools for Signals

DevTools is invaluable here—watch derived selectors for thrash. On an airport kiosk sim (Docker-based hardware), we caught a barcode-scan effect causing needless recompute; a small memoization fix stabilized the flow.

  • Track dependency graphs

  • Spot accidental recomputes

  • Validate effect boundaries

How an Angular Consultant Approaches Signals Migration

Typical engagement timeline

I start with a one-week assessment: flame charts, DevTools traces, NgRx map, and a Signals PoC in a safe feature flag. Then we migrate the top 1–2 hot paths (filters/tables/charts). Finally, we scale slices, add CI guardrails, and document contracts.

  • Week 1: assessment + PoC

  • Weeks 2–3: hot-path migration

  • Weeks 4–6: expand + harden

Deliverables you can bank on

You’ll get a strangler plan, adapter libraries in Nx, stable SignalStore slices, feature flags, and dashboards that prove improvements. When you hire an Angular developer, this is the accountability you want.

  • Adapter facades + SignalStore slices

  • Roll-forward/rollback flags

  • Telemetry dashboards

When to Hire an Angular Developer for Legacy Rescue

Good signals you should bring help

If your team is stuck between rewrite-or-bust and a noisy backlog, bring in an Angular expert who has shipped this exact path. I’ve moved AngularJS/9–14 systems to Signals while keeping production stable and stakeholders happy.

  • Janky dashboards under load

  • NgRx complexity blocking features

  • AngularJS shell with critical deadlines

What success looks like

On IntegrityLens and SageStepper I apply the same guardrails. gitPlumbers maintains 99.98% uptime across continuous modernizations. The playbook scales across domains: analytics, kiosks, telematics, and accounting dashboards.

  • 20–40% fewer recomputes on hot views

  • +10–20 Lighthouse points on key pages

  • Feature velocity maintained

Related Resources

Key takeaways

  • You can adopt Angular 20+ Signals on legacy AngularJS/9–14 apps without a full rewrite using adapters and facades.
  • Start by wrapping existing Observables/NgRx into Signals, then move hot paths and shared state to SignalStore slices.
  • Use a strangler pattern: component-by-component swaps, feature flags, and CI guardrails to avoid regressions.
  • Measure impact with Angular DevTools, Lighthouse, Firebase Performance, and UX telemetry events.
  • This approach works for hybrid ngUpgrade setups and microfrontends; keep shipping while you modernize.

Implementation checklist

  • Inventory state sources: services, BehaviorSubjects, NgRx stores, and AngularJS $rootScope events.
  • Introduce a Signals Facade per domain: wrap select() with selectSignal/toSignal.
  • Add SignalStore for new/critical slices; keep NgRx for complex effects until parity.
  • Flip components incrementally: async pipe → signal reads; add effect() for imperative reactions.
  • Guard with feature flags and CI: a11y, Lighthouse budgets, bundle sizes, and smoke E2Es.
  • Instrument Core Web Vitals and change detection counts; verify improvements before expanding scope.

Questions we hear from teams

Do we need to rewrite NgRx if we move to Signals?
No. Start by exposing NgRx selectors as Signals with selectSignal. Migrate UI-centric slices to SignalStore where it reduces boilerplate. Keep NgRx for effects-heavy, cross-cutting flows until parity is clear.
How long does a Signals modernization take?
A targeted rescue is 2–4 weeks for assessment and hot-path migration, 4–8 weeks to expand slices and harden CI/telemetry. Scope depends on feature count, NgRx complexity, and whether an AngularJS shell is involved.
What does an Angular consultant deliver on this engagement?
A strangler migration plan, adapter facades, SignalStore slices, feature flags for rollbacks, and telemetry dashboards proving UX gains. Expect docs, tests, and guardrails so your team can continue confidently.
How much does it cost to hire an Angular developer for this work?
Costs vary by scope and timelines. Typical rescue pilots start as fixed-fee assessments, then move to weekly engagement. Book a discovery call to align on hot paths and KPIs before we price options.
Will this break production?
The process is flag-driven and incremental. We flip components to Signals behind feature flags, verify with CI and telemetry, and roll back instantly if needed. No big-bang deployments required.

Ready to level up your Angular experience?

Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.

Hire Matthew – Remote Angular Expert, Available Now See how I rescue chaotic code with gitPlumbers

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
NG Wave Component Library

Related resources