From 2677705abb7262a7c309f84d8732186bfcb18c0d Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Tue, 5 Nov 2019 22:06:16 -0500 Subject: [PATCH] Pass non-Redux-store values through the `store` prop (#1447) * Allow non-Redux-store values as a prop named `store` * Formatting --- docs/api/connect.md | 2 +- docs/api/hooks.md | 23 ++++++++++------------- docs/troubleshooting.md | 2 +- src/components/connectAdvanced.js | 13 ++++++++++--- test/components/connect.spec.js | 15 +++++++++++++++ 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/docs/api/connect.md b/docs/api/connect.md index 574af6bc3..3d6aa25dd 100644 --- a/docs/api/connect.md +++ b/docs/api/connect.md @@ -571,7 +571,7 @@ export default connect( The number of declared function parameters of `mapStateToProps` and `mapDispatchToProps` determines whether they receive `ownProps` -> Note: `ownProps` is not passed to `mapStateToProps` and `mapDispatchToProps` if the formal definition of the function contains one mandatory parameter (function has length 1). For example, functions defined like below won't receive `ownProps` as the second argument. If the incoming value of `ownProps` is `undefined`, the default argument value will be used. +> Note: `ownProps` is not passed to `mapStateToProps` and `mapDispatchToProps` if the formal definition of the function contains one mandatory parameter (function has length 1). For example, functions defined like below won't receive `ownProps` as the second argument. If the incoming value of `ownProps` is `undefined`, the default argument value will be used. ```js function mapStateToProps(state) { diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 8390ace90..9ad3308d8 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -13,7 +13,6 @@ React Redux now offers a set of hook APIs as an alternative to the existing `con These hooks were first added in v7.1.0. - ## Using Hooks in a React Redux App As with `connect()`, you should start by wrapping your entire application in a `` component to make the store available throughout the component tree: @@ -212,10 +211,6 @@ export const App = () => { ## Removed: `useActions()` - - - - ## `useDispatch()` ```js @@ -295,7 +290,6 @@ export const CounterComponent = ({ value }) => { } ``` - ## Custom context The `` component allows you to specify an alternate context via the `context` prop. This is useful if you're building a complex reusable component, and you don't want your store to collide with any Redux store your consumers' applications might use. @@ -395,7 +389,7 @@ This hook was in our original alpha release, but removed in `v7.1.0-alpha.4`, ba That suggestion was based on "binding action creators" not being as useful in a hooks-based use case, and causing too much conceptual overhead and syntactic complexity. -You should probably prefer to call the [`useDispatch`](#usedispatch) hook in your components to retrieve a reference to `dispatch`, +You should probably prefer to call the [`useDispatch`](#usedispatch) hook in your components to retrieve a reference to `dispatch`, and manually call `dispatch(someActionCreator())` in callbacks and effects as needed. You may also use the Redux [`bindActionCreators`](https://redux.js.org/api/bindactioncreators) function in your own code to bind action creators, or "manually" bind them like `const boundAddTodo = (text) => dispatch(addTodo(text))`. @@ -410,12 +404,15 @@ import { useMemo } from 'react' export function useActions(actions, deps) { const dispatch = useDispatch() - return useMemo(() => { - if (Array.isArray(actions)) { - return actions.map(a => bindActionCreators(a, dispatch)) - } - return bindActionCreators(actions, dispatch) - }, deps ? [dispatch, ...deps] : [dispatch]) + return useMemo( + () => { + if (Array.isArray(actions)) { + return actions.map(a => bindActionCreators(a, dispatch)) + } + return bindActionCreators(actions, dispatch) + }, + deps ? [dispatch, ...deps] : [dispatch] + ) } ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index cbecafaa8..ab8f7f2a6 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -118,4 +118,4 @@ Or by setting it globally: } ``` -See https://github.com/facebook/react/issues/14927#issuecomment-490426131 \ No newline at end of file +See https://github.com/facebook/react/issues/14927#issuecomment-490426131 diff --git a/src/components/connectAdvanced.js b/src/components/connectAdvanced.js index 94cf10190..dcb18db30 100644 --- a/src/components/connectAdvanced.js +++ b/src/components/connectAdvanced.js @@ -163,8 +163,14 @@ export default function connectAdvanced( // Retrieve the store and ancestor subscription via context, if available const contextValue = useContext(ContextToUse) - // The store _must_ exist as either a prop or in context - const didStoreComeFromProps = Boolean(props.store) + // The store _must_ exist as either a prop or in context. + // We'll check to see if it _looks_ like a Redux store first. + // This allows us to pass through a `store` prop that is just a plain value. + console.log('Store from props: ', props.store) + const didStoreComeFromProps = + Boolean(props.store) && + Boolean(props.store.getState) && + Boolean(props.store.dispatch) const didStoreComeFromContext = Boolean(contextValue) && Boolean(contextValue.store) @@ -176,7 +182,8 @@ export default function connectAdvanced( `React context consumer to ${displayName} in connect options.` ) - const store = props.store || contextValue.store + // Based on the previous check, one of these must be true + const store = didStoreComeFromProps ? props.store : contextValue.store const childPropsSelector = useMemo(() => { // The child props selector needs the store reference as an input. diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 29d6da6ea..f9c527876 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -2090,6 +2090,21 @@ describe('React', () => { expect(actualState).toEqual(expectedState) }) + it('should pass through a store prop that is not actually a Redux store', () => { + const notActuallyAStore = 42 + + const store = createStore(() => 123) + const Decorated = connect(state => ({ state }))(Passthrough) + + const rendered = rtl.render( + + + + ) + + expect(rendered.getByTestId('store')).toHaveTextContent('42') + }) + it('should pass through ancestor subscription when store is given as a prop', () => { const c3Spy = jest.fn() const c2Spy = jest.fn()