-
-
Notifications
You must be signed in to change notification settings - Fork 2k
add withResource
#4834
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
Draft
rainerhahnekamp
wants to merge
13
commits into
ngrx:main
Choose a base branch
from
rainerhahnekamp:signals/feat/with-resource
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
add withResource
#4834
rainerhahnekamp
wants to merge
13
commits into
ngrx:main
from
rainerhahnekamp:signals/feat/with-resource
+1,662
−136
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
…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>
Adds `withResource`, a SignalStore feature for integrating any `ResourceRef` into the store instance. This includes `ResourceRef`s returned from framework-provided helpers like `resource`, `rxResource`, or `httpResource`, as well as custom user-defined implementations. Supports two integration modes: --- **Unnamed Resource (Inheritance)** Spreads all members of the `Resource` onto the SignalStore, making the store implement the `Resource` API directly. The `reload` method is added as a private `_reload()` method. ```ts const UserStore = signalStore( withState({ userId: undefined as number | undefined }), withResource(({ userId }) => httpResource<User>(() => userId === undefined ? undefined : `/users/${userId}` ) ) ); const userStore = new UserStore(); userStore.value(); // User | undefined ``` --- **Named Resources (Composition)** Supports multiple resources by passing a `Record<string, ResourceRef>`. Each resource is prefixed, and its members are merged into the SignalStore. The `reload` method becomes `_{resourceName}Reload()`. ```ts const UserStore = signalStore( withState({ userId: undefined as number | undefined }), withResource(({ userId }) => ({ list: httpResource<User[]>(() => '/users', { defaultValue: [] }), detail: httpResource<User>(() => userId === undefined ? undefined : `/users/${userId}` ), })) ); const userStore = new UserStore(); userStore.listValue(); // [] userStore.detailValue(); // User | undefined ``` --- **Named Resources and `Resource`** The `mapToResource` helper maps a named resource to the standard `Resource` interface. This is useful when working with APIs or utilities that require a `Resource<T>` type. ```ts function processUserResource(userResource: Resource<User | undefined>) { // ... } const userStore = new UserStore(); const userResource = mapToResource(userStore, 'detail'); // Resource<User | undefined> processUserResource(userResource); ```
✅ 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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Introduces
withResource
according to #4833This branch is based on #4795 which has to be merged first.
PR Checklist
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 #4833
What is the new behavior?
Does this PR introduce a breaking change?
Other information