Mert Özen Further With Every Line
Frontend Angular — #28

Angular 19: Standalone by Default, linkedSignal and Data Loading with resource()

Mert Özen Oct 31, 2026 8 min 3 views
Angular 19: Standalone by Default, linkedSignal and Data Loading with resource()

In Angular 19 you no longer write standalone: true. We compare linkedSignal for dependent state and the experimental resource() and rxResource for data loading with the Angular 12 way.

Version note: This post covers what Angular 19 introduced; the code targets Angular 19.

What Did Angular 19 Bring to the Table?

With Angular 18 we saw that zoneless change detection could be switched on experimentally, that control flow became stable, and that @let lets us declare variables right inside a template. Angular 19, released on November 19, 2024, builds two important things on top of that.

The first looks small but touches every file: a line we have been writing into every component for years is no longer needed. The second is a pair of new tools for the signal world: linkedSignal for state that depends on other state, and resource() for loading data from a server. At the end we will take a quick look at the SSR side and at upgrading with ng update.

In Angular 12, Every Component Belonged to a Module

Throughout Block A, using a component meant registering it in an NgModule first:

// product-card.component.ts
@Component({
  selector: 'app-product-card',
  templateUrl: './product-card.component.html'
})
export class ProductCardComponent { }

// app.module.ts
@NgModule({
  declarations: [AppComponent, ProductCardComponent],
  imports: [BrowserModule],
  bootstrap: [AppComponent]
})
export class AppModule { }

The component was written in one place and introduced in another. Forget to add it to declarations and you got the "is not a known element" error. Standalone components cut that tie, but from Angular 15 to 18 we still had to write standalone: true on every standalone component.

With Angular 19, No More standalone: true

import { Component } from '@angular/core';
import { CurrencyPipe } from '@angular/common';

@Component({
  selector: 'app-product-card',
  imports: [CurrencyPipe],
  template: `
    <h3>{{ name }}</h3>
    <p>{{ price | currency:'TRY' }}</p>
  `
})
export class ProductCardComponent {
  name = 'Mekanik Klavye';
  price = 1450;
}

There is no standalone field in the decorator, and yet this component is standalone. In Angular 19 the default became true for components, directives and pipes. The component declares its own dependencies in imports; here all it needs is CurrencyPipe.

So what happens in a project that still uses NgModules? A component listed in a module's declarations now has to say standalone: false explicitly. Think of a phone setting whose factory default changed: the thing you used to switch on every time now comes switched on, and if you want it off, you have to say so.

The good news is you don't do this by hand. The migration that runs during ng update removes existing standalone: true lines and adds standalone: false to module-bound components. If your team wants to close the door on NgModules entirely, put "strictStandalone": true under angularCompilerOptions in tsconfig.json; any component that says standalone: false then fails to compile.

Dependent State: linkedSignal

Picture this: a shipping picker component receives a list of options from outside. When the list changes, the selection should jump back to the first option, but the user must still be able to click any option. In Angular 12 we solved it with ngOnChanges:

export class ShippingPickerComponent implements OnChanges {
  @Input() options: string[] = [];
  selected = '';

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.options) {
      this.selected = this.options[0];
    }
  }

  choose(option: string): void {
    this.selected = option;
  }
}

It works, but the fact that "the selection depends on the list" is buried inside a lifecycle hook. With Angular 19 the same thing is one line:

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

@Component({
  selector: 'app-shipping-picker',
  template: `
    @for (option of options(); track option) {
      <button (click)="selected.set(option)">{{ option }}</button>
    }
    <p>Seçili: {{ selected() }}</p>
  `
})
export class ShippingPickerComponent {
  options = input<string[]>([]);
  selected = linkedSignal(() => this.options()[0]);
}

options is one of the signal inputs we saw earlier. selected is something new that sits between the two worlds. Like computed, it tracks its source: the moment options changes, it recalculates and goes back to the first option. Unlike computed, it is writable: when a button is clicked, selected.set(option) changes the value by hand.

Think of a navigation app. You can leave the suggested route and turn into another street, and the app lets you. But the moment you change the destination, the route is recalculated from scratch.

Sometimes you want to keep the user's choice when the list changes, as long as that choice still exists in the new list. For that you use the longer form:

selected = linkedSignal<string[], string>({
  source: this.options,
  computation: (opts, previous) =>
    previous && opts.includes(previous.value) ? previous.value : opts[0]
});

source is the signal being tracked, computation is the function that produces the new value. Its second parameter, previous, carries the previous value, so you can ask "is the old choice still valid?" In Angular 19, linkedSignal is in developer preview.

A New Way to Fetch Data: resource()

In Angular 12, fetching a list from the server almost always followed the same pattern:

export class UserListComponent implements OnInit {
  users: User[] = [];
  loading = false;
  error = '';

  constructor(private http: HttpClient) { }

  ngOnInit(): void {
    this.loading = true;
    this.http.get<User[]>('/api/users').subscribe({
      next: users => { this.users = users; this.loading = false; },
      error: () => { this.error = 'Kullanıcılar yüklenemedi'; this.loading = false; }
    });
  }
}

Three separate properties, loading = false in two places, and if you want to refetch whenever the sort order changes, a Subject and a switchMap join the party. Angular 19 gathers this pattern into a single construct:

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

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

@Component({
  selector: 'app-user-list',
  template: `
    <button (click)="sort.set('asc')">A-Z</button>
    <button (click)="sort.set('desc')">Z-A</button>
    @if (usersResource.isLoading()) {
      <p>Yükleniyor...</p>
    } @else if (usersResource.error()) {
      <p>Kullanıcılar yüklenemedi</p>
    } @else {
      @for (user of usersResource.value(); track user.id) {
        <p>{{ user.name }}</p>
      }
    }
  `
})
export class UserListComponent {
  sort = signal<'asc' | 'desc'>('asc');

  usersResource = resource({
    request: () => ({ sort: this.sort() }),
    loader: async ({ request, abortSignal }) => {
      const response = await fetch(`/api/users?sort=${request.sort}`, { signal: abortSignal });
      return (await response.json()) as User[];
    }
  });
}

resource() takes two functions. request says which signals the request depends on; here, sort. loader does the actual work and returns a Promise. When sort changes, Angular runs the loader again on its own. If an older request is still in flight, abortSignal lets it be cancelled.

The returned object carries a set of signals: value() gives the data, isLoading() the loading state, error() the error. Since the template reads these, deciding what to show is just a few @if blocks. The loading and error properties we used to maintain by hand are gone.

A warning: resource() is experimental in Angular 19. The shape of the API has not settled yet; in fact, some parameter names change in the very next version. It is great for experimenting, but make a conscious decision before spreading it across a large project.

Keeping HttpClient: rxResource

resource() works with Promises and does not depend on RxJS. But what if you don't want to give up HttpClient, with its interceptors and error handling? That is what rxResource, from the @angular/core/rxjs-interop package, is for:

import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { rxResource } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-user-list',
  template: `...`
})
export class UserListComponent {
  private http = inject(HttpClient);
  sort = signal<'asc' | 'desc'>('asc');

  usersResource = rxResource({
    request: () => ({ sort: this.sort() }),
    loader: ({ request }) =>
      this.http.get<User[]>('/api/users', { params: { sort: request.sort } })
  });
}

The only difference is that loader returns an observable instead of a Promise. The template stays exactly the same. Remember that HttpClient needs provideHttpClient() in the app's app.config.ts. One more detail: rxResource only takes the first value from the observable, so it is designed for one-off requests, not for a continuously flowing stream. It is experimental in Angular 19 too.

Signal APIs and Other Status Changes

Angular 19 made most of the signal-based component APIs we saw earlier stable: input(), output(), model(), viewChild(), viewChildren(), contentChild(), contentChildren() and takeUntilDestroyed(). The template-level @let also moved from developer preview to stable.

effect(), toSignal() and toObservable(), on the other hand, are still in developer preview in this version. Zoneless is still experimental as well; if you want to start a new project zoneless, use the ng new --experimental-zoneless flag.

There is one more small change that is easy to miss. The APP_INITIALIZER token we used to load something at startup has been replaced by a simpler function:

providers: [
  provideAppInitializer(() => inject(ConfigService).load())
]

A Short Note on SSR: Incremental Hydration

If you render your app on the server, incremental hydration in Angular 19 is for you. Normally, once the browser receives the server's HTML, it "brings the whole page to life" in one go. Incremental hydration lets it do that piece by piece, as needed:

// app.config.ts
providers: [provideClientHydration(withIncrementalHydration())]

<!-- template -->
@defer (on timer(15s); hydrate on interaction) {
  <app-comments />
}

hydrate on interaction says the server-rendered comments section stays as inert HTML until the user touches it. Like a restaurant where the menu is on the table from the start, but the waiter only comes over when you raise your hand. This feature is in developer preview in Angular 19. The same version made event replay stable and added the ability to pick a render mode per route.

Moving to Angular 19: ng update

You upgrade a project on Angular 18 with this command:

ng update @angular/core@19 @angular/cli@19

The CLI won't let you skip more than one major version at once; if you are on 17, go to 18 first, then to 19. Angular 19 needs Node.js from one of the ^18.19.1, ^20.11.1 or ^22.0.0 lines. TypeScript must be at least 5.5; with 19.2, versions up to 5.8 are supported.

The standalone migration runs automatically during the update and makes the standalone: true / standalone: false changes we just described. Once the command finishes, look at git diff; seeing the files the migration touched is the quickest way to get the new default into your head.

A Small Experiment

Create a project with npx @angular/cli@19 new deneme. Write the shipping picker component, keep two different option lists in a signal inside AppComponent, and toggle between them with a button. Try the short linkedSignal form first: click an option, switch the list, and watch the selection reset. Then move to the long form and catch the case where the selection is kept.

Next, set up a resource(): keep a signal called id and, inside the loader, fetch a single user from https://jsonplaceholder.typicode.com/users/${request.id}. Hit a "Next" button several times in quick succession and watch the abandoned requests get cancelled in the browser's Network tab.

Keep that request parameter in the back of your mind. Next up is Angular 20, where most signal APIs become stable, that parameter gets a new name, httpResource takes the stage, and app.ts files replace app.component.ts.