-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add react hooks for accessing redux store state and dispatching redux…
… actions (#1248) * add react hooks for accessing redux store state and dispatching redux actions * remove `useReduxContext` from public API * add `useRedux` hook * Preserve stack trace of errors inside store subscription callback Ported changes from react-redux-hooks-poc Note: the "transient errors" test seems flawed atm. * Alter test descriptions to use string names WebStorm won't recognize tests as runnable if `someFunc.name` is used as the `describe()` argument.
- Loading branch information
1 parent
d4b54b5
commit a69b4f4
Showing
14 changed files
with
878 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { bindActionCreators } from 'redux' | ||
import invariant from 'invariant' | ||
import { useDispatch } from './useDispatch' | ||
import { useMemo } from 'react' | ||
|
||
/** | ||
* A hook to bind action creators to the redux store's `dispatch` function | ||
* similar to how redux's `bindActionCreators` works. | ||
* | ||
* Supports passing a single action creator, an array/tuple of action | ||
* creators, or an object of action creators. | ||
* | ||
* Any arguments passed to the created callbacks are passed through to | ||
* the your functions. | ||
* | ||
* This hook takes a dependencies array as an optional second argument, | ||
* which when passed ensures referential stability of the created callbacks. | ||
* | ||
* @param {Function|Function[]|Object.<string, Function>} actions the action creators to bind | ||
* @param {any[]} deps (optional) dependencies array to control referential stability | ||
* | ||
* @returns {Function|Function[]|Object.<string, Function>} callback(s) bound to store's `dispatch` function | ||
* | ||
* Usage: | ||
* | ||
```jsx | ||
import React from 'react' | ||
import { useActions } from 'react-redux' | ||
const increaseCounter = ({ amount }) => ({ | ||
type: 'increase-counter', | ||
amount, | ||
}) | ||
export const CounterComponent = ({ value }) => { | ||
// supports passing an object of action creators | ||
const { increaseCounterByOne, increaseCounterByTwo } = useActions({ | ||
increaseCounterByOne: () => increaseCounter(1), | ||
increaseCounterByTwo: () => increaseCounter(2), | ||
}, []) | ||
// supports passing an array/tuple of action creators | ||
const [increaseCounterByThree, increaseCounterByFour] = useActions([ | ||
() => increaseCounter(3), | ||
() => increaseCounter(4), | ||
], []) | ||
// supports passing a single action creator | ||
const increaseCounterBy5 = useActions(() => increaseCounter(5), []) | ||
// passes through any arguments to the callback | ||
const increaseCounterByX = useActions(x => increaseCounter(x), []) | ||
return ( | ||
<div> | ||
<span>{value}</span> | ||
<button onClick={increaseCounterByOne}>Increase counter by 1</button> | ||
</div> | ||
) | ||
} | ||
``` | ||
*/ | ||
export function useActions(actions, deps) { | ||
invariant(actions, `You must pass actions to useActions`) | ||
|
||
const dispatch = useDispatch() | ||
return useMemo(() => { | ||
if (Array.isArray(actions)) { | ||
return actions.map(a => bindActionCreators(a, dispatch)) | ||
} | ||
|
||
return bindActionCreators(actions, dispatch) | ||
}, deps) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { useStore } from './useStore' | ||
|
||
/** | ||
* A hook to access the redux `dispatch` function. Note that in most cases where you | ||
* might want to use this hook it is recommended to use `useActions` instead to bind | ||
* action creators to the `dispatch` function. | ||
* | ||
* @returns {any} redux store's `dispatch` function | ||
* | ||
* Usage: | ||
* | ||
```jsx | ||
import React, { useCallback } from 'react' | ||
import { useReduxDispatch } from 'react-redux' | ||
export const CounterComponent = ({ value }) => { | ||
const dispatch = useDispatch() | ||
const increaseCounter = useCallback(() => dispatch({ type: 'increase-counter' }), []) | ||
return ( | ||
<div> | ||
<span>{value}</span> | ||
<button onClick={increaseCounter}>Increase counter</button> | ||
</div> | ||
) | ||
} | ||
``` | ||
*/ | ||
export function useDispatch() { | ||
const store = useStore() | ||
return store.dispatch | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { useSelector } from './useSelector' | ||
import { useActions } from './useActions' | ||
|
||
/** | ||
* A hook to access the redux store's state and to bind action creators to | ||
* the store's dispatch function. In essence, this hook is a combination of | ||
* `useSelector` and `useActions`. | ||
* | ||
* @param {Function} selector the selector function | ||
* @param {Function|Function[]|Object.<string, Function>} actions the action creators to bind | ||
* | ||
* @returns {[any, any]} a tuple of the selected state and the bound action creators | ||
* | ||
* Usage: | ||
* | ||
```jsx | ||
import React from 'react' | ||
import { useRedux } from 'react-redux' | ||
export const CounterComponent = () => { | ||
const [counter, { inc1, inc }] = useRedux(state => state.counter, { | ||
inc1: () => ({ type: 'inc1' }), | ||
inc: amount => ({ type: 'inc', amount }), | ||
}) | ||
return ( | ||
<> | ||
<div> | ||
{counter} | ||
</div> | ||
<button onClick={inc1}>Increment by 1</button> | ||
<button onClick={() => inc(5)}>Increment by 5</button> | ||
</> | ||
) | ||
} | ||
``` | ||
*/ | ||
export function useRedux(selector, actions) { | ||
return [useSelector(selector), useActions(actions)] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { useContext } from 'react' | ||
import invariant from 'invariant' | ||
import { ReactReduxContext } from '../components/Context' | ||
|
||
/** | ||
* A hook to access the value of the `ReactReduxContext`. This is a low-level | ||
* hook that you should usually not need to call directly. | ||
* | ||
* @returns {any} the value of the `ReactReduxContext` | ||
* | ||
* Usage: | ||
* | ||
```jsx | ||
import React from 'react' | ||
import { useReduxContext } from 'react-redux' | ||
export const CounterComponent = ({ value }) => { | ||
const { store } = useReduxContext() | ||
return <div>{store.getState()}</div> | ||
} | ||
``` | ||
*/ | ||
export function useReduxContext() { | ||
const contextValue = useContext(ReactReduxContext) | ||
|
||
invariant( | ||
contextValue, | ||
'could not find react-redux context value; please ensure the component is wrapped in a <Provider>' | ||
) | ||
|
||
return contextValue | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { useReducer, useRef, useEffect, useMemo, useLayoutEffect } from 'react' | ||
import invariant from 'invariant' | ||
import { useReduxContext } from './useReduxContext' | ||
import shallowEqual from '../utils/shallowEqual' | ||
import Subscription from '../utils/Subscription' | ||
|
||
// React currently throws a warning when using useLayoutEffect on the server. | ||
// To get around it, we can conditionally useEffect on the server (no-op) and | ||
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store | ||
// subscription callback always has the selector from the latest render commit | ||
// available, otherwise a store update may happen between render and the effect, | ||
// which may cause missed updates; we also must ensure the store subscription | ||
// is created synchronously, otherwise a store update may occur before the | ||
// subscription is created and an inconsistent state may be observed | ||
const useIsomorphicLayoutEffect = | ||
typeof window !== 'undefined' ? useLayoutEffect : useEffect | ||
|
||
/** | ||
* A hook to access the redux store's state. This hook takes a selector function | ||
* as an argument. The selector is called with the store state. | ||
* | ||
* @param {Function} selector the selector function | ||
* | ||
* @returns {any} the selected state | ||
* | ||
* Usage: | ||
* | ||
```jsx | ||
import React from 'react' | ||
import { useSelector } from 'react-redux' | ||
export const CounterComponent = () => { | ||
const counter = useSelector(state => state.counter) | ||
return <div>{counter}</div> | ||
} | ||
``` | ||
*/ | ||
export function useSelector(selector) { | ||
invariant(selector, `You must pass a selector to useSelectors`) | ||
|
||
const { store, subscription: contextSub } = useReduxContext() | ||
const [, forceRender] = useReducer(s => s + 1, 0) | ||
|
||
const subscription = useMemo(() => new Subscription(store, contextSub), [ | ||
store, | ||
contextSub | ||
]) | ||
|
||
const latestSubscriptionCallbackError = useRef() | ||
const latestSelector = useRef(selector) | ||
|
||
let selectedState = undefined | ||
|
||
try { | ||
selectedState = latestSelector.current(store.getState()) | ||
} catch (err) { | ||
let errorMessage = `An error occured while selecting the store state: ${ | ||
err.message | ||
}.` | ||
|
||
if (latestSubscriptionCallbackError.current) { | ||
errorMessage += `\nThe error may be correlated with this previous error:\n${ | ||
latestSubscriptionCallbackError.current.stack | ||
}\n\nOriginal stack trace:` | ||
} | ||
|
||
throw new Error(errorMessage) | ||
} | ||
|
||
const latestSelectedState = useRef(selectedState) | ||
|
||
useIsomorphicLayoutEffect(() => { | ||
latestSelector.current = selector | ||
latestSelectedState.current = selectedState | ||
latestSubscriptionCallbackError.current = undefined | ||
}) | ||
|
||
useIsomorphicLayoutEffect(() => { | ||
function checkForUpdates() { | ||
try { | ||
const newSelectedState = latestSelector.current(store.getState()) | ||
|
||
if (shallowEqual(newSelectedState, latestSelectedState.current)) { | ||
return | ||
} | ||
|
||
latestSelectedState.current = newSelectedState | ||
} catch (err) { | ||
// we ignore all errors here, since when the component | ||
// is re-rendered, the selectors are called again, and | ||
// will throw again, if neither props nor store state | ||
// changed | ||
latestSubscriptionCallbackError.current = err | ||
} | ||
|
||
forceRender({}) | ||
} | ||
|
||
subscription.onStateChange = checkForUpdates | ||
subscription.trySubscribe() | ||
|
||
checkForUpdates() | ||
|
||
return () => subscription.tryUnsubscribe() | ||
}, [store, subscription]) | ||
|
||
return selectedState | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { useReduxContext } from './useReduxContext' | ||
|
||
/** | ||
* A hook to access the redux store. | ||
* | ||
* @returns {any} the redux store | ||
* | ||
* Usage: | ||
* | ||
```jsx | ||
import React from 'react' | ||
import { useStore } from 'react-redux' | ||
export const CounterComponent = ({ value }) => { | ||
const store = useStore() | ||
return <div>{store.getState()}</div> | ||
} | ||
``` | ||
*/ | ||
export function useStore() { | ||
const { store } = useReduxContext() | ||
return store | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.