Skip to content

Commit

Permalink
feat: remove selector to avoid ngrx-store imports in core
Browse files Browse the repository at this point in the history
  • Loading branch information
SaulMoro committed Mar 15, 2024
1 parent 562f121 commit 65076d5
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 103 deletions.
22 changes: 5 additions & 17 deletions projects/ngrx-rtk-query/src/lib/build-hooks.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,9 +25,7 @@ import type { AngularHooksModuleOptions } from './module';
import type {
GenericPrefetchThunk,
MutationHooks,
MutationSelector,
QueryHooks,
QuerySelector,
QueryStateSelector,
UseLazyQuerySubscription,
UseMutation,
Expand Down Expand Up @@ -75,7 +73,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
context,
}: {
api: Api<any, Definitions, any, any, CoreModule>;
moduleOptions: Required<AngularHooksModuleOptions>;
moduleOptions: AngularHooksModuleOptions;
serializeQueryArgs: SerializeQueryArgs<any>;
context: ApiContext<Definitions>;
}) {
Expand Down Expand Up @@ -422,7 +420,6 @@ export function buildHooks<Definitions extends EndpointDefinitions>({

return queryStateResults as any;
},
selector: select as QuerySelector<any>,
};
}

Expand Down Expand Up @@ -466,16 +463,10 @@ export function buildHooks<Definitions extends EndpointDefinitions>({

const requestId = computed(() => promiseRef()?.requestId);
const selectDefaultResult = (requestId?: string) => fixedSelect({ fixedCacheKey, requestId });
const mutationSelector = (
requestId?: string,
): MemoizedSelector<RootState<Definitions, any, any>, any, DefaultProjectorFn<any>> =>
const mutationSelector = (requestId?: string): Selector<RootState<Definitions, any, any>, any> =>
selectFromResult
? createSelector(selectDefaultResult(requestId), selectFromResult)
: (selectDefaultResult(requestId) as MemoizedSelector<
RootState<Definitions, any, any>,
any,
DefaultProjectorFn<any>
>);
: selectDefaultResult(requestId);

const currentState = computed(() => useSelector(mutationSelector(requestId()), { equal: shallowEqual }));
const originalArgs = computed(() => (fixedCacheKey == null ? promiseRef()?.arg.originalArgs : undefined));
Expand All @@ -502,9 +493,6 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
return triggerMutation as any;
};

return {
useMutation,
selector: select as MutationSelector<any>,
};
return { useMutation };
}
}
66 changes: 23 additions & 43 deletions projects/ngrx-rtk-query/src/lib/create-api.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<typeof coreModuleName | typeof angularHooksModuleName> = (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 = <K>(mapFn: (state: any) => K, options?: SelectSignalOptions<K>): Signal<K> =>
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(),
Expand All @@ -33,46 +38,21 @@ export const createApi: CreateApi<typeof coreModuleName | typeof angularHooksMod
getState,
useSelector,
},
createSelector,
getInjector,
}),
);
const api = createApi(options);

const getApiInjector = () => {
const injector = (api as unknown as Api<any, Record<string, any>, 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<string, any> = 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 = <K>(mapFn: (state: any) => K, options?: SelectSignalOptions<K>): Signal<K> =>
getStore().selectSignal(mapFn, options);
Object.assign(api, { initApiStore });

const middleware = (
api as unknown as Api<any, Record<string, any>, string, string, AngularHooksModule | CoreModule>
).middleware({ dispatch, getState: storeState });
).middleware({ dispatch, getState });

return api;
};
47 changes: 21 additions & 26 deletions projects/ngrx-rtk-query/src/lib/module.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -67,18 +65,16 @@ declare module '@reduxjs/toolkit/query' {
/**
* Provides access to the api injector.
*/
injector: Injector;
getInjector: () => Injector;
} & HooksWithUniqueNames<Definitions>;
}
}

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
*/
Expand All @@ -90,12 +86,16 @@ export interface AngularHooksModuleOptions {
/**
* The version of the `useSelector` hook to be used
*/
useSelector: <K>(mapFn: (state: any) => K, options?: SelectSignalOptions<K>) => Signal<K>;
useSelector: <K>(mapFn: (state: any) => K, options?: { equal?: ValueEqualityFn<K> }) => Signal<K>;
};
/**
* A selector creator (usually from `reselect`, or matching the same signature)
*/
createSelector?: typeof _createSelector;
createSelector: <T = any, V = any>(...input: any[]) => Selector<T, V>;
/**
* The injector to be used
*/
getInjector: () => Injector;
}

/**
Expand All @@ -105,53 +105,48 @@ export interface AngularHooksModuleOptions {
* ```ts
* const customCreateApi = buildCreateApi(
* coreModule(),
* angularHooksModule({
* hooks: {
* dispatch: createDispatchHook(MyContext),
* getState: createSelectorHook(MyContext),
* useSelector: createStoreHook(MyContext)
* }
* })
* angularHooksModule(() => myCreateAngularHooksModule())
* );
* ```
*
* @returns A module for use with `buildCreateApi`
*/
export const angularHooksModule = ({
hooks,
createSelector = _createSelector,
}: AngularHooksModuleOptions = {}): Module<AngularHooksModule> => {
createSelector,
getInjector,
}: AngularHooksModuleOptions): Module<AngularHooksModule> => {
return {
name: angularHooksModuleName,
init(api, { serializeQueryArgs }, context) {
const anyApi = api as any as Api<any, Record<string, any>, 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,
useLazyQuery,
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;
}
},
Expand Down
44 changes: 40 additions & 4 deletions projects/ngrx-rtk-query/src/lib/provide-store-api.ts
Original file line number Diff line number Diff line change
@@ -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<any, Record<string, any>, 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<string, any> = store.selectSignal((state) => state)();
return storeState?.[reducerPath]
? storeState
: // Query inside forFeature (Code splitting)
{ [reducerPath]: storeState };
};
const useSelector = <K>(mapFn: (state: any) => K, options?: SelectSignalOptions<K>): Signal<K> =>
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<typeof setupListenersFn>[1] | false;
Expand All @@ -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),
Expand Down
11 changes: 0 additions & 11 deletions projects/ngrx-rtk-query/src/lib/types/hooks-types.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,7 +10,6 @@ import type {
QueryActionCreatorResult,
QueryArgFrom,
QueryDefinition,
QueryResultSelectorResult,
QueryStatus,
QuerySubState,
ResultTypeFrom,
Expand All @@ -30,21 +28,12 @@ export interface QueryHooks<Definition extends QueryDefinition<any, any, any, an
useQuerySubscription: UseQuerySubscription<Definition>;
useLazyQuerySubscription: UseLazyQuerySubscription<Definition>;
useQueryState: UseQueryState<Definition>;
selector: QuerySelector<Definition>;
}

export interface MutationHooks<Definition extends MutationDefinition<any, any, any, any, any>> {
useMutation: UseMutation<Definition>;
selector: MutationSelector<Definition>;
}

export type QuerySelector<Definition extends QueryDefinition<any, any, any, any>> = (
queryArg: QueryArgFrom<Definition> | SkipToken,
) => MemoizedSelector<Record<string, any>, QueryResultSelectorResult<Definition>>;
export type MutationSelector<Definition extends MutationDefinition<any, any, any, any>> = (
requestId: string | { requestId: string | undefined; fixedCacheKey: string | undefined } | SkipToken,
) => MemoizedSelector<Record<string, any>, MutationResultSelectorResult<Definition>>;

/**
* 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
Expand Down
Loading

0 comments on commit 65076d5

Please sign in to comment.