Skip to content

Commit 94fc5a3

Browse files
authored
Merge pull request #2114 from aryaemami59/withTypes
Introduce pre-typed hooks via `hook.withTypes<RootState>()` method
2 parents 4b63c88 + 349e0f0 commit 94fc5a3

11 files changed

+379
-91
lines changed

Diff for: docs/tutorials/typescript.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ While it's possible to import the `RootState` and `AppDispatch` types into each
7979
Since these are actual variables, not types, it's important to define them in a separate file such as `app/hooks.ts`, not the store setup file. This allows you to import them into any component file that needs to use the hooks, and avoids potential circular import dependency issues.
8080
8181
```ts title="app/hooks.ts"
82-
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
82+
import { useDispatch, useSelector } from 'react-redux'
8383
import type { RootState, AppDispatch } from './store'
8484

8585
// highlight-start
8686
// Use throughout your app instead of plain `useDispatch` and `useSelector`
87-
export const useAppDispatch: () => AppDispatch = useDispatch
88-
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
87+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
88+
export const useAppSelector = useSelector.withTypes<RootState>()
8989
// highlight-end
9090
```
9191

Diff for: docs/using-react-redux/usage-with-typescript.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ While it's possible to import the `RootState` and `AppDispatch` types into each
6464
Since these are actual variables, not types, it's important to define them in a separate file such as `app/hooks.ts`, not the store setup file. This allows you to import them into any component file that needs to use the hooks, and avoids potential circular import dependency issues.
6565
6666
```ts title="app/hooks.ts"
67-
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
67+
import { useDispatch, useSelector } from 'react-redux'
6868
import type { RootState, AppDispatch } from './store'
6969

7070
// highlight-start
7171
// Use throughout your app instead of plain `useDispatch` and `useSelector`
72-
export const useAppDispatch: () => AppDispatch = useDispatch
73-
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
72+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
73+
export const useAppSelector = useSelector.withTypes<RootState>()
7474
// highlight-end
7575
```
7676

Diff for: src/exports.ts

+30-51
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,51 @@
1-
import Provider from './components/Provider'
2-
import type { ProviderProps } from './components/Provider'
31
import connect from './components/connect'
4-
import type {
2+
export type {
53
Connect,
64
ConnectProps,
75
ConnectedProps,
86
} from './components/connect'
9-
import type {
10-
SelectorFactory,
11-
Selector,
12-
MapStateToProps,
13-
MapStateToPropsFactory,
14-
MapStateToPropsParam,
15-
MapDispatchToPropsFunction,
16-
MapDispatchToProps,
17-
MapDispatchToPropsFactory,
18-
MapDispatchToPropsParam,
19-
MapDispatchToPropsNonObject,
20-
MergeProps,
21-
} from './connect/selectorFactory'
22-
import { ReactReduxContext } from './components/Context'
23-
import type { ReactReduxContextValue } from './components/Context'
24-
25-
import { useDispatch, createDispatchHook } from './hooks/useDispatch'
26-
import { useSelector, createSelectorHook } from './hooks/useSelector'
27-
import { useStore, createStoreHook } from './hooks/useStore'
287

298
import shallowEqual from './utils/shallowEqual'
30-
import type { Subscription } from './utils/Subscription'
9+
10+
import Provider from './components/Provider'
3111
import { defaultNoopBatch } from './utils/batch'
3212

33-
export * from './types'
13+
export { ReactReduxContext } from './components/Context'
14+
export type { ReactReduxContextValue } from './components/Context'
15+
16+
export type { ProviderProps } from './components/Provider'
17+
3418
export type {
35-
ProviderProps,
36-
SelectorFactory,
37-
Selector,
38-
MapStateToProps,
39-
MapStateToPropsFactory,
40-
MapStateToPropsParam,
41-
Connect,
42-
ConnectProps,
43-
ConnectedProps,
44-
MapDispatchToPropsFunction,
4519
MapDispatchToProps,
4620
MapDispatchToPropsFactory,
47-
MapDispatchToPropsParam,
21+
MapDispatchToPropsFunction,
4822
MapDispatchToPropsNonObject,
23+
MapDispatchToPropsParam,
24+
MapStateToProps,
25+
MapStateToPropsFactory,
26+
MapStateToPropsParam,
4927
MergeProps,
50-
ReactReduxContextValue,
51-
Subscription,
52-
}
28+
Selector,
29+
SelectorFactory,
30+
} from './connect/selectorFactory'
31+
32+
export { createDispatchHook, useDispatch } from './hooks/useDispatch'
33+
export type { UseDispatch } from './hooks/useDispatch'
34+
35+
export { createSelectorHook, useSelector } from './hooks/useSelector'
36+
export type { UseSelector } from './hooks/useSelector'
37+
38+
export { createStoreHook, useStore } from './hooks/useStore'
39+
export type { UseStore } from './hooks/useStore'
40+
41+
export type { Subscription } from './utils/Subscription'
42+
43+
export * from './types'
5344

5445
/**
5546
* @deprecated As of React 18, batching is enabled by default for ReactDOM and React Native.
5647
* This is now a no-op that immediately runs the callback.
5748
*/
5849
const batch = defaultNoopBatch
5950

60-
export {
61-
Provider,
62-
ReactReduxContext,
63-
connect,
64-
useDispatch,
65-
createDispatchHook,
66-
useSelector,
67-
createSelectorHook,
68-
useStore,
69-
createStoreHook,
70-
shallowEqual,
71-
batch,
72-
}
51+
export { Provider, batch, connect, shallowEqual }

Diff for: src/hooks/useDispatch.ts

+61-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,53 @@
1-
import type { Action, Dispatch, UnknownAction } from 'redux'
21
import type { Context } from 'react'
2+
import type { Action, Dispatch, UnknownAction } from 'redux'
33

44
import type { ReactReduxContextValue } from '../components/Context'
55
import { ReactReduxContext } from '../components/Context'
6-
import { useStore as useDefaultStore, createStoreHook } from './useStore'
6+
import { createStoreHook, useStore as useDefaultStore } from './useStore'
7+
8+
/**
9+
* Represents a custom hook that provides a dispatch function
10+
* from the Redux store.
11+
*
12+
* @template DispatchType - The specific type of the dispatch function.
13+
*
14+
* @since 9.1.0
15+
* @public
16+
*/
17+
export interface UseDispatch<
18+
DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>
19+
> {
20+
/**
21+
* Returns the dispatch function from the Redux store.
22+
*
23+
* @returns The dispatch function from the Redux store.
24+
*
25+
* @template AppDispatch - The specific type of the dispatch function.
26+
*/
27+
<AppDispatch extends DispatchType = DispatchType>(): AppDispatch
28+
29+
/**
30+
* Creates a "pre-typed" version of {@linkcode useDispatch useDispatch}
31+
* where the type of the `dispatch` function is predefined.
32+
*
33+
* This allows you to set the `dispatch` type once, eliminating the need to
34+
* specify it with every {@linkcode useDispatch useDispatch} call.
35+
*
36+
* @returns A pre-typed `useDispatch` with the dispatch type already defined.
37+
*
38+
* @example
39+
* ```ts
40+
* export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
41+
* ```
42+
*
43+
* @template OverrideDispatchType - The specific type of the dispatch function.
44+
*
45+
* @since 9.1.0
46+
*/
47+
withTypes: <
48+
OverrideDispatchType extends DispatchType
49+
>() => UseDispatch<OverrideDispatchType>
50+
}
751

852
/**
953
* Hook factory, which creates a `useDispatch` hook bound to a given context.
@@ -12,21 +56,28 @@ import { useStore as useDefaultStore, createStoreHook } from './useStore'
1256
* @returns {Function} A `useDispatch` hook bound to the specified context.
1357
*/
1458
export function createDispatchHook<
15-
S = unknown,
16-
A extends Action<string> = UnknownAction
59+
StateType = unknown,
60+
ActionType extends Action = UnknownAction
61+
>(
1762
// @ts-ignore
18-
>(context?: Context<ReactReduxContextValue<S, A> | null> = ReactReduxContext) {
63+
context?: Context<ReactReduxContextValue<
64+
StateType,
65+
ActionType
66+
> | null> = ReactReduxContext
67+
) {
1968
const useStore =
20-
// @ts-ignore
2169
context === ReactReduxContext ? useDefaultStore : createStoreHook(context)
2270

23-
return function useDispatch<
24-
AppDispatch extends Dispatch<A> = Dispatch<A>
25-
>(): AppDispatch {
71+
const useDispatch = () => {
2672
const store = useStore()
27-
// @ts-ignore
2873
return store.dispatch
2974
}
75+
76+
Object.assign(useDispatch, {
77+
withTypes: () => useDispatch,
78+
})
79+
80+
return useDispatch as UseDispatch<Dispatch<ActionType>>
3081
}
3182

3283
/**

Diff for: src/hooks/useSelector.ts

+57-10
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,56 @@ export interface UseSelectorOptions<Selected = unknown> {
6666
devModeChecks?: Partial<DevModeChecks>
6767
}
6868

69-
export interface UseSelector {
70-
<TState = unknown, Selected = unknown>(
71-
selector: (state: TState) => Selected,
72-
equalityFn?: EqualityFn<Selected>,
73-
): Selected
74-
<TState = unknown, Selected = unknown>(
69+
/**
70+
* Represents a custom hook that allows you to extract data from the
71+
* Redux store state, using a selector function. The selector function
72+
* takes the current state as an argument and returns a part of the state
73+
* or some derived data. The hook also supports an optional equality
74+
* function or options object to customize its behavior.
75+
*
76+
* @template StateType - The specific type of state this hook operates on.
77+
*
78+
* @public
79+
*/
80+
export interface UseSelector<StateType = unknown> {
81+
/**
82+
* A function that takes a selector function as its first argument.
83+
* The selector function is responsible for selecting a part of
84+
* the Redux store's state or computing derived data.
85+
*
86+
* @param selector - A function that receives the current state and returns a part of the state or some derived data.
87+
* @param equalityFnOrOptions - An optional equality function or options object for customizing the behavior of the selector.
88+
* @returns The selected part of the state or derived data.
89+
*
90+
* @template TState - The specific type of state this hook operates on.
91+
* @template Selected - The type of the value that the selector function will return.
92+
*/
93+
<TState extends StateType = StateType, Selected = unknown>(
7594
selector: (state: TState) => Selected,
76-
options?: UseSelectorOptions<Selected>,
95+
equalityFnOrOptions?: EqualityFn<Selected> | UseSelectorOptions<Selected>
7796
): Selected
97+
98+
/**
99+
* Creates a "pre-typed" version of {@linkcode useSelector useSelector}
100+
* where the `state` type is predefined.
101+
*
102+
* This allows you to set the `state` type once, eliminating the need to
103+
* specify it with every {@linkcode useSelector useSelector} call.
104+
*
105+
* @returns A pre-typed `useSelector` with the state type already defined.
106+
*
107+
* @example
108+
* ```ts
109+
* export const useAppSelector = useSelector.withTypes<RootState>()
110+
* ```
111+
*
112+
* @template OverrideStateType - The specific type of state this hook operates on.
113+
*
114+
* @since 9.1.0
115+
*/
116+
withTypes: <
117+
OverrideStateType extends StateType
118+
>() => UseSelector<OverrideStateType>
78119
}
79120

80121
let useSyncExternalStoreWithSelector = notInitialized as uSESWS
@@ -101,12 +142,12 @@ export function createSelectorHook(
101142
? useDefaultReduxContext
102143
: createReduxContextHook(context)
103144

104-
return function useSelector<TState, Selected>(
145+
const useSelector = <TState, Selected extends unknown>(
105146
selector: (state: TState) => Selected,
106147
equalityFnOrOptions:
107148
| EqualityFn<NoInfer<Selected>>
108-
| UseSelectorOptions<NoInfer<Selected>> = {},
109-
): Selected {
149+
| UseSelectorOptions<NoInfer<Selected>> = {}
150+
): Selected => {
110151
const { equalityFn = refEquality, devModeChecks = {} } =
111152
typeof equalityFnOrOptions === 'function'
112153
? { equalityFn: equalityFnOrOptions }
@@ -217,6 +258,12 @@ export function createSelectorHook(
217258

218259
return selectedState
219260
}
261+
262+
Object.assign(useSelector, {
263+
withTypes: () => useSelector,
264+
})
265+
266+
return useSelector as UseSelector
220267
}
221268

222269
/**

0 commit comments

Comments
 (0)