-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
@wordpress/data: Introduce new custom
useDispatch
react hook (#15896)
This pull contains the work exposing a new `useDispatch` hook on the @wordpress/data package. Also, `withDispatch` has been refactored internally to use a new (internal only) `useDispatchWithMap` hook.
- Loading branch information
Showing
9 changed files
with
459 additions
and
153 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,2 @@ | ||
export { default as useDispatch } from './use-dispatch'; | ||
export { default as useDispatchWithMap } from './use-dispatch-with-map'; |
137 changes: 137 additions & 0 deletions
137
packages/data/src/components/use-dispatch/test/use-dispatch.js
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,137 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import TestRenderer, { act } from 'react-test-renderer'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import useDispatch from '../use-dispatch'; | ||
import { createRegistry } from '../../../registry'; | ||
import { RegistryProvider } from '../../registry-provider'; | ||
|
||
describe( 'useDispatch', () => { | ||
let registry; | ||
beforeEach( () => { | ||
registry = createRegistry(); | ||
} ); | ||
|
||
it( 'returns dispatch function from store with no store name provided', () => { | ||
registry.registerStore( 'demoStore', { | ||
reducer: ( state ) => state, | ||
actions: { | ||
foo: () => 'bar', | ||
}, | ||
} ); | ||
const TestComponent = () => { | ||
return <div></div>; | ||
}; | ||
const Component = () => { | ||
const dispatch = useDispatch(); | ||
return <TestComponent dispatch={ dispatch } />; | ||
}; | ||
|
||
let testRenderer; | ||
act( () => { | ||
testRenderer = TestRenderer.create( | ||
<RegistryProvider value={ registry }> | ||
<Component /> | ||
</RegistryProvider> | ||
); | ||
} ); | ||
|
||
const testInstance = testRenderer.root; | ||
|
||
expect( testInstance.findByType( TestComponent ).props.dispatch ) | ||
.toBe( registry.dispatch ); | ||
} ); | ||
it( 'returns expected action creators from store for given storeName', () => { | ||
const noop = () => ( { type: '__INERT__' } ); | ||
const testAction = jest.fn().mockImplementation( noop ); | ||
registry.registerStore( 'demoStore', { | ||
reducer: ( state ) => state, | ||
actions: { | ||
foo: testAction, | ||
}, | ||
} ); | ||
const TestComponent = () => { | ||
const { foo } = useDispatch( 'demoStore' ); | ||
return <button onClick={ foo } />; | ||
}; | ||
|
||
let testRenderer; | ||
|
||
act( () => { | ||
testRenderer = TestRenderer.create( | ||
<RegistryProvider value={ registry } > | ||
<TestComponent /> | ||
</RegistryProvider> | ||
); | ||
} ); | ||
|
||
const testInstance = testRenderer.root; | ||
|
||
act( () => { | ||
testInstance.findByType( 'button' ).props.onClick(); | ||
} ); | ||
|
||
expect( testAction ).toHaveBeenCalledTimes( 1 ); | ||
} ); | ||
it( 'returns dispatch from correct registry if registries change', () => { | ||
const reducer = ( state ) => state; | ||
const noop = () => ( { type: '__INERT__' } ); | ||
const firstRegistryAction = jest.fn().mockImplementation( noop ); | ||
const secondRegistryAction = jest.fn().mockImplementation( noop ); | ||
|
||
const firstRegistry = registry; | ||
firstRegistry.registerStore( 'demo', { | ||
reducer, | ||
actions: { | ||
noop: firstRegistryAction, | ||
}, | ||
} ); | ||
|
||
const TestComponent = () => { | ||
const dispatch = useDispatch(); | ||
return <button onClick={ () => dispatch( 'demo' ).noop() } />; | ||
}; | ||
|
||
let testRenderer; | ||
act( () => { | ||
testRenderer = TestRenderer.create( | ||
<RegistryProvider value={ firstRegistry }> | ||
<TestComponent /> | ||
</RegistryProvider> | ||
); | ||
} ); | ||
const testInstance = testRenderer.root; | ||
|
||
act( () => { | ||
testInstance.findByType( 'button' ).props.onClick(); | ||
} ); | ||
|
||
expect( firstRegistryAction ).toHaveBeenCalledTimes( 1 ); | ||
expect( secondRegistryAction ).toHaveBeenCalledTimes( 0 ); | ||
|
||
const secondRegistry = createRegistry(); | ||
secondRegistry.registerStore( 'demo', { | ||
reducer, | ||
actions: { | ||
noop: secondRegistryAction, | ||
}, | ||
} ); | ||
|
||
act( () => { | ||
testRenderer.update( | ||
<RegistryProvider value={ secondRegistry }> | ||
<TestComponent /> | ||
</RegistryProvider> | ||
); | ||
} ); | ||
act( () => { | ||
testInstance.findByType( 'button' ).props.onClick(); | ||
} ); | ||
expect( firstRegistryAction ).toHaveBeenCalledTimes( 1 ); | ||
expect( secondRegistryAction ).toHaveBeenCalledTimes( 1 ); | ||
} ); | ||
} ); |
71 changes: 71 additions & 0 deletions
71
packages/data/src/components/use-dispatch/use-dispatch-with-map.js
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,71 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { mapValues } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useMemo, useRef, useEffect, useLayoutEffect } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import useRegistry from '../registry-provider/use-registry'; | ||
|
||
/** | ||
* Favor useLayoutEffect to ensure the store subscription callback always has | ||
* the dispatchMap from the latest render. If a store update happens between | ||
* render and the effect, this could cause missed/stale updates or | ||
* inconsistent state. | ||
* | ||
* Fallback to useEffect for server rendered components because currently React | ||
* throws a warning when using useLayoutEffect in that environment. | ||
*/ | ||
const useIsomorphicLayoutEffect = | ||
typeof window !== 'undefined' ? useLayoutEffect : useEffect; | ||
|
||
/** | ||
* Custom react hook for returning aggregate dispatch actions using the provided | ||
* dispatchMap. | ||
* | ||
* Currently this is an internal api only and is implemented by `withDispatch` | ||
* | ||
* @param {Function} dispatchMap Receives the `registry.dispatch` function as | ||
* the first argument and the `registry` object | ||
* as the second argument. Should return an | ||
* object mapping props to functions. | ||
* @param {Array} deps An array of dependencies for the hook. | ||
* @return {Object} An object mapping props to functions created by the passed | ||
* in dispatchMap. | ||
*/ | ||
const useDispatchWithMap = ( dispatchMap, deps ) => { | ||
const registry = useRegistry(); | ||
const currentDispatchMap = useRef( dispatchMap ); | ||
|
||
useIsomorphicLayoutEffect( () => { | ||
currentDispatchMap.current = dispatchMap; | ||
} ); | ||
|
||
return useMemo( () => { | ||
const currentDispatchProps = currentDispatchMap.current( | ||
registry.dispatch, | ||
registry | ||
); | ||
return mapValues( | ||
currentDispatchProps, | ||
( dispatcher, propName ) => { | ||
if ( typeof dispatcher !== 'function' ) { | ||
// eslint-disable-next-line no-console | ||
console.warn( | ||
`Property ${ propName } returned from dispatchMap in useDispatchWithMap must be a function.` | ||
); | ||
} | ||
return ( ...args ) => currentDispatchMap | ||
.current( registry.dispatch, registry )[ propName ]( ...args ); | ||
} | ||
); | ||
}, [ registry, ...deps ] ); | ||
}; | ||
|
||
export default useDispatchWithMap; |
Oops, something went wrong.