-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(signals): add withLinkedState()
#4818
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(signals): add withLinkedState()
#4818
Conversation
✅ Deploy Preview for ngrx-io ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for ngrx-site-v19 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…State` BREAKING CHANGES: `withState` and `signalState` now support user-defined signals like `linkedSignal`, `resource.value`, or any other `WritableSignal`. For example: ```ts const user = signal({ id: 1, name: 'John Doe' }); const userClone = linkedSignal(user); const userValue = resource({ loader: () => Promise.resolve('user'), defaultValue: '' }); const Store = signalStore( withState({ user, userClone, userValue: userValue.value }) ); ``` The state slices don't change: ```ts store.user; // DeepSignal<{ id: number, name: string }> store.userClone; // DeepSignal<{ id: number, name: string }> store.userValue; // Signal<string> ``` The behavior of `linkedSignal` and `resource` is preserved. Since the SignalStore no longer creates the signals internally in these cases, signals passed into `withState` can also be changed externally. This is a foundational change to enable features like `withLinkedState` and potential support for `withResource`. The internal `STATE_SOURCE` is no longer represented as a single `WritableSignal` holding the entire state object. Instead, each top-level property becomes its own `WritableSignal`, or remains as-is if the user already provides a `WritableSignal`. ## Motivation - Internal creation of signals limited flexibility; users couldn’t bring their own signals into the store - Reusing existing signals enables future features like `withLinkedState` or `withResource`. - Splitting state into per-key signals improves the performance, because the root is not the complete state anymore. ## Change to `STATE_SOURCE` Given: ```ts type User = { firstname: string; lastname: string; }; ``` ### Before ```ts STATE_SOURCE: WritableSignal<User>; ``` ### Now ```ts STATE_SOURCE: { firstname: WritableSignal<string>; lastname: WritableSignal<string>; }; ``` ## Breaking Changes ### 1. Different object reference The returned object from `signalState()` or `getState()` no longer keeps the same object identity: ```ts const obj = { ngrx: 'rocks' }; const state = signalState(obj); ``` **Before:** ```ts state() === obj; // ✅ true ``` **Now:** ```ts state() === obj; // ❌ false ``` --- ### 2. No signal change on empty patch Empty patches no longer emit updates, since no signal is mutated: ```ts const state = signalState({ ngrx: 'rocks' }); let count = 0; effect(() => count++); TestBed.flushEffects(); expect(count).toBe(1); patchState(state, {}); ``` **Before:** ```ts expect(count).toBe(2); // triggered ``` **Now:** ```ts expect(count).toBe(1); // no update ``` --- ### 3. No wrapping of top-level `WritableSignal`s ```ts const Store = signalStore( withState({ foo: signal('bar') }) ); const store = new Store(); ``` **Before:** ```ts store.foo; // Signal<Signal<string>> ``` **Now:** ```ts store.foo; // Signal<string> ``` --- ### 4.: `patchState` no longer supports `Record` as root state Using a `Record`as the root state is no longer supported by `patchState`. **Before:** ```ts const Store = signalStore( { providedIn: 'root' }, withState<Record<number, number>>({}), withMethods((store) => ({ addNumber(num: number): void { patchState(store, { [num]: num, }); }, })) ); store.addNumber(1); store.addNumber(2); expect(getState(store)).toEqual({ 1: 1, 2: 2 }); ``` **After:** ```ts const Store = signalStore( { providedIn: 'root' }, withState<Record<number, number>>({}), withMethods((store) => ({ addNumber(num: number): void { patchState(store, { [num]: num, }); }, })) ); store.addNumber(1); store.addNumber(2); expect(getState(store)).toEqual({}); // ❌ Nothing updated ``` If dynamic keys are needed, consider managing them inside a nested signal instead. ## Further Changes - `signalStoreFeature` updated due to changes in `WritableStateSource` - `patchState` now uses `NoInfer` on `updaters` to prevent incorrect type inference when chaining
Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Generates and adds the properties of a `linkedSignal` to the store's state. ## Usage Notes: ```typescript const UserStore = signalStore( withState({ options: [1, 2, 3] }), withLinkedState(({ options }) => ({ selectOption: () => options()[0] ?? undefined })) ); ``` The resulting state is of type `{ options: number[], selectOption: number | undefined }`. Whenever the `options` signal changes, the `selectOption` will automatically update. For advanced use cases, `linkedSignal` can be called within `withLinkedState`: ```typescript const UserStore = signalStore( withState({ id: 1 }), withLinkedState(({ id }) => ({ user: linkedSignal({ source: id, computation: () => ({ firstname: '', lastname: '' }) }) })) ) ``` ## Implementation Notes We do not want to encourage wrapping larger parts of the state into a `linkedSignal`. This decision is primarily driven by performance concerns. When the entire state is bound to a single signal, any change - regardless of which part - - is tracked through that one signal. This means all direct consumers are notified, even if only a small slice of the state actually changed. Instead, each root property of the state should be a Signal on its own. That's why the design of `withLinkedState` cannot represent be the whole state.
7bc804e
to
7b04ea5
Compare
* ) | ||
* ``` | ||
* | ||
* @param linkedStateFactory A function that returns a an object literal with properties container an actual `linkedSignal` or the computation function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param linkedStateFactory A function that returns a an object literal with properties container an actual `linkedSignal` or the computation function. | |
* @param linkedStateFactory A function that returns an object literal with properties container an actual `linkedSignal` or the computation function. |
This allows an integration of external `WritableSignal`s — such as `linkedSignal` or `resource.value`. | ||
|
||
```ts | ||
import { linkedSignal } from '@angular/core'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { linkedSignal } from '@angular/core'; | |
import { linkedSignal, signal } from '@angular/core'; |
|
||
```ts | ||
import { linkedSignal } from '@angular/core'; | ||
import { signalState } from '@ngrx/signals'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { signalState } from '@ngrx/signals'; | |
import { patchState, signalState } from '@ngrx/signals'; |
This is a non-breaking feature to support
linkedSignal
.This branch is based on #4795 which has to be merged first.
Please read the comment in #4871
withLinkedState
generates and adds the properties of alinkedSignal
to the store's state.Usage Notes:
The resulting state is of type
{ options: number[], selectOption: number | undefined }
.Whenever the
options
signal changes, theselectOption
will automatically update.For advanced use cases,
linkedSignal
can be called withinwithLinkedState
:Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Closes #4871
What is the new behavior?
Does this PR introduce a breaking change?
Other information