Skip to content

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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

rainerhahnekamp
Copy link
Contributor

@rainerhahnekamp rainerhahnekamp commented Jun 4, 2025

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 #4781


withLinkedState generates and adds the properties of a linkedSignal to the store's state.

Usage Notes:

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:

const UserStore = signalStore(
  withState({ id: 1 }),
  withLinkedState(({ id }) => linkedSignal({
    source: id,
    computation: () => ({ firstname: '', lastname: '' })
  }))
)

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 #4781

What is the new behavior?

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Other information

Copy link

netlify bot commented Jun 4, 2025

Deploy Preview for ngrx-io ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit ce8d4b0
🔍 Latest deploy log https://app.netlify.com/projects/ngrx-io/deploys/686d77ee917cad0008bd7304
😎 Deploy Preview https://deploy-preview-4818--ngrx-io.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.

Copy link

netlify bot commented Jun 4, 2025

Deploy Preview for ngrx-site-v19 ready!

Name Link
🔨 Latest commit ce8d4b0
🔍 Latest deploy log https://app.netlify.com/projects/ngrx-site-v19/deploys/686d77ee57abc700081fe32a
😎 Deploy Preview https://deploy-preview-4818--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.

@rainerhahnekamp rainerhahnekamp force-pushed the signals/feat/with-linked-state branch from 7bc804e to 7b04ea5 Compare June 7, 2025 22:11
@rainerhahnekamp rainerhahnekamp marked this pull request as ready for review June 16, 2025 16:25
@markostanimirovic markostanimirovic added the Needs Cleanup Review changes needed label Jun 29, 2025
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.
@rainerhahnekamp rainerhahnekamp force-pushed the signals/feat/with-linked-state branch from 7b04ea5 to 4337ebf Compare July 3, 2025 18:57
Co-authored-by: michael-small <33669563+michael-small@users.noreply.github.com>
@rainerhahnekamp rainerhahnekamp removed the Needs Cleanup Review changes needed label Jul 3, 2025
@rainerhahnekamp
Copy link
Contributor Author

@markostanimirovic, @timdeschryver this one has been rebased to main and is now ready for review.

@rainerhahnekamp rainerhahnekamp marked this pull request as draft July 3, 2025 23:23
@rainerhahnekamp rainerhahnekamp marked this pull request as ready for review July 4, 2025 11:05
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.

We should add a new documentation page - "Linked State". It can be added after the "Custom Store Properties" page in the navigation.

1

rainerhahnekamp and others added 9 commits July 7, 2025 13:14
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.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>
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
@rainerhahnekamp
Copy link
Contributor Author

Closing because of stalling github processing (more than 2 hours)

image

@rainerhahnekamp
Copy link
Contributor Author

@markostanimirovic, @timdeschryver, I think I've applied all necessary changes (including the docs). We can do another round of review.

- Functions that return values, which the SignalStore wraps automatically into a `linkedSignal()`, or
- `WritableSignal`, which the user can create with `linkedSignal()`.

### Implicit Linking

Choose a reason for hiding this comment

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

I think all three of these ### h3's should be ## h2's due to the precedence of header sizes

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


<code-example header="book-store.ts">

import { signalStore, linkedSignal } from '@ngrx/signals';
Copy link

@msmallest msmallest Jul 7, 2025

Choose a reason for hiding this comment

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

Suggested change
import { signalStore, linkedSignal } from '@ngrx/signals';
import { linkedSignal } from '@angular/core';
import { signalStore, withLinkedState, withState } from '@ngrx/signals';

Copy link
Member

Choose a reason for hiding this comment

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

linkedSignal should be imported from @angular/core, not @ngrx/signals. withLinkedState import is missing

Choose a reason for hiding this comment

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

Ah yes, good catch, thanks.

Suggestion updated

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


<code-example header="book-store.ts">

import { signalStore, linkedSignal, withLinkedState } from '@ngrx/signals';
Copy link

@msmallest msmallest Jul 7, 2025

Choose a reason for hiding this comment

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

Suggested change
import { signalStore, linkedSignal, withLinkedState } from '@ngrx/signals';
import { linkedSignal } from '@angular/core';
import { signalStore, withLinkedState, withState } from '@ngrx/signals';

edit: imports respectively from Angular itself and the store

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

safeSelectedOption: linkedSignal({
source: selectedIx,
computation: (sel, previous) => {
const ix = selectedIx();

Choose a reason for hiding this comment

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

This should be sel I think

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 (example changed)

safeSelectedOption: linkedSignal({
source: selectedIx,
computation: (sel, previous) => {
const ix = selectedIx();

Choose a reason for hiding this comment

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

Should be sel?

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 (example changed)


<code-example header="book-store.ts">

import { signalStore, linkedSignal } from '@ngrx/signals';
Copy link
Member

Choose a reason for hiding this comment

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

linkedSignal should be imported from @angular/core, not @ngrx/signals. withLinkedState import is missing

rainerhahnekamp and others added 7 commits July 8, 2025 07:59
…te.md

Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
…te.md

Co-authored-by: Michael Small <85510853+msmallest@users.noreply.github.com>
…te.md

Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
…te.md

Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
…te.md

Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
…te.md

Co-authored-by: Marko Stanimirović <markostanimirovic95@gmail.com>
@rainerhahnekamp
Copy link
Contributor Author

@markostanimirovic, @michael-small

Wow, what a review. Corrections outnumbered the actual code by a margin...a review for the records 😅 . I seriously owe you both a drink for that.


I've applied all the changes and double-checked. I also updated the jsdoc for withLinkedState so that it matches the examples of the docs.

Copy link
Contributor

@michael-small michael-small left a comment

Choose a reason for hiding this comment

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

LGTM

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: Add withLinkedState
4 participants