DEV Community

Cover image for Angular 22.1 Is Out — Here's Every Feature Worth Your Attention
Rajat
Rajat

Posted on

Angular 22.1 Is Out — Here's Every Feature Worth Your Attention

A practical, code-first walkthrough of the new release cadence, the @Service() migration, linkedSignal's custom setter, untracked interceptors, and what it all means for your next sprint

Quick question before we dive in: when was the last time you upgraded Angular and actually used the new stuff within the same week, instead of bookmarking the changelog and forgetting about it?

If you are like most of us, minor releases fly by. We update the version number, run the tests, ship, and move on. Angular 22.1 is one of those releases that is easy to skim past — it is not a major version, there is no big migration guide, no press release. But tucked inside it are a handful of changes that quietly fix real pain points: a migration schematic for the new @Service() decorator, a smarter linkedSignal, interceptors that stop leaking into your effects, and — maybe the biggest news of all — a completely new release schedule that changes how every Angular team should plan the next two years.

In this article we will go feature by feature, with runnable code, so you walk away knowing exactly what changed and how to use it today.

By the end of this article, you will know:

  • Why Angular is moving to one major release per year, and what that means for your upgrade planning
  • How to migrate existing services to @Service() with a single CLI command
  • How linkedSignal's new custom setter gives you control over writes without losing derived state
  • Why your effects were secretly re-running because of HTTP interceptors, and how 22.1 fixes it
  • How to write unit tests for signal-based, standalone components using current Angular syntax
  • Whether this is worth its own follow-up series (spoiler: probably yes, and I want your vote on it)

All examples below use standalone components, the new control-flow syntax (@if, @for), and signals. No NgModule, no *ngIf, no *ngFor, no @Input() decorators pretending to be modern. If you have been burned by outdated tutorials before, this one is current as of Angular 22.1.0.

Got a preference on how deep we go? Drop a comment before you finish reading — I am deciding whether to turn this into a five-part deep-dive series, and your input actually shapes what gets written next.

1. The Big One: Angular Is Switching to a Yearly Major Release

Let's start with the change that is not really a "feature" but affects everyone who has ever groaned at an ng update breaking something two Fridays before a release.

Since Angular 4, the team has shipped a major version every six months. As of 22.1, that is over.

Here is what changes concretely:

  • One major version per year, landing every June
  • Version 23 is now expected in June 2027, not November 2026 as the old cadence would have suggested
  • Version 24 follows in June 2028, and so on
  • Each major version gets two years of support instead of eighteen months
  • Minor releases keep shipping roughly every two months, so expect four to six minors between majors

If you manage upgrade cadence for a team, this is worth putting straight into your roadmap. Fewer major-version scrambles, longer support windows, and a predictable June release date instead of guessing whether the next breaking change lands before or after your quarterly freeze.

Discussion point: does a slower major cadence make your team more comfortable upgrading promptly, or does it just mean bigger, scarier majors when they finally land? I would genuinely like to know how your team plans upgrades — tell me in the comments.

2. Migrating to @Service() Without Touching Every File by Hand

Angular 22 introduced the @Service() decorator as a leaner alternative to @Injectable({ providedIn: 'root' }). The CLI has been generating new services with it since then, but there was no automated way to migrate services you had already written. That gap is closed in 22.1.

Run this from your project root:

ng generate @angular/core:service
Enter fullscreen mode Exit fullscreen mode

It takes code like this:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserService {
  getCurrentUser() {
    // fetch logic here
  }
}
Enter fullscreen mode Exit fullscreen mode

And rewrites it to this:

import { Service } from '@angular/core';

@Service()
export class UserService {
  getCurrentUser() {
    // fetch logic here
  }
}
Enter fullscreen mode Exit fullscreen mode

A bare @Injectable() with no arguments becomes @Service() the same way.

The schematic is deliberately conservative. It will skip:

  • Services that rely on constructor-based dependency injection patterns it cannot safely rewrite
  • Services passing options other than providedIn
  • Anything where providedIn is not 'root'

That is a good thing. A migration tool that silently changes DI scope in a large codebase is a migration tool nobody trusts. Run it with --dry-run first so you know exactly how many files are affected before committing to the change.

ng generate @angular/core:service --dry-run
Enter fullscreen mode Exit fullscreen mode

If this saved you from writing a regex-based codemod at 5 p.m. on a Friday, that is exactly the kind of small win worth a clap — it tells me these practical, "here is the command, here is the diff" breakdowns are what you want more of.

3. linkedSignal Gets a Custom Setter

If you have not used linkedSignal yet, here is the short version: it behaves like computed, except you can also write to it directly. That makes it perfect for "selected item" style state that should reset whenever its source list changes, but also needs to be manually overridable by the user.

readonly items = signal<Array<ItemModel>>([]);
protected readonly selectedItem = linkedSignal(() => this.items()[0]);
Enter fullscreen mode Exit fullscreen mode

Every time items changes, selectedItem recalculates to the first item — unless you write to it yourself, in which case your write wins until the source changes again.

What was missing was control over what happens when someone writes to it. Angular 22.1 adds a set option for exactly that:

protected readonly selectedItem = linkedSignal(() => this.items()[0], {
  set: (item: ItemModel) => {
    const items = this.items();
    if (items.indexOf(item) < 0) {
      this.items.set([item, ...items]);
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Now, writing an item that is not already in the list adds it to the front instead of silently failing or producing an inconsistent state. This is the kind of API that looks small in a changelog and saves you an entire custom wrapper class in practice.

Here is a full standalone component putting it together, using the current control-flow syntax:

import { Component, signal, linkedSignal } from '@angular/core';

interface ItemModel {
  id: number;
  name: string;
}

@Component({
  selector: 'app-item-picker',
  template: `
    <ul>
      @for (item of items(); track item.id) {
        <li
          [class.active]="item.id === selectedItem().id"
          (click)="selectedItem.set(item)">
          {{ item.name }}
        </li>
      }
    </ul>

    @if (selectedItem(); as selected) {
      <p>Selected: {{ selected.name }}</p>
    }

    <button (click)="addAndSelect()">Add new item</button>
  `,
})
export class ItemPickerComponent {
  readonly items = signal<Array<ItemModel>>([
    { id: 1, name: 'Draft' },
    { id: 2, name: 'Review' },
  ]);

  protected readonly selectedItem = linkedSignal(() => this.items()[0], {
    set: (item: ItemModel) => {
      const items = this.items();
      if (items.indexOf(item) < 0) {
        this.items.set([item, ...items]);
      }
    },
  });

  addAndSelect() {
    const newItem: ItemModel = { id: Date.now(), name: 'New Task' };
    this.selectedItem.set(newItem);
  }
}
Enter fullscreen mode Exit fullscreen mode

Note there is no standalone: true anywhere in that decorator — that has been the default for a while now, and Angular 22.1 keeps pushing the ecosystem further away from writing it explicitly.

4. HTTP Interceptors No Longer Leak Into Your Effects

This one is subtle, and if you have ever debugged an effect that seemed to re-run "for no reason," it might explain a few lost hours.

effect() automatically tracks every signal it reads. Before 22.1, that tracking extended into whatever your HTTP interceptors read too. So an effect that triggered an HTTP call was silently depending on any signal your interceptors happened to touch — an auth token signal, a locale signal, a feature flag signal, anything.

As of Angular 22.1, interceptors run untracked by default. Your effects now re-run only for reasons you actually intended.

If you still need fine-grained control inside an effect, untracked() remains your explicit escape hatch:

effect(() => {
  const value = this.mySignal();
  untracked(() => {
    // read other signals here without creating a dependency
  });
});
Enter fullscreen mode Exit fullscreen mode

The difference in 22.1 is that you no longer need to reach for untracked() just to stop your interceptors from becoming invisible dependencies. That happens automatically now.

Have you hit a "why does this effect keep firing" bug before? I would bet money at least one of you has traced it back to something exactly like this. Tell me about it in the comments — misery loves company, and so does debugging content.

5. Smaller but Genuinely Useful: DevTools Improvements

Two changes landed in Angular DevTools alongside 22.1:

  • You can now search the signal graph by name and by type, using syntax like type:computed. If you have ever opened DevTools on a component with forty signals trying to find the one causing a re-render, this alone is worth the update.
  • The transfer state panel is now visible by default, instead of being tucked away, making SSR debugging noticeably less annoying.

Neither of these needs a code sample. They just need you to open DevTools after upgrading and notice they are there.

Testing It All: Unit Tests for Signal-Based Standalone Components

Since we are using signals and standalone components throughout, let's write tests that match. Here is a unit test for the ItemPickerComponent above, using TestBed with the current standalone-first testing API:

import { TestBed } from '@angular/core/testing';
import { ItemPickerComponent } from './item-picker.component';

describe('ItemPickerComponent', () => {
  let component: ItemPickerComponent;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ItemPickerComponent],
    }).compileComponents();

    const fixture = TestBed.createComponent(ItemPickerComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('selects the first item by default', () => {
    expect(component'selectedItem'.name).toBe('Draft');
  });

  it('updates selection when an existing item is clicked', () => {
    const items = component.items();
    component['selectedItem'].set(items[1]);

    expect(component'selectedItem'.name).toBe('Review');
  });

  it('adds a new item to the list when selecting one that does not exist yet', () => {
    const beforeCount = component.items().length;
    component.addAndSelect();

    expect(component.items().length).toBe(beforeCount + 1);
    expect(component'selectedItem'.name).toBe('New Task');
  });
});
Enter fullscreen mode Exit fullscreen mode

Because we import the standalone component directly into imports rather than declaring it, there is no NgModule boilerplate to maintain. If your team has started moving its Angular test suite to Vitest, the same tests run with only the runner changed — describe, it, and expect stay identical, which is one more reason the standalone-plus-signals combination pairs so well with modern testing setups.

A Bonus Tip Before You Go

If you are on Angular 22 already, the upgrade to 22.1 should be uneventful:

ng update @angular/core @angular/cli
Enter fullscreen mode Exit fullscreen mode

Run the @Service() migration schematic in dry-run mode right after upgrading, even if you are not planning to use it immediately. It costs nothing, and it gives you a concrete count of how much of your codebase is migration-ready — useful ammunition the next time someone asks "how much technical debt do we actually have."

Recap

Angular 22.1 is a minor release doing a lot of quiet, practical work:

  • A new yearly major release schedule with two-year support windows, replacing the six-month cadence Angular has used since version 4
  • An automated schematic to migrate @Injectable() services to @Service()
  • A custom set option for linkedSignal, giving you control over writes without losing derived, reactive defaults
  • HTTP interceptors now run untracked, fixing a subtle source of unnecessary effect re-runs
  • DevTools gained signal-graph search and a default-visible transfer state panel

None of these are headline-grabbing on their own. Together, they are the kind of release that makes the next twelve months of Angular development a little less error-prone.

There is genuinely enough depth here — especially in the @Service() migration internals and linkedSignal patterns — to justify splitting into a dedicated mini-series, one feature per article, if that is more digestible than one long read. I am leaning toward doing that. More on this below.

What did you think?

Which of these five changes actually affects your day-to-day work the most — the release schedule, the @Service() migration, linkedSignal, the interceptor fix, or DevTools? Drop a comment and tell me, or vote for the topic you want covered in its own deep-dive next.

Found this helpful? If it saved you from digging through five different changelog pages yourself, that is exactly what a clap tells me to keep doing — hit it so more Angular developers find this.

Want more breakdowns like this, written the week a release actually lands instead of months later? Follow for more Angular and React content, or subscribe to get new articles delivered directly when they publish.


Follow Me for More Angular & Frontend Goodness:

I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.

  • 💼 LinkedIn — Let’s connect professionally
  • 🎥 Threads — Short-form frontend insights
  • 🐦 X (Twitter) — Developer banter + code snippets
  • 👥 BlueSky — Stay up to date on frontend trends
  • 🌟 GitHub Projects — Explore code in action
  • 🌐 Website — Everything in one place
  • 📚 Medium Blog — Long-form content and deep-dives
  • 💬 Dev Blog — Free Long-form content and deep-dives
  • ✉️ Substack — Weekly frontend stories & curated resources
  • 🧩 Portfolio — Projects, talks, and recognitions
  • ✍️ Hashnode — Developer blog posts & tech discussions
  • ✍️ Reddit — Developer blog posts & tech discussions

Top comments (0)