Skip to content
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

Chaining custom signal store features with input declared as functions fails #4274

Open
2 tasks
skovalyov opened this issue Mar 7, 2024 · 2 comments
Open
2 tasks

Comments

@skovalyov
Copy link

Which @ngrx/* package(s) are the source of the bug?

signals

Minimal reproduction of the bug/regression with instructions

I have a signal store and two signal store features with input declared as functions.

const withFooFeature = () =>
  signalStoreFeature(
    {
      state: type<{ foo: number }>()
    },
    withState({})
  );

const withBarFeature = () =>
  signalStoreFeature(
    {
      state: type<{ bar: number }>()
    },
    withState({})
  );

export const FooBarStore = signalStore(
  withState({
    foo: 10,
    bar: 20
  }),
  withComputed(store => ({
    foofoo: computed(() => store.foo() * store.foo()),
    barbar: computed(() => store.bar() * store.bar())
  })),
  withFooFeature(),
  withBarFeature()
);

And it fails.

If I add only one feature, it works:

export const FooBarStore = signalStore(
  withState({
    foo: 10,
    bar: 20
  }),
  withComputed(store => ({
    foofoo: computed(() => store.foo() * store.foo()),
    barbar: computed(() => store.bar() * store.bar())
  })),
  withFooFeature()
);

It also works if I add features inline:

export const FooBarStore = signalStore(
  withState({
    foo: 10,
    bar: 20
  }),
  withComputed(store => ({
    foofoo: computed(() => store.foo() * store.foo()),
    barbar: computed(() => store.bar() * store.bar())
  })),
  signalStoreFeature(
    {
      state: type<{ foo: number }>()
    },
    withState({})
  ),
  signalStoreFeature(
    {
      state: type<{ bar: number }>()
    },
    withState({})
  )
);

The same problem occurs with other setups, when the feature input contains not state, but signals.

I have created a playground, where the problem can be reproduced https://stackblitz.com/edit/stackblitz-starters-dm1urm?file=src%2Ffoobar.store.ts

Expected behavior

It should be possible to chain signal store features, so that it is possible to create a better structure for more complicated signal states, where several features can access shared state slices, computed signals and methods.

export const FooBarStore = signalStore(
  withState({
    foo: 10,
    bar: 20
  }),
  withComputed(store => ({
    foofoo: computed(() => store.foo() * store.foo()),
    barbar: computed(() => store.bar() * store.bar())
  })),
  withFooFeature(),
  withBarFeature()
);

Versions of NgRx, Angular, Node, affected browser(s) and operating system(s)

NgRx: 17.1.1
Angular: 17.2.4
Node: 18.16.0

Other information

No response

I would be willing to submit a PR to fix this issue

  • Yes
  • No
@skovalyov
Copy link
Author

skovalyov commented Mar 7, 2024

I looked more into tests such as https://github.com/ngrx/platform/blob/main/modules/signals/spec/types/signal-store.types.spec.ts, and it turned out that this syntax would work, i.e. with <_> in function declaration:

const withFooFeature = <_>() =>
  signalStoreFeature(
    {
      state: type<{ foo: number }>()
    },
    withState({})
  );

const withBarFeature = <_>() =>
  signalStoreFeature(
    {
      state: type<{ bar: number }>()
    },
    withState({})
  );

export const FooBarStore = signalStore(
  withState({
    foo: 10,
    bar: 20
  }),
  withComputed(store => ({
    foofoo: computed(() => store.foo() * store.foo()),
    barbar: computed(() => store.bar() * store.bar())
  })),
  withFooFeature(),
  withBarFeature()
);

It is not very intuitive, so I think documentation update would make sense.

@markostanimirovic
Copy link
Member

Correct, it can be fixed with unused generic. And I agree with you, this is unintuitive TypeScript behavior.

Once we change SignalStore types to disable property/method overriding, the type implementation will be simpler, so this issue may disappear. Let's see.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants