Repository: reduxjs/react-redux
Pull request #1309 (2019-06-11 16:34:48)

feat: implement `create*Hook` APIs
As discussed in issue #1304.

For the `create*Hook` factory functions, I was debating between using `context` as the argument to all of them, vs asking users to call `createReduxContextHook` and pass their custom `useReduxContext` as the argument to the remaining three. I chose the latter since it seemed like less churn, but I'd be happy to explore other options or refactor as needed.

netlify[bot]: Deploy preview for *react-redux-docs* ready!

Built with commit efd270f3e549bf95ae916621b502a3e81dca6f7e

https://deploy-preview-1309--react-redux-docs.netlify.com
timdorr: So, I was kind of expecting this:
```js
export const myContext = React.createContext(null)

export const useStore = createStoreHook(myContext)
export const useDispatch = createDispatchHook(myContext)
export const useSelector = createSelectorHook(myContext)
```
I don't think creating a `useContext` factory adds a ton of value.  I think it's just as simple as `context === ReactReduxContext ? useReduxContext() : useContext(context)`.
markerikson: As a vaguely related note - @drmcda made some kind of suggestion on Reddit about [somehow using closures to avoid needing a Provider at all](https://www.reddit.com/r/reactjs/comments/bz6vcu/reactredux_710_the_one_with_hooks_released/eqr02cq/).  I don't actually get what he's suggesting there, but maybe he can pop in and explain?
ryaninvents: @markerikson I think that Reddit thread describes what I was originally envisioning, but Tim described the potential issues in [this comment](https://github.com/reduxjs/react-redux/issues/1304#issuecomment-500852772).
timdorr: OK, I think this looks good. Any further comments from anyone else?
markerikson: Just out of curiosity, was there any discussion or thought put into just updating the hooks to accept a context object either as a direct parameter or in an options object?
dai-shi: If it's still not too late, I'd like to open a discussion to create all hooks and a provider in one function.

```javascript
const { Provider, useStore, useDispatch, useSelector } = createCustomHooks(myContext);
```

This could make type inference easier for DefinitelyTyped and flow-typed.

> thought put into just updating the hooks to accept a context object

To me, if we go with`create*Hook`, `createProvider(myContext)` seems more consistent. Or, accept a context object for hooks and Provider, which I did [so](https://github.com/dai-shi/reactive-react-redux/blob/cde66a5b147aff767d8d3da341e36b63e6dd9588/src/useDispatch.js#L6-L8) without any thoughtful reason.

timdorr: > Just out of curiosity, was there any discussion or thought put into just updating the hooks to accept a context object either as a direct parameter or in an options object?

I'm much more in favor of composition over configuration. We've already established a 2nd arg on useSelector, so now remembering argument order would come into play. Also, we'll have to handle defaults or do the polymorphic stuff that createStore does. A factory function feels like a better option, especially when it makes reuse easy. 
ryaninvents: @markerikson I thought about adding an extra parameter, but then all hooks using the default context (which would be the vast majority) would see a very small performance hit. I realize that it's unlikely that we'd even be able to measure the perf hit, but on principle I felt as though this enhancement for the edge case shouldn't impact the vast majority of users who would never run into this problem.

@dai-shi There's some related discussion in the [feature request issue](https://github.com/reduxjs/react-redux/issues/1304#issuecomment-500827537); I'd originally implemented it the way you describe.
joshjg: @markerikson @timdorr @ryaninvents Anything blocking this from moving forward? Any help needed?
timdorr: Just updated against master. Provided that passes, I'll merge this in.

Patch:
diff --git a/docs/api/hooks.md b/docs/api/hooks.md
index cbc0043..7e76237 100644
--- a/docs/api/hooks.md
+++ b/docs/api/hooks.md
@@ -192,7 +192,7 @@ export const TodoCounterForIsDoneValue = ({ isDone }) => {
   )
 
   const numOfTodosWithIsDoneValue = useSelector(state =>
-    selectNumOfTodosWithIsDoneValue(state, isDone)
+    selectNumOfTodosWithIsDone(state, isDone)
   )
 
   return <div>{numOfTodosWithIsDoneValue}</div>
@@ -295,6 +295,40 @@ export const CounterComponent = ({ value }) => {
 }
 ```
 
+
+## Custom context
+
+The `<Provider>` 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.
+
+To access an alternate context via the hooks API, use the hook creator functions:
+
+```js
+import React from 'react'
+import {
+  Provider,
+  createStoreHook,
+  createDispatchHook,
+  createSelectorHook
+} from 'react-redux'
+
+const MyContext = React.createContext(null)
+
+// Export your custom hooks if you wish to use them in other files.
+export const useStore = createStoreHook(MyContext)
+export const useDispatch = createDispatchHook(MyContext)
+export const useSelector = createSelectorHook(MyContext)
+
+const myStore = createStore(rootReducer)
+
+export function MyProvider({ children }) {
+  return (
+    <Provider context={MyContext} store={myStore}>
+      {children}
+    </Provider>
+  )
+}
+```
+
 ## Usage Warnings
 
 ### Stale Props and "Zombie Children"
diff --git a/src/hooks/useDispatch.js b/src/hooks/useDispatch.js
index 42696c0..b4224fe 100644
--- a/src/hooks/useDispatch.js
+++ b/src/hooks/useDispatch.js
@@ -1,4 +1,20 @@
-import { useStore } from './useStore'
+import { ReactReduxContext } from '../components/Context'
+import { useStore as useDefaultStore, createStoreHook } from './useStore'
+
+/**
+ * Hook factory, which creates a `useDispatch` hook bound to a given context.
+ *
+ * @param {Function} [context=ReactReduxContext] Context passed to your `<Provider>`.
+ * @returns {Function} A `useDispatch` hook bound to the specified context.
+ */
+export function createDispatchHook(context = ReactReduxContext) {
+  const useStore =
+    context === ReactReduxContext ? useDefaultStore : createStoreHook(context)
+  return function useDispatch() {
+    const store = useStore()
+    return store.dispatch
+  }
+}
 
 /**
  * A hook to access the redux `dispatch` function.
@@ -21,7 +37,4 @@ import { useStore } from './useStore'
  *   )
  * }
  */
-export function useDispatch() {
-  const store = useStore()
-  return store.dispatch
-}
+export const useDispatch = createDispatchHook()
diff --git a/src/hooks/useSelector.js b/src/hooks/useSelector.js
index 9fcc1f0..2b88b16 100644
--- a/src/hooks/useSelector.js
+++ b/src/hooks/useSelector.js
@@ -1,7 +1,15 @@
-import { useReducer, useRef, useEffect, useMemo, useLayoutEffect } from 'react'
+import {
+  useReducer,
+  useRef,
+  useEffect,
+  useMemo,
+  useLayoutEffect,
+  useContext
+} from 'react'
 import invariant from 'invariant'
-import { useReduxContext } from './useReduxContext'
+import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
 import Subscription from '../utils/Subscription'
+import { ReactReduxContext } from '../components/Context'
 
 // 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
@@ -16,33 +24,12 @@ const useIsomorphicLayoutEffect =
 
 const refEquality = (a, b) => a === b
 
-/**
- * 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.
- *
- * This hook takes an optional equality comparison function as the second parameter
- * that allows you to customize the way the selected state is compared to determine
- * whether the component needs to be re-rendered.
- *
- * @param {Function} selector the selector function
- * @param {Function=} equalityFn the function that will be used to determine equality
- *
- * @returns {any} the selected state
- *
- * @example
- *
- * 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, equalityFn = refEquality) {
-  invariant(selector, `You must pass a selector to useSelectors`)
-
-  const { store, subscription: contextSub } = useReduxContext()
+function useSelectorWithStoreAndSubscription(
+  selector,
+  equalityFn,
+  store,
+  contextSub
+) {
   const [, forceRender] = useReducer(s => s + 1, 0)
 
   const subscription = useMemo(() => new Subscription(store, contextSub), [
@@ -112,3 +99,53 @@ export function useSelector(selector, equalityFn = refEquality) {
 
   return selectedState
 }
+
+/**
+ * Hook factory, which creates a `useSelector` hook bound to a given context.
+ *
+ * @param {Function} [context=ReactReduxContext] Context passed to your `<Provider>`.
+ * @returns {Function} A `useSelector` hook bound to the specified context.
+ */
+export function createSelectorHook(context = ReactReduxContext) {
+  const useReduxContext =
+    context === ReactReduxContext
+      ? useDefaultReduxContext
+      : () => useContext(context)
+  return function useSelector(selector, equalityFn = refEquality) {
+    invariant(selector, `You must pass a selector to useSelectors`)
+
+    const { store, subscription: contextSub } = useReduxContext()
+
+    return useSelectorWithStoreAndSubscription(
+      selector,
+      equalityFn,
+      store,
+      contextSub
+    )
+  }
+}
+
+/**
+ * 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.
+ *
+ * This hook takes an optional equality comparison function as the second parameter
+ * that allows you to customize the way the selected state is compared to determine
+ * whether the component needs to be re-rendered.
+ *
+ * @param {Function} selector the selector function
+ * @param {Function=} equalityFn the function that will be used to determine equality
+ *
+ * @returns {any} the selected state
+ *
+ * @example
+ *
+ * import React from 'react'
+ * import { useSelector } from 'react-redux'
+ *
+ * export const CounterComponent = () => {
+ *   const counter = useSelector(state => state.counter)
+ *   return <div>{counter}</div>
+ * }
+ */
+export const useSelector = createSelectorHook()
diff --git a/src/hooks/useStore.js b/src/hooks/useStore.js
index 16cca17..4cc4265 100644
--- a/src/hooks/useStore.js
+++ b/src/hooks/useStore.js
@@ -1,4 +1,23 @@
-import { useReduxContext } from './useReduxContext'
+import { useContext } from 'react'
+import { ReactReduxContext } from '../components/Context'
+import { useReduxContext as useDefaultReduxContext } from './useReduxContext'
+
+/**
+ * Hook factory, which creates a `useStore` hook bound to a given context.
+ *
+ * @param {Function} [context=ReactReduxContext] Context passed to your `<Provider>`.
+ * @returns {Function} A `useStore` hook bound to the specified context.
+ */
+export function createStoreHook(context = ReactReduxContext) {
+  const useReduxContext =
+    context === ReactReduxContext
+      ? useDefaultReduxContext
+      : () => useContext(context)
+  return function useStore() {
+    const { store } = useReduxContext()
+    return store
+  }
+}
 
 /**
  * A hook to access the redux store.
@@ -15,7 +34,4 @@ import { useReduxContext } from './useReduxContext'
  *   return <div>{store.getState()}</div>
  * }
  */
-export function useStore() {
-  const { store } = useReduxContext()
-  return store
-}
+export const useStore = createStoreHook()
diff --git a/src/index.js b/src/index.js
index 8817a27..d02c35a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,9 +3,9 @@ import connectAdvanced from './components/connectAdvanced'
 import { ReactReduxContext } from './components/Context'
 import connect from './connect/connect'
 
-import { useDispatch } from './hooks/useDispatch'
-import { useSelector } from './hooks/useSelector'
-import { useStore } from './hooks/useStore'
+import { useDispatch, createDispatchHook } from './hooks/useDispatch'
+import { useSelector, createSelectorHook } from './hooks/useSelector'
+import { useStore, createStoreHook } from './hooks/useStore'
 
 import { setBatch } from './utils/batch'
 import { unstable_batchedUpdates as batch } from './utils/reactBatchedUpdates'
@@ -20,7 +20,10 @@ export {
   connect,
   batch,
   useDispatch,
+  createDispatchHook,
   useSelector,
+  createSelectorHook,
   useStore,
+  createStoreHook,
   shallowEqual
 }


Test patch:
diff --git a/test/hooks/useDispatch.spec.js b/test/hooks/useDispatch.spec.js
index 95d654d..c4f64fa 100644
--- a/test/hooks/useDispatch.spec.js
+++ b/test/hooks/useDispatch.spec.js
@@ -1,9 +1,14 @@
 import React from 'react'
 import { createStore } from 'redux'
 import { renderHook } from '@testing-library/react-hooks'
-import { Provider as ProviderMock, useDispatch } from '../../src/index.js'
+import {
+  Provider as ProviderMock,
+  useDispatch,
+  createDispatchHook
+} from '../../src/index.js'
 
 const store = createStore(c => c + 1)
+const store2 = createStore(c => c + 2)
 
 describe('React', () => {
   describe('hooks', () => {
@@ -16,5 +21,36 @@ describe('React', () => {
         expect(result.current).toBe(store.dispatch)
       })
     })
+    describe('createDispatchHook', () => {
+      it("returns the correct store's dispatch function", () => {
+        const nestedContext = React.createContext(null)
+        const useCustomDispatch = createDispatchHook(nestedContext)
+        const { result } = renderHook(() => useDispatch(), {
+          // eslint-disable-next-line react/prop-types
+          wrapper: ({ children, ...props }) => (
+            <ProviderMock {...props} store={store}>
+              <ProviderMock context={nestedContext} store={store2}>
+                {children}
+              </ProviderMock>
+            </ProviderMock>
+          )
+        })
+
+        expect(result.current).toBe(store.dispatch)
+
+        const { result: result2 } = renderHook(() => useCustomDispatch(), {
+          // eslint-disable-next-line react/prop-types
+          wrapper: ({ children, ...props }) => (
+            <ProviderMock {...props} store={store}>
+              <ProviderMock context={nestedContext} store={store2}>
+                {children}
+              </ProviderMock>
+            </ProviderMock>
+          )
+        })
+
+        expect(result2.current).toBe(store2.dispatch)
+      })
+    })
   })
 })
diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js
index 8cdc504..b0c8692 100644
--- a/test/hooks/useSelector.spec.js
+++ b/test/hooks/useSelector.spec.js
@@ -8,7 +8,8 @@ import {
   Provider as ProviderMock,
   useSelector,
   shallowEqual,
-  connect
+  connect,
+  createSelectorHook
 } from '../../src/index.js'
 import { useReduxContext } from '../../src/hooks/useReduxContext'
 
@@ -383,5 +384,54 @@ describe('React', () => {
         })
       })
     })
+
+    describe('createSelectorHook', () => {
+      let defaultStore
+      let customStore
+
+      beforeEach(() => {
+        defaultStore = createStore(({ count } = { count: -1 }) => ({
+          count: count + 1
+        }))
+        customStore = createStore(({ count } = { count: 10 }) => ({
+          count: count + 2
+        }))
+      })
+
+      afterEach(() => rtl.cleanup())
+
+      it('subscribes to the correct store', () => {
+        const nestedContext = React.createContext(null)
+        const useCustomSelector = createSelectorHook(nestedContext)
+        let defaultCount = null
+        let customCount = null
+
+        const getCount = s => s.count
+
+        const DisplayDefaultCount = ({ children = null }) => {
+          const count = useSelector(getCount)
+          defaultCount = count
+          return <>{children}</>
+        }
+        const DisplayCustomCount = ({ children = null }) => {
+          const count = useCustomSelector(getCount)
+          customCount = count
+          return <>{children}</>
+        }
+
+        rtl.render(
+          <ProviderMock store={defaultStore}>
+            <ProviderMock context={nestedContext} store={customStore}>
+              <DisplayCustomCount>
+                <DisplayDefaultCount />
+              </DisplayCustomCount>
+            </ProviderMock>
+          </ProviderMock>
+        )
+
+        expect(defaultCount).toBe(0)
+        expect(customCount).toBe(12)
+      })
+    })
   })
 })