In-depth Guides
Signals

Dependent state with linkedSignal

You can use the signal function to hold some state in your Angular code. Sometimes, this state depends on some other state. For example, imagine a component that lets the user select a shipping method for an order:

@Component({
  /* ... */
})
export class ShippingMethodPicker {
  shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();

  // Select the first shipping option by default.
  selectedOption = signal(this.shippingOptions()[0]);

  changeShipping(newOptionIndex: number) {
    this.selectedOption.set(this.shippingOptions()[newOptionIndex]);
  }
}

In this example, the selectedOption defaults to the first option, but changes if the user selects another option. But shippingOptions is a signal— its value may change! If shippingOptions changes, selectedOption may contain a value that is no longer a valid option.

The linkedSignal function lets you create a signal to hold some state that is intrinsically linked to some other state. Revisiting the example above, linkedSignal can replace signal:

@Component({
  /* ... */
})
export class ShippingMethodPicker {
  shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();

  // Initialize selectedOption to the first shipping option.
  selectedOption = linkedSignal(() => this.shippingOptions()[0]);

  changeShipping(index: number) {
    this.selectedOption.set(this.shippingOptions()[index]);
  }
}

linkedSignal works similarly to signal with one key difference— instead of passing a default value, you pass a computation function, just like computed. When the value of the computation changes, the value of the linkedSignal changes to the computation result. This helps ensure that the linkedSignal always has a valid value.

The following example shows how the value of a linkedSignal can change based on its linked state:

const shippingOptions = signal(['Ground', 'Air', 'Sea']);
const selectedOption = linkedSignal(() => shippingOptions()[0]);
console.log(selectedOption()); // 'Ground'

selectedOption.set(shippingOptions()[2]);
console.log(selectedOption()); // 'Sea'

shippingOptions.set(['Email', 'Will Call', 'Postal service']);
console.log(selectedOption()); // 'Email'

Accounting for previous state

In some cases, the computation for a linkedSignal needs to account for the previous value of the linkedSignal.

In the example above, selectedOption always updates back to the first option when shippingOptions changes. You may, however, want to preserve the user's selection if their selected option is still somewhere in the list. To accomplish this, you can create a linkedSignal with a separate source and computation:

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

@Component({
  /* ... */
})
export class ShippingMethodPicker {
  constructor() {
    this.changeShipping(2);
    this.changeShippingOptions();
    console.log(this.selectedOption()); // {"id":2,"name":"Postal Service"}
  }

  shippingOptions = signal<ShippingMethod[]>([
    {id: 0, name: 'Ground'},
    {id: 1, name: 'Air'},
    {id: 2, name: 'Sea'},
  ]);

  selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
    // `selectedOption` is set to the `computation` result whenever this `source` changes.
    source: this.shippingOptions,
    computation: (newOptions, previous) => {
      // If the newOptions contain the previously selected option, preserve that selection.
      // Otherwise, default to the first option.
      return newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0];
    },
  });

  changeShipping(index: number) {
    this.selectedOption.set(this.shippingOptions()[index]);
  }

  changeShippingOptions() {
    this.shippingOptions.set([
      {id: 0, name: 'Email'},
      {id: 1, name: 'Sea'},
      {id: 2, name: 'Postal Service'},
    ]);
  }
}

When you create a linkedSignal, you can pass an object with separate source and computation properties instead of providing just a computation.

The source can be any signal, such as a computed or component input. The linkedSignal updates its value when the source changes or when any signal referenced in the computation changes, updating its value with the result of the provided computation.

The computation is a function that receives the new value of source and a previous object. The previous object has two properties— previous.source is the previous value of source, and previous.value is the previous value of the linkedSignal. You can use these previous values to decide the new result of the computation.

HELPFUL: When using the previous parameter, it is necessary to provide the generic type arguments of linkedSignal explicitly. The first generic type corresponds with the type of source and the second generic type determines the output type of computation.

Custom equality comparison

linkedSignal, as any other signal, can be configured with a custom equality function. This function is used by downstream dependencies to determine if that value of the linkedSignal (result of a computation) changed:

const activeUser = signal({id: 123, name: 'Morgan', isAdmin: true});

const activeUserEditCopy = linkedSignal(() => activeUser(), {
  // Consider the user as the same if it's the same `id`.
  equal: (a, b) => a.id === b.id,
});

// Or, if separating `source` and `computation`
const activeUserEditCopy = linkedSignal({
  source: activeUser,
  computation: (user) => user,
  equal: (a, b) => a.id === b.id,
});

Customizing the set operation

Sometimes you may want the set and update operations of a linkedSignal to write back to the source of truth instead of updating the linkedSignal's value directly. You can customize this behavior by passing a set function in the options.

The custom set function receives two arguments:

  1. The new value being set.
  2. A rawSet function, which you can invoke to update the linkedSignal's internal state directly (matching the default behavior).

NOTE: Using rawSet allows you to update the linkedSignal's value directly. This can be useful to prevent the computation from running, for example if it is an expensive derivation and you already know the result.

Writing back to a source signal

Consider a component that displays and allows editing temperature in Fahrenheit, but uses a Celsius signal as its source of truth:

const tempC = signal(0);
const tempF = linkedSignal(() => (tempC() * 9) / 5 + 32, {
  set: (valF) => tempC.set(((valF - 32) * 5) / 9),
});

console.log(tempF()); // 32

// Setting Fahrenheit updates Celsius, which reactively updates Fahrenheit
tempF.set(212);
console.log(tempC()); // 100
console.log(tempF()); // 212

Updating a property inside a parent object

Another common scenario is updating a specific property inside a parent object. The parent is held in a signal, and you link to a nested property:

interface Order {
  id: number;
  shippingMethod: string;
}

const order = signal<Order>({
  id: 42,
  shippingMethod: 'Ground',
});

const shippingMethod = linkedSignal(() => order().shippingMethod, {
  set: (newMethod) => {
    // Perform an immutable update to write the change back to the order
    order.update((currentOrder) => ({
      ...currentOrder,
      shippingMethod: newMethod,
    }));
  },
});

console.log(shippingMethod()); // 'Ground'

// Updating the shippingMethod updates the parent order object
shippingMethod.set('Air');
console.log(order()); // { id: 42, shippingMethod: 'Air' }
console.log(shippingMethod()); // 'Air'