Skip to content

Commit 0a087f6

Browse files
committed
feat: add support for withTypes
1 parent c206cf7 commit 0a087f6

File tree

3 files changed

+313
-45
lines changed

3 files changed

+313
-45
lines changed
Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,91 @@
1-
import type { Dispatch, UnknownAction } from 'redux'
1+
import type { Dispatch, UnknownAction, Action } from 'redux'
22
import { assertInInjectionContext } from '@angular/core'
33
import { injectStore } from './inject-store'
44

5-
// TODO: Add `withTypes` support
6-
export function injectDispatch<AppDispatch extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>(): AppDispatch {
7-
assertInInjectionContext(injectDispatch)
8-
const store = injectStore();
9-
return store.dispatch as AppDispatch
5+
6+
/**
7+
* Represents a custom injection that provides a dispatch function
8+
* from the Redux store.
9+
*
10+
* @template DispatchType - The specific type of the dispatch function.
11+
*
12+
* @public
13+
*/
14+
export interface InjectDispatch<
15+
DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>,
16+
> {
17+
/**
18+
* Returns the dispatch function from the Redux store.
19+
*
20+
* @returns The dispatch function from the Redux store.
21+
*
22+
* @template AppDispatch - The specific type of the dispatch function.
23+
*/
24+
<AppDispatch extends DispatchType = DispatchType>(): AppDispatch
25+
26+
/**
27+
* Creates a "pre-typed" version of {@linkcode injectDispatch injectDispatch}
28+
* where the type of the `dispatch` function is predefined.
29+
*
30+
* This allows you to set the `dispatch` type once, eliminating the need to
31+
* specify it with every {@linkcode injectDispatch injectDispatch} call.
32+
*
33+
* @returns A pre-typed `injectDispatch` with the dispatch type already defined.
34+
*
35+
* @example
36+
* ```ts
37+
* export const injectAppDispatch = injectDispatch.withTypes<AppDispatch>()
38+
* ```
39+
*
40+
* @template OverrideDispatchType - The specific type of the dispatch function.
41+
*/
42+
withTypes: <
43+
OverrideDispatchType extends DispatchType,
44+
>() => InjectDispatch<OverrideDispatchType>
1045
}
46+
47+
/**
48+
* Injection factory, which creates a `injectDispatch` injection bound to a given context.
49+
*
50+
* @returns {Function} A `injectDispatch` injection bound to the specified context.
51+
*/
52+
export function createDispatchInjection<
53+
ActionType extends Action = UnknownAction,
54+
>() {
55+
const injectDispatch = <AppDispatch extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>(): AppDispatch => {
56+
assertInInjectionContext(injectDispatch)
57+
const store = injectStore();
58+
return store.dispatch as AppDispatch
59+
}
60+
61+
Object.assign(injectDispatch, {
62+
withTypes: () => injectDispatch,
63+
})
64+
65+
return injectDispatch as InjectDispatch<Dispatch<ActionType>>
66+
}
67+
68+
/**
69+
* A injection to access the redux `dispatch` function.
70+
*
71+
* @returns {any|function} redux store's `dispatch` function
72+
*
73+
* @example
74+
*
75+
* import { injectDispatch } from 'angular-redux'
76+
*
77+
* @Component({
78+
* selector: 'example-component',
79+
* template: `
80+
* <div>
81+
* <span>{{value}}</span>
82+
* <button (click)="increaseCounter()">Increase counter</button>
83+
* </div>
84+
* `
85+
* })
86+
* export class CounterComponent {
87+
* dispatch = injectDispatch()
88+
* increaseCounter = () => dispatch({ type: 'increase-counter' })
89+
* }
90+
*/
91+
export const injectDispatch = /* #__PURE__*/ createDispatchInjection()

projects/angular-redux/src/lib/inject-selector.ts

Lines changed: 124 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,137 @@ import { EqualityFn } from './types'
22
import { assertInInjectionContext, effect, inject, Signal, signal } from '@angular/core'
33
import { ReduxProvider } from './provider'
44

5-
export interface UseSelectorOptions<Selected = unknown> {
5+
export interface InjectSelectorOptions<Selected = unknown> {
66
equalityFn?: EqualityFn<Selected>
77
}
88

99
const refEquality: EqualityFn<any> = (a, b) => a === b
1010

11-
// TODO: Add support for `withTypes`
12-
export function injectSelector<TState = unknown, Selected = unknown>(
13-
selector: (state: TState) => Selected,
14-
equalityFnOrOptions: EqualityFn<Selected> | UseSelectorOptions<Selected> = {},
15-
): Signal<Selected> {
16-
assertInInjectionContext(injectSelector)
17-
const reduxContext = inject(ReduxProvider);
18-
19-
const { equalityFn = refEquality } =
20-
typeof equalityFnOrOptions === 'function'
21-
? { equalityFn: equalityFnOrOptions }
22-
: equalityFnOrOptions
23-
24-
const {
25-
store,
26-
subscription
27-
} = reduxContext
28-
29-
const selectedState = signal(selector(store.getState()))
30-
31-
effect((onCleanup) => {
32-
const unsubscribe = subscription.addNestedSub(() => {
33-
const data = selector(store.getState());
34-
if (equalityFn(selectedState(), data)) {
35-
return
36-
}
37-
38-
selectedState.set(data);
39-
})
11+
/**
12+
* Represents a custom injection that allows you to extract data from the
13+
* Redux store state, using a selector function. The selector function
14+
* takes the current state as an argument and returns a part of the state
15+
* or some derived data. The injection also supports an optional equality
16+
* function or options object to customize its behavior.
17+
*
18+
* @template StateType - The specific type of state this injection operates on.
19+
*
20+
* @public
21+
*/
22+
export interface InjectSelector<StateType = unknown> {
23+
/**
24+
* A function that takes a selector function as its first argument.
25+
* The selector function is responsible for selecting a part of
26+
* the Redux store's state or computing derived data.
27+
*
28+
* @param selector - A function that receives the current state and returns a part of the state or some derived data.
29+
* @param equalityFnOrOptions - An optional equality function or options object for customizing the behavior of the selector.
30+
* @returns The selected part of the state or derived data.
31+
*
32+
* @template TState - The specific type of state this injection operates on.
33+
* @template Selected - The type of the value that the selector function will return.
34+
*/
35+
<TState extends StateType = StateType, Selected = unknown>(
36+
selector: (state: TState) => Selected,
37+
equalityFnOrOptions?: EqualityFn<Selected> | InjectSelectorOptions<Selected>,
38+
): Signal<Selected>
39+
40+
/**
41+
* Creates a "pre-typed" version of {@linkcode injectSelector injectSelector}
42+
* where the `state` type is predefined.
43+
*
44+
* This allows you to set the `state` type once, eliminating the need to
45+
* specify it with every {@linkcode injectSelector injectSelector} call.
46+
*
47+
* @returns A pre-typed `injectSelector` with the state type already defined.
48+
*
49+
* @example
50+
* ```ts
51+
* export const injectAppSelector = injectSelector.withTypes<RootState>()
52+
* ```
53+
*
54+
* @template OverrideStateType - The specific type of state this injection operates on.
55+
*/
56+
withTypes: <
57+
OverrideStateType extends StateType,
58+
>() => InjectSelector<OverrideStateType>
59+
}
60+
61+
/**
62+
* Injection factory, which creates a `injectSelector` injection bound to a given context.
63+
*
64+
* @returns {Function} A `injectSelector` injection bound to the specified context.
65+
*/
66+
export function createSelectorInjection(): InjectSelector {
67+
const injectSelector =
68+
<TState, Selected>(
69+
selector: (state: TState) => Selected,
70+
equalityFnOrOptions: EqualityFn<Selected> | InjectSelectorOptions<Selected> = {},
71+
): Signal<Selected> =>
72+
{
73+
assertInInjectionContext(injectSelector)
74+
const reduxContext = inject(ReduxProvider);
75+
76+
const { equalityFn = refEquality } =
77+
typeof equalityFnOrOptions === 'function'
78+
? { equalityFn: equalityFnOrOptions }
79+
: equalityFnOrOptions
80+
81+
const {
82+
store,
83+
subscription
84+
} = reduxContext
4085

41-
onCleanup(() => {
42-
unsubscribe()
86+
const selectedState = signal(selector(store.getState()))
87+
88+
effect((onCleanup) => {
89+
const unsubscribe = subscription.addNestedSub(() => {
90+
const data = selector(store.getState());
91+
if (equalityFn(selectedState(), data)) {
92+
return
93+
}
94+
95+
selectedState.set(data);
96+
})
97+
98+
onCleanup(() => {
99+
unsubscribe()
100+
})
43101
})
102+
103+
return selectedState
104+
}
105+
106+
Object.assign(injectSelector, {
107+
withTypes: () => injectSelector,
44108
})
45109

46-
return selectedState
110+
return injectSelector as InjectSelector
47111
}
112+
113+
/**
114+
* A injection to access the redux store's state. This injection takes a selector function
115+
* as an argument. The selector is called with the store state.
116+
*
117+
* This injection takes an optional equality comparison function as the second parameter
118+
* that allows you to customize the way the selected state is compared to determine
119+
* whether the component needs to be re-rendered.
120+
*
121+
* @param {Function} selector the selector function
122+
* @param {Function=} equalityFn the function that will be used to determine equality
123+
*
124+
* @returns {any} the selected state
125+
*
126+
* @example
127+
*
128+
* import { injectSelector } from 'angular-redux'
129+
*
130+
* @Component({
131+
* selector: 'counter-component',
132+
* template: `<div>{{counter}}</div>`
133+
* })
134+
* export class CounterComponent {
135+
* counter = injectSelector(state => state.counter)
136+
* }
137+
*/
138+
export const injectSelector = /* #__PURE__*/ createSelectorInjection()
Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,106 @@
11
import { assertInInjectionContext, inject } from '@angular/core'
22
import { ReduxProvider } from './provider'
3-
import type { Store, Action, UnknownAction } from 'redux'
3+
import type { Store, Action } from 'redux'
44

5-
export function injectStore<A extends Action<string> = UnknownAction, S = unknown>(): Store<S, A> {
6-
assertInInjectionContext(injectStore)
7-
const context = inject(ReduxProvider)
8-
const { store } = context
9-
return store
5+
/**
6+
* Represents a type that extracts the action type from a given Redux store.
7+
*
8+
* @template StoreType - The specific type of the Redux store.
9+
*
10+
* @internal
11+
*/
12+
export type ExtractStoreActionType<StoreType extends Store> =
13+
StoreType extends Store<any, infer ActionType> ? ActionType : never
14+
15+
/**
16+
* Represents a custom injection that provides access to the Redux store.
17+
*
18+
* @template StoreType - The specific type of the Redux store that gets returned.
19+
*
20+
* @public
21+
*/
22+
export interface InjectStore<StoreType extends Store> {
23+
/**
24+
* Returns the Redux store instance.
25+
*
26+
* @returns The Redux store instance.
27+
*/
28+
(): StoreType
29+
30+
/**
31+
* Returns the Redux store instance with specific state and action types.
32+
*
33+
* @returns The Redux store with the specified state and action types.
34+
*
35+
* @template StateType - The specific type of the state used in the store.
36+
* @template ActionType - The specific type of the actions used in the store.
37+
*/
38+
<
39+
StateType extends ReturnType<StoreType['getState']> = ReturnType<
40+
StoreType['getState']
41+
>,
42+
ActionType extends Action = ExtractStoreActionType<Store>,
43+
>(): Store<StateType, ActionType>
44+
45+
/**
46+
* Creates a "pre-typed" version of {@linkcode injectStore injectStore}
47+
* where the type of the Redux `store` is predefined.
48+
*
49+
* This allows you to set the `store` type once, eliminating the need to
50+
* specify it with every {@linkcode injectStore injectStore} call.
51+
*
52+
* @returns A pre-typed `injectStore` with the store type already defined.
53+
*
54+
* @example
55+
* ```ts
56+
* export const useAppStore = injectStore.withTypes<AppStore>()
57+
* ```
58+
*
59+
* @template OverrideStoreType - The specific type of the Redux store that gets returned.
60+
*/
61+
withTypes: <
62+
OverrideStoreType extends StoreType,
63+
>() => InjectStore<OverrideStoreType>
64+
}
65+
66+
/**
67+
* Injection factory, which creates a `injectStore` injection bound to a given context.
68+
*
69+
* @returns {Function} A `injectStore` injection bound to the specified context.
70+
*/
71+
export function createStoreInjection<
72+
StateType = unknown,
73+
ActionType extends Action = Action,
74+
>() {
75+
const injectStore = () => {
76+
assertInInjectionContext(injectStore)
77+
const context = inject(ReduxProvider)
78+
const { store } = context
79+
return store
80+
}
81+
82+
Object.assign(injectStore, {
83+
withTypes: () => injectStore,
84+
})
85+
86+
return injectStore as InjectStore<Store<StateType, ActionType>>
1087
}
88+
89+
/**
90+
* A injection to access the redux store.
91+
*
92+
* @returns {any} the redux store
93+
*
94+
* @example
95+
*
96+
* import { injectStore } from 'angular-redux'
97+
*
98+
* @Component({
99+
* selector: 'example-component',
100+
* template: `<div>{{store.getState()}}</div>`
101+
* })
102+
* export class CounterComponent {
103+
* store = injectStore()
104+
* }
105+
*/
106+
export const injectStore = /* #__PURE__*/ createStoreInjection()

0 commit comments

Comments
 (0)