Skip to content

Commit

Permalink
Merge pull request #2114 from aryaemami59/withTypes
Browse files Browse the repository at this point in the history
Introduce pre-typed hooks via `hook.withTypes<RootState>()` method
  • Loading branch information
EskiMojo14 authored Jan 12, 2024
2 parents 4b63c88 + 349e0f0 commit 94fc5a3
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 91 deletions.
6 changes: 3 additions & 3 deletions docs/tutorials/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ While it's possible to import the `RootState` and `AppDispatch` types into each
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.
```ts title="app/hooks.ts"
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// highlight-start
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
// highlight-end
```

Expand Down
6 changes: 3 additions & 3 deletions docs/using-react-redux/usage-with-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ While it's possible to import the `RootState` and `AppDispatch` types into each
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.
```ts title="app/hooks.ts"
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

// highlight-start
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
// highlight-end
```

Expand Down
81 changes: 30 additions & 51 deletions src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,51 @@
import Provider from './components/Provider'
import type { ProviderProps } from './components/Provider'
import connect from './components/connect'
import type {
export type {
Connect,
ConnectProps,
ConnectedProps,
} from './components/connect'
import type {
SelectorFactory,
Selector,
MapStateToProps,
MapStateToPropsFactory,
MapStateToPropsParam,
MapDispatchToPropsFunction,
MapDispatchToProps,
MapDispatchToPropsFactory,
MapDispatchToPropsParam,
MapDispatchToPropsNonObject,
MergeProps,
} from './connect/selectorFactory'
import { ReactReduxContext } from './components/Context'
import type { ReactReduxContextValue } from './components/Context'

import { useDispatch, createDispatchHook } from './hooks/useDispatch'
import { useSelector, createSelectorHook } from './hooks/useSelector'
import { useStore, createStoreHook } from './hooks/useStore'

import shallowEqual from './utils/shallowEqual'
import type { Subscription } from './utils/Subscription'

import Provider from './components/Provider'
import { defaultNoopBatch } from './utils/batch'

export * from './types'
export { ReactReduxContext } from './components/Context'
export type { ReactReduxContextValue } from './components/Context'

export type { ProviderProps } from './components/Provider'

export type {
ProviderProps,
SelectorFactory,
Selector,
MapStateToProps,
MapStateToPropsFactory,
MapStateToPropsParam,
Connect,
ConnectProps,
ConnectedProps,
MapDispatchToPropsFunction,
MapDispatchToProps,
MapDispatchToPropsFactory,
MapDispatchToPropsParam,
MapDispatchToPropsFunction,
MapDispatchToPropsNonObject,
MapDispatchToPropsParam,
MapStateToProps,
MapStateToPropsFactory,
MapStateToPropsParam,
MergeProps,
ReactReduxContextValue,
Subscription,
}
Selector,
SelectorFactory,
} from './connect/selectorFactory'

export { createDispatchHook, useDispatch } from './hooks/useDispatch'
export type { UseDispatch } from './hooks/useDispatch'

export { createSelectorHook, useSelector } from './hooks/useSelector'
export type { UseSelector } from './hooks/useSelector'

export { createStoreHook, useStore } from './hooks/useStore'
export type { UseStore } from './hooks/useStore'

export type { Subscription } from './utils/Subscription'

export * from './types'

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

export {
Provider,
ReactReduxContext,
connect,
useDispatch,
createDispatchHook,
useSelector,
createSelectorHook,
useStore,
createStoreHook,
shallowEqual,
batch,
}
export { Provider, batch, connect, shallowEqual }
71 changes: 61 additions & 10 deletions src/hooks/useDispatch.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,53 @@
import type { Action, Dispatch, UnknownAction } from 'redux'
import type { Context } from 'react'
import type { Action, Dispatch, UnknownAction } from 'redux'

import type { ReactReduxContextValue } from '../components/Context'
import { ReactReduxContext } from '../components/Context'
import { useStore as useDefaultStore, createStoreHook } from './useStore'
import { createStoreHook, useStore as useDefaultStore } from './useStore'

/**
* Represents a custom hook that provides a dispatch function
* from the Redux store.
*
* @template DispatchType - The specific type of the dispatch function.
*
* @since 9.1.0
* @public
*/
export interface UseDispatch<
DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>
> {
/**
* Returns the dispatch function from the Redux store.
*
* @returns The dispatch function from the Redux store.
*
* @template AppDispatch - The specific type of the dispatch function.
*/
<AppDispatch extends DispatchType = DispatchType>(): AppDispatch

/**
* Creates a "pre-typed" version of {@linkcode useDispatch useDispatch}
* where the type of the `dispatch` function is predefined.
*
* This allows you to set the `dispatch` type once, eliminating the need to
* specify it with every {@linkcode useDispatch useDispatch} call.
*
* @returns A pre-typed `useDispatch` with the dispatch type already defined.
*
* @example
* ```ts
* export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
* ```
*
* @template OverrideDispatchType - The specific type of the dispatch function.
*
* @since 9.1.0
*/
withTypes: <
OverrideDispatchType extends DispatchType
>() => UseDispatch<OverrideDispatchType>
}

/**
* Hook factory, which creates a `useDispatch` hook bound to a given context.
Expand All @@ -12,21 +56,28 @@ import { useStore as useDefaultStore, createStoreHook } from './useStore'
* @returns {Function} A `useDispatch` hook bound to the specified context.
*/
export function createDispatchHook<
S = unknown,
A extends Action<string> = UnknownAction
StateType = unknown,
ActionType extends Action = UnknownAction
>(
// @ts-ignore
>(context?: Context<ReactReduxContextValue<S, A> | null> = ReactReduxContext) {
context?: Context<ReactReduxContextValue<
StateType,
ActionType
> | null> = ReactReduxContext
) {
const useStore =
// @ts-ignore
context === ReactReduxContext ? useDefaultStore : createStoreHook(context)

return function useDispatch<
AppDispatch extends Dispatch<A> = Dispatch<A>
>(): AppDispatch {
const useDispatch = () => {
const store = useStore()
// @ts-ignore
return store.dispatch
}

Object.assign(useDispatch, {
withTypes: () => useDispatch,
})

return useDispatch as UseDispatch<Dispatch<ActionType>>
}

/**
Expand Down
67 changes: 57 additions & 10 deletions src/hooks/useSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,56 @@ export interface UseSelectorOptions<Selected = unknown> {
devModeChecks?: Partial<DevModeChecks>
}

export interface UseSelector {
<TState = unknown, Selected = unknown>(
selector: (state: TState) => Selected,
equalityFn?: EqualityFn<Selected>,
): Selected
<TState = unknown, Selected = unknown>(
/**
* Represents a custom hook that allows you to extract data from the
* Redux store state, using a selector function. The selector function
* takes the current state as an argument and returns a part of the state
* or some derived data. The hook also supports an optional equality
* function or options object to customize its behavior.
*
* @template StateType - The specific type of state this hook operates on.
*
* @public
*/
export interface UseSelector<StateType = unknown> {
/**
* A function that takes a selector function as its first argument.
* The selector function is responsible for selecting a part of
* the Redux store's state or computing derived data.
*
* @param selector - A function that receives the current state and returns a part of the state or some derived data.
* @param equalityFnOrOptions - An optional equality function or options object for customizing the behavior of the selector.
* @returns The selected part of the state or derived data.
*
* @template TState - The specific type of state this hook operates on.
* @template Selected - The type of the value that the selector function will return.
*/
<TState extends StateType = StateType, Selected = unknown>(
selector: (state: TState) => Selected,
options?: UseSelectorOptions<Selected>,
equalityFnOrOptions?: EqualityFn<Selected> | UseSelectorOptions<Selected>
): Selected

/**
* Creates a "pre-typed" version of {@linkcode useSelector useSelector}
* where the `state` type is predefined.
*
* This allows you to set the `state` type once, eliminating the need to
* specify it with every {@linkcode useSelector useSelector} call.
*
* @returns A pre-typed `useSelector` with the state type already defined.
*
* @example
* ```ts
* export const useAppSelector = useSelector.withTypes<RootState>()
* ```
*
* @template OverrideStateType - The specific type of state this hook operates on.
*
* @since 9.1.0
*/
withTypes: <
OverrideStateType extends StateType
>() => UseSelector<OverrideStateType>
}

let useSyncExternalStoreWithSelector = notInitialized as uSESWS
Expand All @@ -101,12 +142,12 @@ export function createSelectorHook(
? useDefaultReduxContext
: createReduxContextHook(context)

return function useSelector<TState, Selected>(
const useSelector = <TState, Selected extends unknown>(
selector: (state: TState) => Selected,
equalityFnOrOptions:
| EqualityFn<NoInfer<Selected>>
| UseSelectorOptions<NoInfer<Selected>> = {},
): Selected {
| UseSelectorOptions<NoInfer<Selected>> = {}
): Selected => {
const { equalityFn = refEquality, devModeChecks = {} } =
typeof equalityFnOrOptions === 'function'
? { equalityFn: equalityFnOrOptions }
Expand Down Expand Up @@ -217,6 +258,12 @@ export function createSelectorHook(

return selectedState
}

Object.assign(useSelector, {
withTypes: () => useSelector,
})

return useSelector as UseSelector
}

/**
Expand Down
Loading

0 comments on commit 94fc5a3

Please sign in to comment.