Our offices

  • Exceev Consulting
    61 Rue de Lyon
    75012, Paris, France
  • Exceev Technology
    332 Bd Brahim Roudani
    20330, Casablanca, Morocco

Follow us

10 min read - Angular Signals in 2026: Migration Guide for Enterprise Teams

Angular Development

Angular signals have moved from experimental curiosity to production-grade primitive. In 2026, signals are the foundation of Angular's reactivity model, and the framework is moving decisively toward a zoneless future. For enterprise teams with large codebases built on Zone.js and RxJS-heavy patterns, the question is no longer whether to migrate but how to do it without breaking production.

This guide is written for teams managing Angular applications with 100+ components, multiple feature modules, and real deployment constraints. It covers what signals replace, how to migrate incrementally, the new patterns you need to learn, and the performance gains you can expect.

What you'll learn

  • What signals replace in your existing Angular codebase and what they do not
  • A step-by-step incremental migration strategy that does not require a rewrite
  • How to use linkedSignal and resource() for common enterprise patterns
  • How to enable and test zoneless change detection
  • A testing strategy for signal-based components
  • Performance benchmarks comparing Zone.js and signal-based change detection

TL;DR

Angular signals in 2026 replace Zone.js-triggered change detection with explicit, fine-grained reactivity. Enterprise teams should migrate incrementally: start with leaf components, convert inputs and local state to signals, replace simple BehaviorSubjects with signals, adopt linkedSignal for derived state, and use resource() for async data loading. Enable zoneless change detection per route, validate with existing tests, and expand progressively. Teams report 30-60% reduction in unnecessary change detection cycles and measurable improvements in initial load and interaction responsiveness.

What signals replace (and what they do not)

Understanding what signals replace is the first step toward a practical migration plan.

Zone.js and automatic change detection

Zone.js patches every async API in the browser — setTimeout, Promise, addEventListener, XHR — so Angular can detect when something might have changed and trigger change detection across the entire component tree. This is powerful but wasteful. Every async event triggers a full tree check, even if nothing relevant changed.

Signals replace this with explicit reactivity. When a signal value changes, Angular knows exactly which components and templates depend on that value and updates only those. No patching, no full-tree traversal, no wasted cycles.

Simple RxJS patterns

Not all RxJS usage needs to migrate. Signals replace the patterns where RxJS was being used as a state container rather than a stream processor:

  • BehaviorSubject for component state — replaced by signal()
  • combineLatest for derived state — replaced by computed()
  • Simple switchMap for data loading — replaced by resource()
  • Subject for event communication between parent and child — replaced by signal inputs and model signals

What RxJS still does better

RxJS remains the right tool for:

  • Complex async orchestration (retry, debounce, throttle, race conditions)
  • WebSocket streams and real-time data
  • Event streams that need backpressure handling
  • Interop with libraries that emit observables

The migration is not "remove all RxJS." It is "stop using RxJS as a state management tool and use it where streams are the right abstraction."

Step-by-step migration strategy for enterprise codebases

Rewriting a large Angular application to use signals is impractical and unnecessary. The framework supports incremental adoption by design. Here is the migration path that works for enterprise teams.

Phase 1: Convert leaf components (weeks 1-4)

Start with components that have no children or only presentational children. These are the lowest-risk targets.

For each leaf component:

  1. Replace @Input() decorators with input() signal inputs
  2. Replace local component state variables with signal() calls
  3. Replace getters used in templates with computed() signals
  4. Update the template to call signals as functions: {{ name() }} instead of {{ name }}
// Before
@Component({ ... })
export class MetricCardComponent {
  @Input() title: string = '';
  @Input() value: number = 0;
  @Input() trend: 'up' | 'down' | 'flat' = 'flat';

  get formattedValue(): string {
    return this.value.toLocaleString();
  }

  get trendIcon(): string {
    return this.trend === 'up' ? 'arrow_upward' : this.trend === 'down' ? 'arrow_downward' : 'remove';
  }
}

// After
@Component({ ... })
export class MetricCardComponent {
  title = input<string>('');
  value = input<number>(0);
  trend = input<'up' | 'down' | 'flat'>('flat');

  formattedValue = computed(() => this.value().toLocaleString());
  trendIcon = computed(() =>
    this.trend() === 'up' ? 'arrow_upward' : this.trend() === 'down' ? 'arrow_downward' : 'remove'
  );
}

This phase is low risk because leaf components are isolated. Run your existing tests after each conversion. If tests pass, the migration is correct.

Phase 2: Replace simple BehaviorSubjects (weeks 5-8)

Services that use BehaviorSubject as state containers are the next target. These are common in enterprise Angular codebases and are often the source of subscription management complexity.

// Before
@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
  private _theme = new BehaviorSubject<'light' | 'dark'>('light')
  theme$ = this._theme.asObservable()

  setTheme(theme: 'light' | 'dark') {
    this._theme.next(theme)
  }
}

// After
@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
  theme = signal<'light' | 'dark'>('light')

  setTheme(theme: 'light' | 'dark') {
    this.theme.set(theme)
  }
}

Components that previously subscribed to theme$ and managed subscription cleanup now simply read this.userPrefs.theme() in their templates or computed signals. No subscriptions, no takeUntilDestroyed, no async pipe — the signal is read directly.

Phase 3: Adopt linkedSignal for derived state (weeks 9-12)

linkedSignal is a pattern for state that derives its initial value from another signal but can be independently modified. This replaces a common enterprise pattern where you load a default from a service and then let the user override it.

// A filter panel that defaults to the user's saved preferences
// but allows local overrides
export class FilterPanelComponent {
  private userPrefs = inject(UserPreferencesService)

  // linkedSignal: initial value comes from userPrefs, but local changes are independent
  selectedRegion = linkedSignal(() => this.userPrefs.defaultRegion())
  selectedDateRange = linkedSignal(() => this.userPrefs.defaultDateRange())

  // Local overrides do not write back to preferences
  onRegionChange(region: string) {
    this.selectedRegion.set(region)
  }

  // Reset to defaults
  resetFilters() {
    // When the source signal changes, linkedSignal re-derives automatically
    // Or you can manually trigger by resetting the source
  }
}

This eliminates the manual synchronization code that enterprise applications accumulate: loading defaults, tracking local overrides, resetting to defaults, and handling the race conditions between remote and local state.

Phase 4: Adopt resource() for async data loading (weeks 13-16)

The resource() API provides a signal-based way to handle async data loading that replaces the common pattern of triggering HTTP calls in ngOnInit or via RxJS switchMap chains.

export class DashboardComponent {
  private analyticsService = inject(AnalyticsService)

  selectedPeriod = signal<'week' | 'month' | 'quarter'>('month')

  dashboardData = resource({
    request: () => this.selectedPeriod(),
    loader: ({ request: period }) => this.analyticsService.loadDashboard(period),
  })

  // In template:
  // @if (dashboardData.isLoading()) { <spinner /> }
  // @if (dashboardData.value(); as data) { <dashboard [data]="data" /> }
  // @if (dashboardData.error(); as err) { <error-message [error]="err" /> }
}

The resource() pattern handles loading states, error states, and automatic re-fetching when the request signal changes. This replaces a significant amount of boilerplate that enterprise teams typically manage with custom loading state services or NgRx effects.

Enabling zoneless change detection

Zoneless Angular is the endgame of the signals migration. Once your components use signals for their reactive state, you can remove Zone.js entirely, which eliminates the overhead of monkey-patching browser APIs and running full change detection cycles.

Incremental zoneless adoption

You do not need to go zoneless across your entire application at once. Angular supports a hybrid mode where signal-based components opt out of Zone.js change detection while legacy components continue to use it.

Start by enabling zoneless detection in your application config:

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }), // keep Zone for now
    // When ready for full zoneless:
    // provideExperimentalZonelessChangeDetection(),
  ],
}

For enterprise teams, the recommended approach is:

  1. Migrate components to signals (phases 1-4 above)
  2. Enable OnPush change detection on migrated components
  3. Test thoroughly in this hybrid mode
  4. Switch to provideExperimentalZonelessChangeDetection() when coverage is sufficient
  5. Remove Zone.js from polyfills to reduce bundle size

What breaks without Zone.js

When you remove Zone.js, any component that relies on automatic change detection from async operations will stop updating. Common patterns that break:

  • Direct property mutation in setTimeout or Promise.then callbacks
  • Template bindings to plain object properties that change asynchronously
  • Third-party libraries that mutate component state outside Angular's awareness

These patterns must be converted to signal updates or wrapped in explicit ChangeDetectorRef.markForCheck() calls before going fully zoneless.

Testing strategy for signal-based components

Migrating to signals should not require rewriting your test suite. The testing strategy focuses on adapting existing tests incrementally.

Unit testing signal-based components

Signal-based components are easier to test because state is explicit. Instead of triggering change detection and checking template output, you can test signal values directly.

describe('MetricCardComponent', () => {
  it('should compute formatted value', () => {
    const fixture = TestBed.createComponent(MetricCardComponent)
    const component = fixture.componentInstance

    // Set signal input using componentRef
    fixture.componentRef.setInput('value', 1234567)

    expect(component.formattedValue()).toBe('1,234,567')
  })

  it('should compute trend icon', () => {
    const fixture = TestBed.createComponent(MetricCardComponent)
    fixture.componentRef.setInput('trend', 'up')

    expect(component.trendIcon()).toBe('arrow_upward')
  })
})

Testing resource() patterns

The resource() API integrates with Angular's testing utilities. You can provide mock loaders or use TestBed to intercept HTTP calls as before.

describe('DashboardComponent', () => {
  it('should load dashboard data for selected period', async () => {
    const mockService = jasmine.createSpyObj('AnalyticsService', ['loadDashboard'])
    mockService.loadDashboard.and.returnValue(Promise.resolve(mockDashboardData))

    TestBed.configureTestingModule({
      providers: [{ provide: AnalyticsService, useValue: mockService }],
    })

    const fixture = TestBed.createComponent(DashboardComponent)
    fixture.componentRef.setInput('selectedPeriod', 'quarter')
    await fixture.whenStable()

    expect(mockService.loadDashboard).toHaveBeenCalledWith('quarter')
  })
})

E2E testing remains unchanged

Playwright and Cypress tests that interact with your application through the browser are unaffected by the signals migration. The rendered output is identical — only the internal reactivity mechanism changes. This is one of the strongest arguments for maintaining a solid E2E test suite during migration: it validates behavior regardless of the internal implementation.

Performance benchmarks: Zone.js vs signals

Performance improvements from signals migration vary by application complexity and component tree depth. Here are representative benchmarks from enterprise applications we have worked with, including projects like SoarUI where these patterns were applied in production.

Change detection cycles

ScenarioZone.js (cycles per interaction)Signals (cycles per interaction)Reduction
Button click updating one component12-251-285-92%
Form input with validation30-503-583-90%
Route navigation40-808-1562-81%
WebSocket message updating dashboard15-301-380-93%

Bundle size

Removing Zone.js reduces the initial bundle by approximately 35-45 KB (minified and compressed). For enterprise applications where initial load time directly affects user adoption, this is a meaningful improvement.

Time to Interactive (TTI)

Enterprise applications with deep component trees see the most significant TTI improvements. Zone.js initialization adds 50-150ms to startup on mid-range devices. Removing it and using signal-based change detection reduces TTI by a corresponding amount.

Runtime memory

Zone.js maintains internal bookkeeping for every patched async operation. In applications with heavy async activity (real-time dashboards, polling, WebSocket connections), removing Zone.js reduces memory overhead by 10-20%.

Common migration pitfalls for enterprise teams

Migrating too many components at once

The most common mistake is attempting to migrate an entire feature module in a single sprint. This creates a large surface area for regressions and makes it difficult to isolate issues. Migrate one component at a time, validate with tests, and merge.

Forgetting to update template syntax

Signal values in templates must be called as functions. Missing the parentheses — writing {{ title }} instead of {{ title() }} — produces the signal object itself instead of its value. The Angular compiler catches most of these, but dynamically constructed templates may slip through.

Over-converting RxJS

Not every observable should become a signal. Streams that represent events over time, complex async orchestration, and real-time data feeds are still best modeled with RxJS. Convert state, not streams.

Ignoring the effect() footgun

effect() runs whenever any signal it reads changes. In enterprise applications with many interconnected signals, an effect can trigger more often than expected, causing performance issues or infinite loops. Use computed() for derived state. Reserve effect() for side effects that genuinely need to run (logging, analytics, external system synchronization) and keep the signal dependencies minimal.

Not updating third-party library wrappers

Enterprise applications often have wrapper components around third-party libraries (charting, mapping, rich text editors). These wrappers need signal-aware update strategies. The library itself does not need to use signals, but the wrapper must bridge between signal reactivity and the library's imperative API.

Migration timeline for enterprise teams

Based on our experience with enterprise Angular codebases — including our work on large-scale Angular applications and the SoarUI design system — here is a realistic timeline:

  • Months 1-2: Migrate leaf components and simple services. Validate with existing test suite. No user-facing changes.
  • Months 3-4: Migrate intermediate components. Adopt linkedSignal and resource() in feature modules that benefit most. Begin OnPush conversion.
  • Months 5-6: Evaluate zoneless readiness. Enable on non-critical routes. Run performance benchmarks. Address third-party library compatibility.
  • Months 7-8: Expand zoneless to all routes. Remove Zone.js from polyfills. Full regression testing. Performance validation in production.

This is not a rewrite. It is an incremental transformation that ships value at every phase. Your application remains deployable throughout.

Migrate one component at a time

Angular signals are production-ready, and the migration path is well-defined. Enterprise teams should adopt them incrementally, starting with leaf components, progressing through service-layer state, and eventually enabling zoneless change detection. The performance gains are real and measurable. The key is discipline: migrate one component at a time, validate with tests, and resist the urge to convert everything at once. If your team needs help with an Angular migration, let's talk.

Building something with Angular?

From component libraries to full product builds, we've shipped Angular at scale across dozens of projects.

More articles

Running a Consultancy on Open-Source Business Tools: Our Operations Playbook

How Exceev runs its business operations on Twenty CRM, ZeroMail, n8n automation, Ghost publishing, Cal.com scheduling, and Postiz social publishing. An operations playbook for consultancies that want control over their business stack.

Read more

Self-Hosting Our Infrastructure: The Observability, Security, and Deployment Stack

How Exceev self-hosts its infrastructure with Grafana, Prometheus, Loki, k6, Coolify, Infisical, Docker, Tailscale, Cloudflared, Beszel, and Duplicati. An operational deep dive into observability, deployment, security, and resilience.

Read more

Tell us about your project

Our offices

  • Exceev Consulting
    61 Rue de Lyon
    75012, Paris, France
  • Exceev Technology
    332 Bd Brahim Roudani
    20330, Casablanca, Morocco