What changes when moving to each Angular 22.x release, and how to migrate — rated by criticality, difficulty and impact.
Angular 22.1 ships no breaking changes, but it tightens several security defaults (XSRF, Meta tags, SSR), deprecates JSONP and changes how the SSR transfer cache and prerendering behave. Most apps can update in minutes; review the high-criticality items first.
Then go through the changes below, most critical first.
provideHttpClient(. If it is missing, your app relied on the root-provided HttpClient and is affected.provideHttpClient(withXsrfConfiguration({ cookieName: '…', headerName: '…' })).X-XSRF-TOKEN (or your custom header) on mutating endpoints.provideHttpClient(withNoXsrfProtection()) and document why.// app.config.ts — no HTTP provider, XSRF interceptor was NOT applied
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};// app.config.ts — explicit and self-documenting
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withXsrfConfiguration({
cookieName: "XSRF-TOKEN",
headerName: "X-XSRF-TOKEN",
}),
),
],
};@angular/* patch releases.ng build) before merging the update.ng serve and fix the root cause (missing data at build time, browser-only APIs, failing HTTP calls).afterNextRender() or isPlatformBrowser().RenderMode.Server or RenderMode.Client in your server routes config instead of letting it fail.// app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
{ path: "dashboard/**", renderMode: RenderMode.Client },
{ path: "products/:id", renderMode: RenderMode.Server },
{ path: "**", renderMode: RenderMode.Prerender },
];on is passed, to prevent XSS through meta elements.addTag(, addTags( and updateTag( usages.on* attribute (onload, onclick…) can reach these calls, especially when the tag definition comes from external data.// Forwards whatever the CMS returns
this.meta.addTag({ ...cmsTag });const { name, property, content } = cmsTag;
this.meta.addTag({ name, property, content });withHttpTransferCacheOptions({ includeRequestsWithCredentials: true }) and/or includeNonCacheableRequests: true.includeNonCacheableRequests globally.provideClientHydration(
withHttpTransferCacheOptions({
includeRequestsWithCredentials: true,
includeNonCacheableRequests: false,
}),
);withRequestsMadeViaParent(.withFetch() from those child provideHttpClient() calls: the fetch backend is inherited from the parent.// lazy route providers
provideHttpClient(withRequestsMadeViaParent(), withFetch())// withFetch() stays in the root config only
provideHttpClient(
withRequestsMadeViaParent(),
withInterceptors([featureInterceptor]),
)jsonp(, withJsonpSupport and HttpClientJsonpModule.http.get().withJsonpSupport() / HttpClientJsonpModule once no JSONP call remains.provideHttpClient(withJsonpSupport());
this.http.jsonp<Weather>(url, "callback");provideHttpClient();
this.http.get<Weather>(url); // endpoint must send CORS headersstringToFileBuffer and fileBufferToString.new TextEncoder().encode() and new TextDecoder().decode().import { fileBufferToString, stringToFileBuffer } from "@angular-devkit/core";
const text = fileBufferToString(buffer);
const buffer = stringToFileBuffer(text);const text = new TextDecoder().decode(buffer);
const buffer = new TextEncoder().encode(text);@Injectable({ providedIn: 'root' }) classes to the @Service() decorator. It is optional and can be run folder by folder.--path src/app/feature).inject() first if you want those classes migrated too.@Injectable({ providedIn: "root" })
export class CartStore {
private readonly http = inject(HttpClient);
}@Service()
export class CartStore {
private readonly http = inject(HttpClient);
}set option called by set() and update(). It lets a derived signal write back to its source, replacing effect-based synchronization.effect() calls whose only job is to write a signal derived from another one.set option updates the source signal.rawSet argument if you also need to update the linkedSignal's own value.const order = signal<Order>(initialOrder);
const shipping = signal(order().shippingMethod);
effect(() => {
const method = shipping();
order.update((o) => ({ ...o, shippingMethod: method }));
});const order = signal<Order>(initialOrder);
const shipping = linkedSignal(() => order().shippingMethod, {
set: (method) =>
order.update((o) => ({ ...o, shippingMethod: method })),
});provideCssVarNamespacing() in app.config.ts, using a namespace that matches your APP_ID plus a separator (e.g. my-app_).--global-- (e.g. --global--brand-color). Names like --global-foo are rejected by the compiler.style.setProperty('--x', …)) with the CssVarNamespacer service so names are namespaced consistently.ng build and compare bundle budgets and server output with 22.0.