Skip to content

Commit fdadc6e

Browse files
committed
add react hooks for accessing redux store state and dispatching redux actions
1 parent d4b54b5 commit fdadc6e

12 files changed

+728
-3
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min",
3030
"clean": "rimraf lib dist es coverage",
3131
"format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"docs/**/*.md\"",
32-
"lint": "eslint src test/utils test/components",
32+
"lint": "eslint src test/utils test/components test/hooks",
3333
"prepare": "npm run clean && npm run build",
3434
"pretest": "npm run lint",
3535
"test": "jest",

src/alternate-renderers.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,26 @@ import connectAdvanced from './components/connectAdvanced'
33
import { ReactReduxContext } from './components/Context'
44
import connect from './connect/connect'
55

6+
import { useActions } from './hooks/useActions'
7+
import { useDispatch } from './hooks/useDispatch'
8+
import { useReduxContext } from './hooks/useReduxContext'
9+
import { useSelector } from './hooks/useSelector'
10+
import { useStore } from './hooks/useStore'
11+
612
import { getBatch } from './utils/batch'
713

814
// For other renderers besides ReactDOM and React Native, use the default noop batch function
915
const batch = getBatch()
1016

11-
export { Provider, connectAdvanced, ReactReduxContext, connect, batch }
17+
export {
18+
Provider,
19+
connectAdvanced,
20+
ReactReduxContext,
21+
connect,
22+
batch,
23+
useActions,
24+
useDispatch,
25+
useReduxContext,
26+
useSelector,
27+
useStore
28+
}

src/hooks/useActions.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { bindActionCreators } from 'redux'
2+
import invariant from 'invariant'
3+
import { useDispatch } from './useDispatch'
4+
import { useMemo } from 'react'
5+
6+
/**
7+
* A hook to bind action creators to the redux store's `dispatch` function
8+
* similar to how redux's `bindActionCreators` works.
9+
*
10+
* Supports passing a single action creator, an array/tuple of action
11+
* creators, or an object of action creators.
12+
*
13+
* Any arguments passed to the created callbacks are passed through to
14+
* the your functions.
15+
*
16+
* This hook takes a dependencies array as an optional second argument,
17+
* which when passed ensures referential stability of the created callbacks.
18+
*
19+
* @param {Function|Function[]|Object.<string, Function>} actions the action creators to bind
20+
* @param {any[]} deps (optional) dependencies array to control referential stability
21+
*
22+
* @returns {Function|Function[]|Object.<string, Function>} callback(s) bound to store's `dispatch` function
23+
*
24+
* Usage:
25+
*
26+
```jsx
27+
import React from 'react'
28+
import { useActions } from 'react-redux'
29+
30+
const increaseCounter = ({ amount }) => ({
31+
type: 'increase-counter',
32+
amount,
33+
})
34+
35+
export const CounterComponent = ({ value }) => {
36+
// supports passing an object of action creators
37+
const { increaseCounterByOne, increaseCounterByTwo } = useActions({
38+
increaseCounterByOne: () => increaseCounter(1),
39+
increaseCounterByTwo: () => increaseCounter(2),
40+
}, [])
41+
42+
// supports passing an array/tuple of action creators
43+
const [increaseCounterByThree, increaseCounterByFour] = useActions([
44+
() => increaseCounter(3),
45+
() => increaseCounter(4),
46+
], [])
47+
48+
// supports passing a single action creator
49+
const increaseCounterBy5 = useActions(() => increaseCounter(5), [])
50+
51+
// passes through any arguments to the callback
52+
const increaseCounterByX = useActions(x => increaseCounter(x), [])
53+
54+
return (
55+
<div>
56+
<span>{value}</span>
57+
<button onClick={increaseCounterByOne}>Increase counter by 1</button>
58+
</div>
59+
)
60+
}
61+
```
62+
*/
63+
export function useActions(actions, deps) {
64+
invariant(actions, `You must pass actions to useActions`)
65+
66+
const dispatch = useDispatch()
67+
return useMemo(() => {
68+
if (Array.isArray(actions)) {
69+
return actions.map(a => bindActionCreators(a, dispatch))
70+
}
71+
72+
return bindActionCreators(actions, dispatch)
73+
}, deps)
74+
}

src/hooks/useDispatch.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useStore } from './useStore'
2+
3+
/**
4+
* A hook to access the redux `dispatch` function. Note that in most cases where you
5+
* might want to use this hook it is recommended to use `useActions` instead to bind
6+
* action creators to the `dispatch` function.
7+
*
8+
* @returns {any} redux store's `dispatch` function
9+
*
10+
* Usage:
11+
*
12+
```jsx
13+
import React, { useCallback } from 'react'
14+
import { useReduxDispatch } from 'react-redux'
15+
16+
export const CounterComponent = ({ value }) => {
17+
const dispatch = useDispatch()
18+
const increaseCounter = useCallback(() => dispatch({ type: 'increase-counter' }), [])
19+
return (
20+
<div>
21+
<span>{value}</span>
22+
<button onClick={increaseCounter}>Increase counter</button>
23+
</div>
24+
)
25+
}
26+
```
27+
*/
28+
export function useDispatch() {
29+
const store = useStore()
30+
return store.dispatch
31+
}

src/hooks/useReduxContext.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useContext } from 'react'
2+
import invariant from 'invariant'
3+
import { ReactReduxContext } from '../components/Context'
4+
5+
/**
6+
* A hook to access the value of the `ReactReduxContext`. This is a low-level
7+
* hook that you should usually not need to call directly.
8+
*
9+
* @returns {any} the value of the `ReactReduxContext`
10+
*
11+
* Usage:
12+
*
13+
```jsx
14+
import React from 'react'
15+
import { useReduxContext } from 'react-redux'
16+
17+
export const CounterComponent = ({ value }) => {
18+
const { store } = useReduxContext()
19+
return <div>{store.getState()}</div>
20+
}
21+
```
22+
*/
23+
export function useReduxContext() {
24+
const contextValue = useContext(ReactReduxContext)
25+
26+
invariant(
27+
contextValue,
28+
'could not find react-redux context value; please ensure the component is wrapped in a <Provider>'
29+
)
30+
31+
return contextValue
32+
}

src/hooks/useSelector.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useReducer, useRef, useEffect, useMemo, useLayoutEffect } from 'react'
2+
import invariant from 'invariant'
3+
import { useReduxContext } from './useReduxContext'
4+
import shallowEqual from '../utils/shallowEqual'
5+
import Subscription from '../utils/Subscription'
6+
7+
// React currently throws a warning when using useLayoutEffect on the server.
8+
// To get around it, we can conditionally useEffect on the server (no-op) and
9+
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
10+
// subscription callback always has the selector from the latest render commit
11+
// available, otherwise a store update may happen between render and the effect,
12+
// which may cause missed updates; we also must ensure the store subscription
13+
// is created synchronously, otherwise a store update may occur before the
14+
// subscription is created and an inconsistent state may be observed
15+
const useIsomorphicLayoutEffect =
16+
typeof window !== 'undefined' ? useLayoutEffect : useEffect
17+
18+
/**
19+
* A hook to access the redux store's state. This hook takes a selector function
20+
* as an argument. The selector is called with the store state.
21+
*
22+
* @param {Function} selector the selector function
23+
*
24+
* @returns {any} the selected state
25+
*
26+
* Usage:
27+
*
28+
```jsx
29+
import React from 'react'
30+
import { useSelector } from 'react-redux'
31+
32+
export const CounterComponent = () => {
33+
const counter = useSelector(state => state.counter)
34+
return <div>{counter}</div>
35+
}
36+
```
37+
*/
38+
export function useSelector(selector) {
39+
invariant(selector, `You must pass a selector to useSelectors`)
40+
41+
const { store, subscription: contextSub } = useReduxContext()
42+
const [, forceRender] = useReducer(s => s + 1, 0)
43+
44+
const subscription = useMemo(() => new Subscription(store, contextSub), [
45+
store,
46+
contextSub
47+
])
48+
49+
const latestSelector = useRef(selector)
50+
const selectedState = selector(store.getState())
51+
const latestSelectedState = useRef(selectedState)
52+
53+
useIsomorphicLayoutEffect(() => {
54+
latestSelector.current = selector
55+
latestSelectedState.current = selectedState
56+
})
57+
58+
useIsomorphicLayoutEffect(() => {
59+
function checkForUpdates() {
60+
try {
61+
const newSelectedState = latestSelector.current(store.getState())
62+
63+
if (shallowEqual(newSelectedState, latestSelectedState.current)) {
64+
return
65+
}
66+
67+
latestSelectedState.current = newSelectedState
68+
} catch {
69+
// we ignore all errors here, since when the component
70+
// is re-rendered, the selectors are called again, and
71+
// will throw again, if neither props nor store state
72+
// changed
73+
}
74+
75+
forceRender({})
76+
}
77+
78+
subscription.onStateChange = checkForUpdates
79+
subscription.trySubscribe()
80+
81+
checkForUpdates()
82+
83+
return () => subscription.tryUnsubscribe()
84+
}, [store, subscription])
85+
86+
return selectedState
87+
}

src/hooks/useStore.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useReduxContext } from './useReduxContext'
2+
3+
/**
4+
* A hook to access the redux store.
5+
*
6+
* @returns {any} the redux store
7+
*
8+
* Usage:
9+
*
10+
```jsx
11+
import React from 'react'
12+
import { useStore } from 'react-redux'
13+
14+
export const CounterComponent = ({ value }) => {
15+
const store = useStore()
16+
return <div>{store.getState()}</div>
17+
}
18+
```
19+
*/
20+
export function useStore() {
21+
const { store } = useReduxContext()
22+
return store
23+
}

src/index.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,26 @@ import connectAdvanced from './components/connectAdvanced'
33
import { ReactReduxContext } from './components/Context'
44
import connect from './connect/connect'
55

6+
import { useActions } from './hooks/useActions'
7+
import { useDispatch } from './hooks/useDispatch'
8+
import { useReduxContext } from './hooks/useReduxContext'
9+
import { useSelector } from './hooks/useSelector'
10+
import { useStore } from './hooks/useStore'
11+
612
import { setBatch } from './utils/batch'
713
import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
814

915
setBatch(batch)
1016

11-
export { Provider, connectAdvanced, ReactReduxContext, connect, batch }
17+
export {
18+
Provider,
19+
connectAdvanced,
20+
ReactReduxContext,
21+
connect,
22+
batch,
23+
useActions,
24+
useDispatch,
25+
useReduxContext,
26+
useSelector,
27+
useStore
28+
}

0 commit comments

Comments
 (0)