diff --git a/docs/api/hooks.md b/docs/api/hooks.md index cbc0043dd..402c77924 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -295,6 +295,40 @@ 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. + +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 ( + + {children} + + ) +} +``` + ## Usage Warnings ### Stale Props and "Zombie Children" diff --git a/src/hooks/useDispatch.js b/src/hooks/useDispatch.js index 42696c089..b4224fe34 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 ``. + * @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 9fcc1f017..2b88b16d1 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
{counter}
- * } - */ -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 ``. + * @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
{counter}
+ * } + */ +export const useSelector = createSelectorHook() diff --git a/src/hooks/useStore.js b/src/hooks/useStore.js index 16cca17a4..4cc426521 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 ``. + * @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
{store.getState()}
* } */ -export function useStore() { - const { store } = useReduxContext() - return store -} +export const useStore = createStoreHook() diff --git a/src/index.js b/src/index.js index 8817a27aa..d02c35a07 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 } diff --git a/test/hooks/useDispatch.spec.js b/test/hooks/useDispatch.spec.js index 95d654d5f..c4f64face 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 }) => ( + + + {children} + + + ) + }) + + expect(result.current).toBe(store.dispatch) + + const { result: result2 } = renderHook(() => useCustomDispatch(), { + // eslint-disable-next-line react/prop-types + wrapper: ({ children, ...props }) => ( + + + {children} + + + ) + }) + + expect(result2.current).toBe(store2.dispatch) + }) + }) }) }) diff --git a/test/hooks/useSelector.spec.js b/test/hooks/useSelector.spec.js index 8cdc5045c..b0c8692b0 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( + + + + + + + + ) + + expect(defaultCount).toBe(0) + expect(customCount).toBe(12) + }) + }) }) })