Skip to content

RFC: withFeatureFactory #4913

@RomainDood

Description

@RomainDood

Which @ngrx/* package(s) are relevant/related to the feature request?

signals

Information

Hi,

I believe I've found a better approach than the existing withFeature function to implement custom SignalStore features that depend on an input value from the store.

Currently, to add a custom feature that requires an input from the store, developers must manually use the withFeature utility.
While this works, it introduces boilerplate and breaks the composability of features, since the custom feature cannot be passed directly into the signalStore factory.

The goal is to make it possible to use custom signalStoreFeatures in the same way as built-in functions like withState, withMethods, etc.
This improves readability and results in cleaner, more consistent code.

Here's a comparison between using withFeature and the proposed withFeatureFactory:

const filteredBooksFeature = (books: Signal<Book[]>) =>
  signalStoreFeature(
    withState({ query: '' }),
    withComputed((store) => ({
      filteredBooks: computed(() =>
        books().filter((b) => b.name.includes(store.query()))
      ),
    })),
    withMethods((store) => ({
      setQuery(query: string): void {
        patchState(store, { query });
      },
    }))
  );

const withBooksFilter = withFeatureFactory(filteredBooksFeature);

const BooksStore = signalStore(
  withEntities<Book>(),
  // ⚠️ withfeature 
  withFeature(({ entities }) => filteredBooksFeature(entities)), // 👈 NgRx example
  // ✅ withFeatureFactory👇 full access to the store with type-safety
  withBooksFilter(({ entities }) => entities)
);

The returned type of the inner function must match the type of the first parameter of the feature (in this case, filteredBooksFeature).
This is fully type-safe and enforced by TypeScript.

However, there is one limitation: the custom feature (like filteredBooksFeature) must accept exactly one parameter.
Otherwise, the inner function (withBooksFilter(({ entities }) => entities)) would have to return an array, which would be semantically strange and less intuitive.

To avoid this, I chose to enforce that the feature factory only accepts a single parameter.
If multiple values need to be passed, they can be wrapped in a single object with multiple properties.

// ❌ Not allowed:
withFeatureFactory((signalA: A, signalB: B) => ...)

// ✅ Good:
withFeatureFactory(({ signalA, signalB }: {signalA: A, signalB: B}) => ...)

Here's a reproduction where I explored different approaches to find the most ergonomic and type-safe solution:
https://stackblitz.com/edit/stackblitz-starters-31qrd2nq?file=withFeature%2F1-simple-only-one-helper-to-use.spec.ts

And I made an article about that (https://dev.to/romain_geffrault_10d88369/ngrx-signalstore-hacks-beautiful-dx-with-custom-features-1n4k)

Describe any alternatives/workarounds you're currently using

No response

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

  • Yes
  • No

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions