Telerik blogs

Look at an experimental addition to the signal API—linkedSignal(). It helps developers manage dependent states.

State refers to data that powers applications. A characteristic of stateful data is reactivity, which means that whenever application state changes, it should be immediately reflected in the user interface. In Angular applications, state is managed using three reactive primitives: regular instance variables, observables or signals. Of all these primitives, signals are the latest addition.

Signals already existed in other frameworks but were officially introduced in Angular in Version 17. Signals make it incredibly easy for developers to handle both simple and complex state management tasks using their easy-to-use synchronous API.

In this guide, we will look at an experimental addition to the signal API—linkedSignal() that helps developers manage dependent state, which was a pain to do with the existing API. We will show both simple and more advanced uses of this function.

Dealing with dependent states has many real-world use cases that all share the same central idea. In our case, our mini app will be a simple template-driven form with dependent input fields. Let us begin.

Prerequisites

To proceed with this guide, it is assumed you are familiar with basic TypeScript and a frontend framework.

Project Setup

Assuming you have the Angular CLI installed, open your terminal and run the following command in your desired folder to create an Angular project.

ng new linked-signals-intro

In our case, our project is housed in a folder called linked-signals-intro. Follow the prompt to configure your Angular application and install the necessary dependencies.

Project setup

Signal API Preamble

Now, let’s briefly discuss the already existing functions provided by the signal API. This will enable us to understand how the Signal API works and how linked signals build on top of it.

Before the linkedSignal() function was added, the former API consisted of three main functions: signal(), computed() and effect().

Signal

The signal() function is the basis of all the other functions provided by the API. As you might have guessed already, this function is used to create a signal.

Here is a simple use of this function:

user = signal({ fullName: "John Doe", id: 1, phoneNumber: "" });

The call to the signal() function expects a primitive or an object. In our case, the call returns a wrapper around the user object. Signals created using the signal() function are both readable and writable.

For example, to read the id of the user object, we can do this:

user().id. // prints 1

And to write to the signal, we can use the set() or update() methods.

user.set({ fullName: "John Doe", id: 1, phoneNumber: "1234" });

// or

user.update((prevUser) => ({ ...prevUser, phoneNumber: "56789" }));

Effect

The effect() function accepts a callback that runs asynchronously each time the signals used in the callback change. For example, we can write code to call some third-party API or print some logs for debugging.

authStatus = signal("authenticated");

effect(() => {
  console.log("the user is now " + authStatus());
});

In the example above, we write an effect that prints a message to the screen whenever the authentication status changes.

Computed

This function allows us to derive a signal from another signal. It receives a callback function that computes a value based on one or more signals. The callback is recomputed each time any of the signals used in the callback’s body changes.

firstname = signal("john");

lastname = signal("bobo");

fullname = computed(() => firstname() + " " + lastname());

In the example above, we create a computed signal called fullname that just concatenates the values of the firstname and lastname signals.

It is important to note that an attempt to modify the value of a signal in the callback passed to effect() or computed() is an antipattern and can easily introduce bugs in applications.

The main issue with computed signals is that while they help us create relationships between state by allowing us to derive new signals from one or more signals, the resulting signals they produce are not writable. In our case, fullname is not writable; it is read-only.

linkedSignal

Linked signals are quite similar to signals generated using the computed() function in that they allow us to create relationships between signals by allowing us to compute new signals from one or more sources.

The differences between signals created with the computed() function and those created using the linkedSignal() function are as follows:

Note that the linkedSignal() function has two overloaded variants as we will see next.

  1. The linkedSignal() function allows the creation of writable computed signals, which makes its behavior identical to using just the plain signal() function. Signals created from the computed() function are read-only.
@Component({
selector: 'dummy-comp',
template: `<button (click)="this.update()">update</button>`,
})
export class DummyComponent {
  value = signal(3);
  valueSquare = linkedSignal(() => this.value() \* this.value());

dummyUpdate() {
  this.value.set(9);
  // valueSquare is now 81
  this.value.set(5);
  // valueSquare is now 25

    // we can also update valueSquare directly
    this.valueSquare.set(121);

  }
}

In the example above, valueSquare derives its value from the square of the value signal. Each time we update the value signal, valueSquare is recomputed. Also, since valueSquare is just a regular signal, we can directly update it using its set() or update() methods.

  1. An added benefit of linked signals is that they provide more control when creating the computed signal. They allow developers to use the previous values of the computed signal and the sources used to generate them to help determine the new value of the signal.
@Component({
  selector: "dummy-comp",
  template: `<button (click)="this.update()">update</button>`,
})
export class DummyComponent1 {
  value = signal(10);

  evenValue = linkedSignal({
    source: () => this.value(),
    computation(currentSource, old) {
      if (currentSource % 2 === 0) {
        return currentSource;
      }
      return old?.value % 2 === 0 ? old?.value : 0;
    },
  });

  update() {
    this.value.set(13);
    // evenValue remains 10
    this.value.set(14);
    // evenValue is now 14

    // we can also update evenValue directly
    evenValue.set(64);
  }
}

In the code snippet above, evenValue depends on value; however, it uses the more verbose definition of the linkedSignal() function that accepts two mandatory parameters. The first parameter, source, is a callback that resolves to one or more signals that evenValue will be derived from. You can return an object in case you are interested in using multiple signals. In our case, it only returned the value signal.

Th second parameter is computed, which is a callback intended to return an actual value for evenValue. It accepts two parameters: the current value of the source(s) and a second parameter which is an object that holds the previous value of evenValue and the source(s) signals used to compute it.

evenValue can be modified directly using its set() or update() methods like a regular signal, or if the value signal it depends on holds an even number. However, if the value signal holds an odd number, it will try to check if the previous value of value (i.e., old?.value) is even, then it resolves to that value; else it resolves to 0.

What We Will Be Building

We have already established how linked signals work and the different ways to invoke the linkedSignal function within components.

In this section, we will show a practical use case in a simple application. We will build a simple admission form that allows the user to choose a university and then select their desired major.

The goal of our application again is to show how linked signals help us to work with dependent state and show the different forms of using the overloaded linkedSignal() function.

All the code we will write will go into our app.component.ts file.

For now, this file looks like this:

import { Component, computed, signal, linkedSignal } from "@angular/core";
import { FormsModule } from "@angular/forms";

@Component({
  selector: "app-root",
  template: ``,
  imports: [FormsModule],
})
export class AppComponent {}

For imports, we included most of the functions that are included in the signal API. Next, we imported the FormModule, which will help us when building our forms. We are only interested in using the ngModel directive in our form.

Our app will contain four signals. Let’s now incrementally update the app.component.ts file to look like this:

type Tmajor = {
  id: string;
  name: string;
};
type Tinstitution = {
  id: string;
  name: string;
  majors: Tmajor[];
};
const INSTITUTIONS: Tinstitution[] = [
  {
    id: "1",
    name: "univerity 1",
    majors: [
      {
        id: "1",
        name: "computer Science",
      },
      {
        id: "2",
        name: "Engineering",
      },
      {
        id: "3",
        name: "Psycology",
      },
    ],
  },
  {
    id: "2",
    name: "univerity 2",
    majors: [
      {
        id: "4",
        name: "Arts",
      },
      {
        id: "1",
        name: "computer Science",
      },
    ],
  },
];

@Component({
  selector: "app-root",
  template: `
    <section class="prose lg:prose-xl mx-auto pt-10">
      <div
        class="rounded-sm max-w-[400px] mx-auto  border border-neutral-600 p-7"
      >
        <div class="grid gap-3">
          <div class="text-center ">
            <h3>Admission form</h3>
          </div>
          <label class="form-control w-full grid">
            <div class="label">
              <span class="label-text">Pick university</span>
            </div>
            <select
              class="select select-bordered"
              [(ngModel)]="selectedInstitution"
            >
              @for (major of institutions(); track $index) {
              <option [value]="major.id">{{ major.name }}</option>
              }
            </select>
          </label>
        </div>
      </div>
    </section>
  `,
  imports: [FormsModule],
})
export class AppComponent {
  institutions = signal(INSTITUTIONS);
  selectedInstitution = linkedSignal(() => this.institutions()[0].id);
}

We created a hardcoded list of institutions and their majors in a variable called INSTITUTIONS. Notice that a major can be offered. Next, within the app component, we wrap this object in a signal called institution, and we render it in the UI in a select dropdown.

We set up a linked signal called selectedInstitution because, by default, we want the first institution to be selected, and we also want to allow the user to update their choice by picking an institution from the dropdown.

@Component({
  selector: "app-root",
  template: `
    <section class="prose lg:prose-xl mx-auto pt-10">
      <div
        class="rounded-sm max-w-[400px] mx-auto  border border-neutral-600 p-7"
      >
        <div class="grid gap-3">
          <div class="text-center ">
            <h3>Admission form</h3>
          </div>
          <label class="form-control w-full grid"> //...form field </label>

          //Add this
          <label class="form-control w-full grid">
            <div class="label">
              <span class="label-text">Pick your major</span>
            </div>
            <select class="select select-bordered" [(ngModel)]="major">
              @for (major of majors(); track $index) {
              <option [value]="major.id">{{ major.name }}</option>
              }
            </select>
          </label>
        </div>
      </div>
    </section>
  `,
  imports: [FormsModule],
})
export class AppComponent {
  institutions = signal(INSTITUTIONS);
  selectedInstitution = linkedSignal(() => this.institutions()[0].id);

  // Add this

  majors = computed(() => {
    const chosenInstitution = this.institutions().find(
      (ins) => ins.id === this.selectedInstitution()
    );
    if (chosenInstitution) {
      return chosenInstitution.majors;
    }
    return [];
  });

  major = linkedSignal<Tmajor[], string>({
    source: this.majors,
    computation(currentSource, oldData) {
      const existingMajor = currentSource.find(
        (maj) => maj.id === oldData?.value
      );
      if (existingMajor) {
        return existingMajor.id;
      }
      return currentSource[0].id;
    },
  });
}

In the code above, we maintain a signal called majors that depends on the selectedInstitution signal. Since we don’t have plans to modify its value, we resolved its value using the computed() function as it generates readonly signals. The list of majors again is rendered in a select dropdown.

The major signal again depends on the list of majors and needs to be writable since it’s bound to the select dropdown, so it’s wrapped as a linked signal. We chose the more verbose definition to be able to resolve the correct value.

In the computation function, we set the default to the first major being selected from the current institution.

Your app should look like this:

By default, the first major is selected

However, if the institution is modified and the majors list changes, and the previously selected major appears in the new list of majors, it is not modified.

Computer science exists in the new list of majors

Conclusion

Signals are not meant to be a replacement for observables or other reactive primitives supported by Angular. They just provide an alternative that gets the job done and is easy to use and understand by developers. This guide includes information about an addition to this API that can be used to solve how we manage dependent state in our Angular projects.

Learn more about Signals in Angular.


About the Author

Christian Nwamba

Chris Nwamba is a Senior Developer Advocate at AWS focusing on AWS Amplify. He is also a teacher with years of experience building products and communities.

Related Posts

Comments

Comments are disabled in preview mode.