|
| 1 | +# Dependent state with `linkedSignal` |
| 2 | + |
| 3 | +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: |
| 4 | + |
| 5 | +```typescript |
| 6 | +@Component({/* ... */}) |
| 7 | +export class ShippingMethodPicker { |
| 8 | + shippingOptions: Signal<ShippingMethod[]> = getShippingOptions(); |
| 9 | + |
| 10 | + // Select the first shipping option by default. |
| 11 | + selectedOption = signal(this.shippingOptions()[0]); |
| 12 | + |
| 13 | + changeShipping(newOptionIndex: number) { |
| 14 | + this.selectedOption.set(this.shippingOptions()[newOptionIndex]); |
| 15 | + } |
| 16 | +} |
| 17 | +``` |
| 18 | + |
| 19 | +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. |
| 20 | + |
| 21 | +**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`: |
| 22 | + |
| 23 | +```typescript |
| 24 | +@Component({/* ... */}) |
| 25 | +export class ShippingMethodPicker { |
| 26 | + shippingOptions: Signal<ShippingMethod[]> = getShippingOptions(); |
| 27 | + |
| 28 | + // Initialize selectedOption to the first shipping option. |
| 29 | + selectedOption = linkedSignal(() => this.shippingOptions()[0]); |
| 30 | + |
| 31 | + changeShipping(index: number) { |
| 32 | + this.selectedOption.set(this.shippingOptions()[index]); |
| 33 | + } |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +`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. |
| 38 | + |
| 39 | +The following example shows how the value of a `linkedSignal` can change based on its linked state: |
| 40 | + |
| 41 | +```typescript |
| 42 | +const shippingOptions = signal(['Ground', 'Air', 'Sea']); |
| 43 | +const selectedOption = linkedSignal(() => shippingOptions()[0]); |
| 44 | +console.log(selectedOption()); // 'Ground' |
| 45 | + |
| 46 | +selectedOption.set(shippingOptions()[2]); |
| 47 | +console.log(selectedOption()); // 'Sea' |
| 48 | + |
| 49 | +shippingOptions.set(['Email', 'Will Call', 'Postal service']); |
| 50 | +console.log(selectedOption()); // 'Email' |
| 51 | +``` |
| 52 | + |
| 53 | +## Accounting for previous state |
| 54 | + |
| 55 | +In some cases, the computation for a `linkedSignal` needs to account for the previous value of the `linkedSignal`. |
| 56 | + |
| 57 | +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_: |
| 58 | + |
| 59 | +```typescript |
| 60 | +interface ShippingMethod { |
| 61 | + id: number; |
| 62 | + name: string; |
| 63 | +} |
| 64 | + |
| 65 | +@Component({/* ... */}) |
| 66 | +export class ShippingMethodPicker { |
| 67 | + constructor() { |
| 68 | + this.changeShipping(2); |
| 69 | + this.changeShippingOptions(); |
| 70 | + console.log(this.selectedOption()); // {"id":2,"name":"Postal Service"} |
| 71 | + } |
| 72 | + |
| 73 | + shippingOptions = signal<ShippingMethod[]>([ |
| 74 | + { id: 0, name: 'Ground' }, |
| 75 | + { id: 1, name: 'Air' }, |
| 76 | + { id: 2, name: 'Sea' }, |
| 77 | + ]); |
| 78 | + |
| 79 | + selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({ |
| 80 | + // `selectedOption` is set to the `computation` result whenever this `source` changes. |
| 81 | + source: this.shippingOptions, |
| 82 | + computation: (newOptions, previous) => { |
| 83 | + // If the newOptions contain the previously selected option, preserve that selection. |
| 84 | + // Otherwise, default to the first option. |
| 85 | + return ( |
| 86 | + newOptions.find((opt) => opt.id === previous?.value.id) ?? newOptions[0] |
| 87 | + ); |
| 88 | + }, |
| 89 | + }); |
| 90 | + |
| 91 | + changeShipping(index: number) { |
| 92 | + this.selectedOption.set(this.shippingOptions()[index]); |
| 93 | + } |
| 94 | + |
| 95 | + changeShippingOptions() { |
| 96 | + this.shippingOptions.set([ |
| 97 | + { id: 0, name: 'Email' }, |
| 98 | + { id: 1, name: 'Sea' }, |
| 99 | + { id: 2, name: 'Postal Service' }, |
| 100 | + ]); |
| 101 | + } |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +When you create a `linkedSignal`, you can pass an object with separate `source` and `computation` properties instead of providing just a computation. |
| 106 | + |
| 107 | +The `source` can be any signal, such as a `computed` or component `input`. When the value of `source` changes, `linkedSignal` updates its value to the result of the provided `computation`. |
| 108 | + |
| 109 | +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 result of the `computation`. You can use these previous values to decide the new result of the computation. |
| 110 | + |
| 111 | +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`. |
| 112 | + |
| 113 | +## Custom equality comparison |
| 114 | + |
| 115 | +`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: |
| 116 | + |
| 117 | +```typescript |
| 118 | +const activeUser = signal({id: 123, name: 'Morgan', isAdmin: true}); |
| 119 | + |
| 120 | +const activeUserEditCopy = linkedSignal(() => activeUser(), { |
| 121 | + // Consider the user as the same if it's the same `id`. |
| 122 | + equal: (a, b) => a.id === b.id, |
| 123 | +}); |
| 124 | + |
| 125 | +// Or, if separating `source` and `computation` |
| 126 | +const activeUserEditCopy = linkedSignal({ |
| 127 | + source: activeUser, |
| 128 | + computation: user => user, |
| 129 | + equal: (a, b) => a.id === b.id, |
| 130 | +}); |
| 131 | +``` |
0 commit comments