Skip to content

RFC: withKeyed signal store feature #4825

@Devin-Harris

Description

@Devin-Harris

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

signals

Information

I have found I often have reusable signal store features that I want to use multiple times in the same signal store or in other features with property name constraints. Having the ability to pull in all the state, methods, properties, etc.. for a given feature under some unique key defined where you use the feature would be useful.

Something like the following is what I am thinking as far as api

const withCommonFeature = () =>
   signalStoreFeature(
      withState(() => ({ commonField: 'Hello world!' })),
      withMethods((store) => ({
         setField: (value: string) => {
            patchState(store, setField(value));
         },
      }))
   );

const Store = signalStore(
   withKeyed('A', withCommonFeature()),
   withKeyed('B', withCommonFeature()),
   withMethods((store) => ({
      log() {
         console.log(store['A'].commonField());
         console.log(store['B'].commonField());
      },
   }))
);

withKeyed would be the function defined in the standard library in this example.

I have a somewhat working fork, if this feature is considered.
Note it is still a little rough around utilising writable state so any ideas on that could be discussed here. The main con in my current pass is I have a watchState because I am not running the keyed feature on the root store , so two stateSources need to be kept in sync when patches happen inside the inner and outer stores.

Describe any alternatives/workarounds you're currently using

The project I am working in is still on v18, so my workaround is for that version of signal store (i can port this to v19 for those interested if need be). Its a rough version using native withComputed as shown here:


type ComputedSignalsKeyType<O extends SignalStoreFeatureResult> = keyof O['computed'];
type ComputedSignalsValueType<O extends SignalStoreFeatureResult> = {
   [x in ComputedSignalsKeyType<O>]: O['computed'][x] extends Signal<infer V> ? V : O['computed'][x];
};

type WithKeyedOutputComputedResult<O extends SignalStoreFeatureResult> = O['computed'] &
   Signal<O['state'] & ComputedSignalsValueType<O>> &
   O['methods'];
type WithKeyedOutputStateResult<O extends SignalStoreFeatureResult> = O['state'] & ComputedSignalsValueType<O>;
type WithKeyedOutputResult<T extends string | symbol, O extends SignalStoreFeatureResult> = EmptyFeatureResult & {
   state: { [x in T]: WithKeyedOutputStateResult<O> };
   computed: {
      [x in T]: WithKeyedOutputComputedResult<O>;
   };
   methods: {};
};

export function withKeyed<
   T extends string | symbol,
   I extends SignalStoreFeatureResult,
   O extends SignalStoreFeatureResult,
>(key: T, innerFeature: SignalStoreFeature<I, O>): SignalStoreFeature<I, WithKeyedOutputResult<T, O>> {
   return (store) => {
      const storeForFactory = inject(
         signalStore(
            { providedIn: 'root' },
            withState(() => ({}))
         )
      ) as Parameters<SignalStoreFeature<I, O>>[0];

      const storeForFactoryWithHooks = {
         ...storeForFactory,
         hooks: (store as any).hooks ?? {},
      };

      const _innerStore = innerFeature(storeForFactoryWithHooks);

      const computedValues = computed(() => {
         const computedSignals = _innerStore.computedSignals;
         const computedKeys: ComputedSignalsKeyType<O>[] = Object.keys(computedSignals);
         return computedKeys.reduce((acc, k) => {
            acc[k] = isSignal(computedSignals[k]) ? computedSignals[k]() : (computedSignals[k] as any);
            return acc;
         }, {} as ComputedSignalsValueType<O>);
      });
      const state = computed(() => {
         return { ...getState(_innerStore), ...computedValues() };
      });
      Object.assign(state, _innerStore.methods);
      Object.assign(state, _innerStore.computedSignals);
      Object.assign(state, _innerStore.stateSignals);

      const storeWithKey = withComputed(
         () =>
            ({
               [key]: state,
            }) as {
               [x in T]: WithKeyedOutputComputedResult<O>;
            }
      )(store);
      const storeWithHooks = withHooks(() => _innerStore.hooks)(storeWithKey) as any;

      return storeWithHooks;
   };
}

Without some of the internal symbols, types, and helper methods, its a little verbose and limited on how writes inside the keyed feature are handled. Especially when their are keyed features inside other keyed features, thus the feature request.

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