diff --git a/projects/ngrx-rtk-query/src/lib/build-hooks.ts b/projects/ngrx-rtk-query/src/lib/build-hooks.ts index 1386d17..04c3732 100644 --- a/projects/ngrx-rtk-query/src/lib/build-hooks.ts +++ b/projects/ngrx-rtk-query/src/lib/build-hooks.ts @@ -1,5 +1,5 @@ import { DestroyRef, computed, effect, inject, isDevMode, signal, untracked } from '@angular/core'; -import type { Action, DefaultProjectorFn, MemoizedSelector } from '@ngrx/store'; +import type { Action, Selector } from '@reduxjs/toolkit'; import type { SubscriptionSelectors } from '@reduxjs/toolkit/dist/query/core/buildMiddleware/types'; import type { Api, @@ -25,9 +25,7 @@ import type { AngularHooksModuleOptions } from './module'; import type { GenericPrefetchThunk, MutationHooks, - MutationSelector, QueryHooks, - QuerySelector, QueryStateSelector, UseLazyQuerySubscription, UseMutation, @@ -75,7 +73,7 @@ export function buildHooks({ context, }: { api: Api; - moduleOptions: Required; + moduleOptions: AngularHooksModuleOptions; serializeQueryArgs: SerializeQueryArgs; context: ApiContext; }) { @@ -422,7 +420,6 @@ export function buildHooks({ return queryStateResults as any; }, - selector: select as QuerySelector, }; } @@ -466,16 +463,10 @@ export function buildHooks({ const requestId = computed(() => promiseRef()?.requestId); const selectDefaultResult = (requestId?: string) => fixedSelect({ fixedCacheKey, requestId }); - const mutationSelector = ( - requestId?: string, - ): MemoizedSelector, any, DefaultProjectorFn> => + const mutationSelector = (requestId?: string): Selector, any> => selectFromResult ? createSelector(selectDefaultResult(requestId), selectFromResult) - : (selectDefaultResult(requestId) as MemoizedSelector< - RootState, - any, - DefaultProjectorFn - >); + : selectDefaultResult(requestId); const currentState = computed(() => useSelector(mutationSelector(requestId()), { equal: shallowEqual })); const originalArgs = computed(() => (fixedCacheKey == null ? promiseRef()?.arg.originalArgs : undefined)); @@ -502,9 +493,6 @@ export function buildHooks({ return triggerMutation as any; }; - return { - useMutation, - selector: select as MutationSelector, - }; + return { useMutation }; } } diff --git a/projects/ngrx-rtk-query/src/lib/create-api.ts b/projects/ngrx-rtk-query/src/lib/create-api.ts index 9bddd97..8177638 100644 --- a/projects/ngrx-rtk-query/src/lib/create-api.ts +++ b/projects/ngrx-rtk-query/src/lib/create-api.ts @@ -1,6 +1,4 @@ -import type { Signal } from '@angular/core'; -import { Store, type Action } from '@ngrx/store'; -import type { SelectSignalOptions } from '@ngrx/store/src/models'; +import type { Action } from '@reduxjs/toolkit'; import { buildCreateApi, coreModule, @@ -9,21 +7,28 @@ import { type CoreModule, type CreateApi, } from '@reduxjs/toolkit/query'; -import { angularHooksModule, angularHooksModuleName, type AngularHooksModule, type Dispatch } from './module'; +import { + angularHooksModule, + angularHooksModuleName, + type AngularHooksModule, + type AngularHooksModuleOptions, + type Dispatch, +} from './module'; export const createApi: CreateApi = (options) => { - const reducerPath = options.reducerPath as string; - const next = (action: unknown): unknown => { if (typeof action === 'function') { - return action(dispatch, storeState, { injector: getApiInjector() }); + return action(dispatch, getState, { injector: getInjector() }); } - return storeDispatch(action as Action); + return store.hooks.dispatch(action as Action); }; const dispatch = (action: unknown): unknown => middleware(next)(action); - const getState = () => storeState(); - const useSelector = (mapFn: (state: any) => K, options?: SelectSignalOptions): Signal => - storeSelect(mapFn, options); + + const getState: AngularHooksModuleOptions['hooks']['getState'] = () => store.hooks.getState(); + const useSelector: AngularHooksModuleOptions['hooks']['useSelector'] = (mapFn, options) => + store.hooks.useSelector(mapFn, options); + const createSelector: AngularHooksModuleOptions['createSelector'] = (...input) => store.createSelector(...input); + const getInjector: AngularHooksModuleOptions['getInjector'] = () => store.getInjector(); const createApi = /* @__PURE__ */ buildCreateApi( coreModule(), @@ -33,46 +38,21 @@ export const createApi: CreateApi { - const injector = (api as unknown as Api, string, string, AngularHooksModule | CoreModule>) - .injector; - if (!injector) { - throw new Error( - `Provide the API (${reducerPath}) is necessary to use the queries. Did you forget to provide the queries api?`, - ); - } - return injector; - }; - - const getStore = () => { - const injector = getApiInjector(); - const store = injector.get(Store, undefined, { optional: true }); - if (!store) { - throw new Error(`Provide the Store is necessary to use the queries. Did you forget to provide the store?`); - } - return store; - }; - const storeDispatch = (action: Action) => { - getStore().dispatch(action); - return action; - }; - const storeState = () => { - const storeState: Record = getStore().selectSignal((state) => state)(); - return storeState?.[reducerPath] - ? storeState - : // Query inside forFeature (Code splitting) - { [reducerPath]: storeState }; + let store: AngularHooksModuleOptions; + const initApiStore = (setupFn: () => AngularHooksModuleOptions) => { + store = setupFn(); }; - const storeSelect = (mapFn: (state: any) => K, options?: SelectSignalOptions): Signal => - getStore().selectSignal(mapFn, options); + Object.assign(api, { initApiStore }); const middleware = ( api as unknown as Api, string, string, AngularHooksModule | CoreModule> - ).middleware({ dispatch, getState: storeState }); + ).middleware({ dispatch, getState }); return api; }; diff --git a/projects/ngrx-rtk-query/src/lib/module.ts b/projects/ngrx-rtk-query/src/lib/module.ts index 923adc3..969e013 100644 --- a/projects/ngrx-rtk-query/src/lib/module.ts +++ b/projects/ngrx-rtk-query/src/lib/module.ts @@ -1,7 +1,5 @@ -import { type Injector, type Signal } from '@angular/core'; -import { createSelectorFactory, defaultMemoize, type Action } from '@ngrx/store'; -import type { SelectSignalOptions } from '@ngrx/store/src/models'; -import type { ThunkAction } from '@reduxjs/toolkit'; +import type { Injector, Signal, ValueEqualityFn } from '@angular/core'; +import type { Action, Selector, ThunkAction } from '@reduxjs/toolkit'; import type { Api, BaseQueryFn, @@ -22,7 +20,7 @@ import { type MutationHooks, type QueryHooks, } from './types'; -import { capitalize, safeAssign, shallowEqual } from './utils'; +import { capitalize, safeAssign } from './utils'; export const angularHooksModuleName = /* @__PURE__ */ Symbol(); export type AngularHooksModule = typeof angularHooksModuleName; @@ -67,18 +65,16 @@ declare module '@reduxjs/toolkit/query' { /** * Provides access to the api injector. */ - injector: Injector; + getInjector: () => Injector; } & HooksWithUniqueNames; } } -const _createSelector = createSelectorFactory((projector) => defaultMemoize(projector, shallowEqual, shallowEqual)); - export interface AngularHooksModuleOptions { /** * The hooks from Redux to be used */ - hooks?: { + hooks: { /** * The version of the `dispatch` to be used */ @@ -90,12 +86,16 @@ export interface AngularHooksModuleOptions { /** * The version of the `useSelector` hook to be used */ - useSelector: (mapFn: (state: any) => K, options?: SelectSignalOptions) => Signal; + useSelector: (mapFn: (state: any) => K, options?: { equal?: ValueEqualityFn }) => Signal; }; /** * A selector creator (usually from `reselect`, or matching the same signature) */ - createSelector?: typeof _createSelector; + createSelector: (...input: any[]) => Selector; + /** + * The injector to be used + */ + getInjector: () => Injector; } /** @@ -105,13 +105,7 @@ export interface AngularHooksModuleOptions { * ```ts * const customCreateApi = buildCreateApi( * coreModule(), - * angularHooksModule({ - * hooks: { - * dispatch: createDispatchHook(MyContext), - * getState: createSelectorHook(MyContext), - * useSelector: createStoreHook(MyContext) - * } - * }) + * angularHooksModule(() => myCreateAngularHooksModule()) * ); * ``` * @@ -119,25 +113,27 @@ export interface AngularHooksModuleOptions { */ export const angularHooksModule = ({ hooks, - createSelector = _createSelector, -}: AngularHooksModuleOptions = {}): Module => { + createSelector, + getInjector, +}: AngularHooksModuleOptions): Module => { return { name: angularHooksModuleName, init(api, { serializeQueryArgs }, context) { const anyApi = api as any as Api, any, any, AngularHooksModule>; const { buildQueryHooks, buildMutationHook, usePrefetch } = buildHooks({ api, - moduleOptions: { hooks: hooks!, createSelector }, + moduleOptions: { hooks, createSelector, getInjector }, serializeQueryArgs, context, }); safeAssign(anyApi, { usePrefetch }); - safeAssign(anyApi, { dispatch: hooks!.dispatch }); + safeAssign(anyApi, { dispatch: hooks.dispatch }); + safeAssign(anyApi, { getInjector }); return { injectEndpoint(endpointName, definition) { if (isQueryDefinition(definition)) { - const { useQuery, useLazyQuery, useLazyQuerySubscription, useQueryState, useQuerySubscription, selector } = + const { useQuery, useLazyQuery, useLazyQuerySubscription, useQueryState, useQuerySubscription } = buildQueryHooks(endpointName); safeAssign(anyApi.endpoints[endpointName], { useQuery, @@ -145,13 +141,12 @@ export const angularHooksModule = ({ useLazyQuerySubscription, useQueryState, useQuerySubscription, - selector, }); (api as any)[`use${capitalize(endpointName)}Query`] = useQuery; (api as any)[`useLazy${capitalize(endpointName)}Query`] = useLazyQuery; } else if (isMutationDefinition(definition)) { - const { useMutation, selector } = buildMutationHook(endpointName); - safeAssign(anyApi.endpoints[endpointName], { useMutation, selector }); + const { useMutation } = buildMutationHook(endpointName); + safeAssign(anyApi.endpoints[endpointName], { useMutation }); (api as any)[`use${capitalize(endpointName)}Mutation`] = useMutation; } }, diff --git a/projects/ngrx-rtk-query/src/lib/provide-store-api.ts b/projects/ngrx-rtk-query/src/lib/provide-store-api.ts index c902d11..4ab5f54 100644 --- a/projects/ngrx-rtk-query/src/lib/provide-store-api.ts +++ b/projects/ngrx-rtk-query/src/lib/provide-store-api.ts @@ -1,12 +1,49 @@ import { - ApplicationRef, ENVIRONMENT_INITIALIZER, + Injector, inject, makeEnvironmentProviders, type EnvironmentProviders, + type Signal, } from '@angular/core'; -import { provideState } from '@ngrx/store'; +import { Store, createSelectorFactory, defaultMemoize, provideState, type Action } from '@ngrx/store'; +import type { SelectSignalOptions } from '@ngrx/store/src/models'; import { setupListeners as setupListenersFn, type Api } from '@reduxjs/toolkit/query'; +import type { AngularHooksModuleOptions, Dispatch } from './module'; +import { shallowEqual } from './utils'; + +const createStoreApi = ( + api: Api, string, string, any>, + { injector = inject(Injector) }: { injector?: Injector } = {}, +) => { + return (): AngularHooksModuleOptions => { + const store = injector.get(Store, undefined, { optional: true }); + if (!store) { + throw new Error(`Provide the Store is necessary to use the queries. Did you forget to provide the store?`); + } + + const dispatch = (action: Action) => { + store.dispatch(action); + return action; + }; + const reducerPath = api.reducerPath as string; + const getState = () => { + const storeState: Record = store.selectSignal((state) => state)(); + return storeState?.[reducerPath] + ? storeState + : // Query inside forFeature (Code splitting) + { [reducerPath]: storeState }; + }; + const useSelector = (mapFn: (state: any) => K, options?: SelectSignalOptions): Signal => + store.selectSignal(mapFn, options); + + const hooks = { dispatch: dispatch as Dispatch, getState, useSelector }; + const createSelector = createSelectorFactory((projector) => defaultMemoize(projector, shallowEqual, shallowEqual)); + const getInjector = () => injector; + + return { hooks, createSelector, getInjector }; + }; +}; export interface StoreQueryConfig { setupListeners?: Parameters[1] | false; @@ -23,8 +60,7 @@ export function provideStoreApi( provide: ENVIRONMENT_INITIALIZER, multi: true, useValue() { - const appRef = inject(ApplicationRef); - Object.assign(api, { injector: appRef.injector }); + api.initApiStore(createStoreApi(api)); }, }, provideState(api.reducerPath, api.reducer), diff --git a/projects/ngrx-rtk-query/src/lib/types/hooks-types.ts b/projects/ngrx-rtk-query/src/lib/types/hooks-types.ts index bd7dc0c..8ab2857 100644 --- a/projects/ngrx-rtk-query/src/lib/types/hooks-types.ts +++ b/projects/ngrx-rtk-query/src/lib/types/hooks-types.ts @@ -1,5 +1,4 @@ import type { Signal } from '@angular/core'; -import type { MemoizedSelector } from '@ngrx/store'; import type { ThunkAction, UnknownAction } from '@reduxjs/toolkit'; import type { BaseQueryFn, @@ -11,7 +10,6 @@ import type { QueryActionCreatorResult, QueryArgFrom, QueryDefinition, - QueryResultSelectorResult, QueryStatus, QuerySubState, ResultTypeFrom, @@ -30,21 +28,12 @@ export interface QueryHooks; useLazyQuerySubscription: UseLazyQuerySubscription; useQueryState: UseQueryState; - selector: QuerySelector; } export interface MutationHooks> { useMutation: UseMutation; - selector: MutationSelector; } -export type QuerySelector> = ( - queryArg: QueryArgFrom | SkipToken, -) => MemoizedSelector, QueryResultSelectorResult>; -export type MutationSelector> = ( - requestId: string | { requestId: string | undefined; fixedCacheKey: string | undefined } | SkipToken, -) => MemoizedSelector, MutationResultSelectorResult>; - /** * A hook that automatically triggers fetches of data from an endpoint, 'subscribes' the component * to the cached data, and reads the request status and cached data from the Redux store. The component diff --git a/src/app/features/counter/counter-manager/counter-manager.component.ts b/src/app/features/counter/counter-manager/counter-manager.component.ts index 4883217..3a0a86f 100644 --- a/src/app/features/counter/counter-manager/counter-manager.component.ts +++ b/src/app/features/counter/counter-manager/counter-manager.component.ts @@ -1,5 +1,6 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { useDecrementCountMutation, useGetCountQuery, useIncrementCountMutation } from '@app/core/services'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { counterApi, useDecrementCountMutation, useGetCountQuery, useIncrementCountMutation } from '@app/core/services'; +import { Store } from '@ngrx/store'; import { nanoid } from '@reduxjs/toolkit'; @Component({ @@ -19,6 +20,13 @@ import { nanoid } from '@reduxjs/toolkit';

{{ countQuery() | json }}

+ +
+ Data from selector: +

+ {{ test() | json }} +

+
@@ -42,6 +50,8 @@ export class CounterManagerComponent { increment = useIncrementCountMutation(); decrement = useDecrementCountMutation(); + test = inject(Store).selectSignal(counterApi.endpoints.getCount.select()); + counters: string[] = []; addCounter(): void {