Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce pre-typed hooks via hook.withTypes<RootState>() method #2114

Merged
merged 14 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>()
aryaemami59 marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading