Skip to content

feat(signals): enhance withComputed to accept computation functions #4822

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

Conversation

rainerhahnekamp
Copy link
Contributor

Example:

const CounterStore = signalStore(
  withState({ count: 0 }),
  withComputed(({ count }) => ({
    doubleCount: () => count() * 2,
  })),
);
  • Updated withComputed to automatically wrap functions in computed signals.
  • Added tests to verify automatic computation and mixing user-defined computeds.
  • Improved documentation to clarify the factory function's return type.

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

[ ] Bugfix
[x] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Other... Please describe:

What is the current behavior?

Closes #4782

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Copy link

netlify bot commented Jun 7, 2025

Deploy Preview for ngrx-io failed.

Built without sensitive environment variables

Name Link
🔨 Latest commit 370a474
🔍 Latest deploy log https://app.netlify.com/projects/ngrx-io/deploys/685daeefe41b6000082c3c3e

Copy link

netlify bot commented Jun 7, 2025

Deploy Preview for ngrx-site-v19 ready!

Name Link
🔨 Latest commit 370a474
🔍 Latest deploy log https://app.netlify.com/projects/ngrx-site-v19/deploys/685daeef0dcd410008237be2
😎 Deploy Preview https://deploy-preview-4822--ngrx-site-v19.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@@ -54,4 +59,34 @@ describe('withComputed', () => {
'p1, s2, m1, Symbol(computed_secret)'
);
});

it('adds computed automatically if the value is function', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can add a type test to signal-store.types.spec.ts to validate the new withComputed behavior just in case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 16 to 18
[P in keyof ComputedSignals]: ComputedSignals[P] extends () => infer V
? Signal<V>
: ComputedSignals[P];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provided Signals are also compatible with () => infer V type, but we should not change their type (e.g. when DeepSignal is provided to withComputed by using deepComputed helper).

Suggested change
[P in keyof ComputedSignals]: ComputedSignals[P] extends () => infer V
? Signal<V>
: ComputedSignals[P];
[K in keyof ComputedInput]: ComputedInput[K] extends Signal<any>
? ComputedInput[K]
: ComputedInput[K] extends () => infer V ? Signal<V> : never;

When adding a type test, please also add one computed signal by using deepComputed and one writable signal to ensure their type hasn't been changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, I went here also with the term ComputedDictionary. Also used unknown instead any. Please see me question in the comment below.

return withProps(signalsFactory);
return withProps((store) => {
const signals = signalsFactory(store);
const stateKeys = Reflect.ownKeys(signals);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const stateKeys = Reflect.ownKeys(signals);
const computedResultKeys = Reflect.ownKeys(signals);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -145,7 +145,7 @@ export class BookSearch {

Computed signals can be added to the store using the `withComputed` feature.
This feature accepts a factory function as an input argument, which is executed within the injection context.
The factory should return a dictionary of computed signals, utilizing previously defined state signals and properties that are accessible through its input argument.
The factory should return a dictionary containing either computed signals or functions that return values (which will automatically be wrapped in computed signals), utilizing previously defined state signals and properties that are accessible through its input argument.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The factory should return a dictionary containing either computed signals or functions that return values (which will automatically be wrapped in computed signals), utilizing previously defined state signals and properties that are accessible through its input argument.
The factory should return a dictionary containing either computed signals or computation functions (which are automatically converted to computed signals), utilizing previously defined state signals and properties that are accessible through its input argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markostanimirovic @timdeschryver, do we want to use the term "computation functions"? I am afraid that this could confuse users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine. We can always revisit later.

@rainerhahnekamp
Copy link
Contributor Author

@markostanimirovic, @timdeschryver.

I've applied the suggested changes with the following additions:

  • ComputedResult calls its type now also ComputedRecord instead ComputedInput.
  • @markostanimirovic, you were suggesting [P in keyof ComputedDictionary]: ComputedDictionary[P] extends Signal<any>; I used [P in keyof ComputedDictionary]: ComputedDictionary[P] extends Signal<unknown>. Is there a reason why any should be preferred over unkown.
  • Type tests have been added to verify that we stick to the Signal type when that one is provided.

rainerhahnekamp and others added 8 commits June 22, 2025 11:08
… to functions

Example:

```ts
const CounterStore = signalStore(
  withState({ count: 0 }),
  withComputed(({ count }) => ({
    doubleCount: () => count() * 2,
  })),
);
```

- Updated withComputed to automatically wrap functions in computed signals.
- Added tests to verify automatic computation and mixing user-defined computeds.
- Improved documentation to clarify the factory function's return type.
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
@rainerhahnekamp rainerhahnekamp force-pushed the signals/feat/add-computed-on-computation-function-in-with-computed branch from 9352161 to 5e38ee7 Compare June 22, 2025 09:08
@rainerhahnekamp
Copy link
Contributor Author

As discussed, the latest commit enables access to methods within withComputed. This is a necessary step toward supporting a potential withResource feature in the 20.x release line.

When a resource is in an error state, accessing its value will throw. Therefore, users must first verify the presence of a value using the hasValue method.

In the meantime, I am going to prepare the updated docs (in a separate PR).

Since this is a significant change, I've also asked @timdeschryver for a re-review.

const Store = signalStore(
  withResource(() =>
    // spreads members of the resource to the store, including hasValue as method
    resource({
      loader: () => Promise.resolve({ firstName: 'John', lastName: 'Doe' }),
    })
  ),
  withComputed((store) => ({
    safeUser: () =>
      store.user.hasValue()
        ? store.user.value()
        : { firstName: '', lastName: '' },
  }))
);

@rainerhahnekamp rainerhahnekamp force-pushed the signals/feat/add-computed-on-computation-function-in-with-computed branch from 7b0cf69 to 5e38ee7 Compare June 24, 2025 11:31
Copy link
Member

@timdeschryver timdeschryver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@maxime1992
Copy link
Contributor

I'd very much like to have the ability to provide a function with a parameter that returns a signal in order to create an equivalent of a factory selector in ngrx store world. I know this can be achieved in the withMethod block but wouldn't it make sense to be able to do that from withComputed when it just all comes down to dealing with signals and not just observables etc?

Just asking because of this comment which would I assume make that hard:

Updated withComputed to automatically wrap functions in computed signals

But maybe the recommendation is to just use withMethod for that?

rainerhahnekamp and others added 3 commits June 26, 2025 21:43
Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Co-authored-by: michael-small <33669563+michael-small@users.noreply.github.com>
@rainerhahnekamp
Copy link
Contributor Author

@maxime1992

I'd very much like to have the ability to provide a function with a parameter that returns a signal in order to create an equivalent of a factory selector in ngrx store world.

Yes, factory functions for computed are actually methods. So they should go in withMethods. The computed itself is of course also a function in technical terms, but I hope you see the difference in a function creating a computed vs. a function which is wrapped by a computed

Copy link
Member

@markostanimirovic markostanimirovic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 Great work Rainer! 👏

@markostanimirovic markostanimirovic changed the title feat(signals): enhance withComputed to automatically add computed feat(signals): enhance withComputed to accept computation functions Jun 26, 2025
@markostanimirovic markostanimirovic merged commit c8b15dd into ngrx:main Jun 26, 2025
10 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@ngrx/signals: Allow returning computation functions from withComputed callback
5 participants