8 min read - Angular Signals in 2026: Migration Guide for Enterprise Teams
Angular Development
Published February 20, 2026 · Author Exceev Consulting
Signals are stable Angular reactivity primitives. Zoneless change detection is also stable, but it is a separate architectural choice: signals help Angular know when state changes, while zoneless mode changes how the framework schedules change detection. A large application can adopt either one incrementally.
This guide is for teams managing a large Angular codebase with real deployment constraints. It covers which state patterns are good migration candidates, how to proceed incrementally and how to measure the result in the application itself.
Signals do not replace Zone.js by themselves. They track state dependencies and notify Angular when values used by a template change. If the application also adopts zoneless change detection, Angular documents the notification mechanisms and compatibility work required. Migrate a bounded slice, test behaviour and accessibility, then compare traces and user-facing performance with the original version.
The responsibilities signals take over
Understanding what signals replace is the first step toward a practical migration plan.
Zone.js and automatic change detection
Zone.js patches asynchronous browser APIs so Angular can be notified that application state may have changed. The exact amount of subsequent work depends on the component tree and change-detection strategy.
Signals provide dependency tracking. When a template reads a signal, Angular records that dependency and marks the component when the value changes. Zoneless mode can then rely on documented notifications such as signal updates, event listeners, AsyncPipe and markForCheck instead of Zone.js.
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:
- Replace
@Input()decorators withinput()signal inputs - Replace local component state variables with
signal()calls - Replace getters used in templates with
computed()signals - 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:
- Migrate components to signals (phases 1-4 above)
- Enable
OnPushchange detection on migrated components - Test thoroughly in this hybrid mode
- Switch to
provideExperimentalZonelessChangeDetection()when coverage is sufficient - 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
setTimeoutorPromise.thencallbacks - 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.
Measure the migration in your application
There is no credible universal percentage for a signals migration. Record a baseline on representative devices and repeat the same user journeys after each change. Inspect change-detection work in Angular DevTools, browser main-thread time, transferred JavaScript, memory during long-running sessions and the user-facing Core Web Vitals relevant to the route. Keep the change only if behaviour remains correct and the measured trade-off is worthwhile.
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
Use the following sequence as a planning example, not a promised calendar:
- First slice: migrate leaf components and simple services, then validate with the existing test suite.
- Next slice: migrate intermediate components and adopt newer primitives only where they simplify the code.
- Readiness review: test zoneless compatibility, server rendering and third-party wrappers in a non-critical environment.
- Rollout: expand only after regression tests and production measurements support the decision.
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 and zoneless change detection are stable, but neither removes the need for application-specific testing. Move one bounded slice at a time, keep RxJS where streams remain the clearer abstraction and let measurements decide whether zoneless mode is worth the migration. If your team needs help with an Angular migration, let's talk.
Primary sources
- Angular Signals guide, reviewed 27 August 2026
- Angular zoneless guide, reviewed 27 August 2026
Building something with Angular?
From component libraries to full product builds, we've shipped Angular at scale across dozens of projects.