-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
@wordpress/data: Introduce new custom useDispatch
react hook
#15896
Changes from 14 commits
d54e692
0db22f7
2c15377
2f1e32d
6f3ee01
9c9df2f
9dcce6f
690830c
5ee59b6
89a7fb4
70845bb
33293b2
50e8a70
6946a6a
6500117
4b19d7c
5137db6
16b2b04
0426938
540860b
21189e7
326fba1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'; |
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import useRegistry from '../registry-provider/use-registry'; | ||
|
||
/** | ||
* A custom react hook returning the current registry dispatch actions creators. | ||
* | ||
* Note: The component using this hook must be within the context of a | ||
* RegistryProvider. | ||
* | ||
* @param {?string} storeName Optionally provide the name of the store from | ||
* which to retrieve action creators. If not | ||
* provided, the registry.dispatch function is | ||
* returned instead. | ||
* | ||
* @example | ||
* This illustrates a pattern where you may need to retrieve dynamic data from | ||
* the server via the `useSelect` hook to use in combination with the dispatch | ||
* action. | ||
* | ||
* ```jsx | ||
* const { useDispatch, useSelect } = wp.data; | ||
* const { useCallback } = wp.element; | ||
* | ||
* function Button( { onClick, children } ) { | ||
* return <button type="button" onClick={ onClick }>{ children }</button> | ||
* } | ||
* | ||
* const SaleButton = ( { children } ) => { | ||
* const { stockNumber } = useSelect( | ||
* ( select ) => select( 'my-shop' ).getStockNumber() | ||
* ); | ||
* const { startSale } = useDispatch( 'my-shop' ); | ||
* const onClick = useCallback( () => { | ||
* const discountPercent = stockNumber > 50 ? 10: 20; | ||
* startSale( discountPercent ); | ||
* }, [ stockNumber ] ); | ||
* return <Button onClick={ onClick }>{ children }</Button> | ||
* } | ||
* | ||
* // Rendered somewhere in the application: | ||
* // | ||
* // <SaleButton>Start Sale!</SaleButton> | ||
* ``` | ||
* @return {Function} A custom react hook. | ||
*/ | ||
const useDispatch = ( storeName ) => { | ||
const { dispatch } = useRegistry(); | ||
return storeName === void 0 ? dispatch : dispatch( storeName ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First time I see There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ya There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's not been possible to override in any browser which conforms to ES5 and newer (IE9+). https://www.ecma-international.org/ecma-262/5.1/#sec-15.1.1.3 A future maintainer most certainly will struggle to under what this code is doing. Not a big issue, but in general we should feel fine to compare against There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Babel might even take care of it too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel really old all of a sudden 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was curious about this, and it doesn't seem like Babel does, but Uglify will convert it, yes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, maybe for the older targets? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likely because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ha! That's my new reason. |
||
}; | ||
|
||
export default useDispatch; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,12 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { mapValues } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Component } from '@wordpress/element'; | ||
import { createHigherOrderComponent } from '@wordpress/compose'; | ||
import { pure, createHigherOrderComponent } from '@wordpress/compose'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { RegistryConsumer } from '../registry-provider'; | ||
import { useDispatchWithMap } from '../use-dispatch'; | ||
|
||
/** | ||
* Higher-order component used to add dispatch props using registered action creators. | ||
|
@@ -79,61 +73,17 @@ import { RegistryConsumer } from '../registry-provider'; | |
* @return {Component} Enhanced component with merged dispatcher props. | ||
*/ | ||
const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent( | ||
( WrappedComponent ) => { | ||
class ComponentWithDispatch extends Component { | ||
constructor( props ) { | ||
super( ...arguments ); | ||
|
||
this.proxyProps = {}; | ||
|
||
this.setProxyProps( props ); | ||
} | ||
|
||
proxyDispatch( propName, ...args ) { | ||
// Original dispatcher is a pre-bound (dispatching) action creator. | ||
mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps, this.props.registry )[ propName ]( ...args ); | ||
} | ||
|
||
setProxyProps( props ) { | ||
// Assign as instance property so that in subsequent render | ||
// reconciliation, the prop values are referentially equal. | ||
// Importantly, note that while `mapDispatchToProps` is | ||
// called, it is done only to determine the keys for which | ||
// proxy functions should be created. The actual registry | ||
// dispatch does not occur until the function is called. | ||
const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps, this.props.registry ); | ||
this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => { | ||
if ( typeof dispatcher !== 'function' ) { | ||
// eslint-disable-next-line no-console | ||
console.warn( `Property ${ propName } returned from mapDispatchToProps in withDispatch must be a function.` ); | ||
} | ||
// Prebind with prop name so we have reference to the original | ||
// dispatcher to invoke. Track between re-renders to avoid | ||
// creating new function references every render. | ||
if ( this.proxyProps.hasOwnProperty( propName ) ) { | ||
return this.proxyProps[ propName ]; | ||
} | ||
|
||
return this.proxyDispatch.bind( this, propName ); | ||
} ); | ||
} | ||
|
||
render() { | ||
return <WrappedComponent { ...this.props.ownProps } { ...this.proxyProps } />; | ||
} | ||
( WrappedComponent ) => pure( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh good point, I'll try without it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed in 6500117. The new 3 run average for the new Average time to load: 6590ms So ya, it does look like there was some unnecessary overhead added by using |
||
( ownProps ) => { | ||
const mapDispatch = ( dispatch, registry ) => mapDispatchToProps( | ||
dispatch, | ||
ownProps, | ||
registry | ||
); | ||
const dispatchProps = useDispatchWithMap( mapDispatch, [] ); | ||
return <WrappedComponent { ...ownProps } { ...dispatchProps } />; | ||
} | ||
|
||
return ( ownProps ) => ( | ||
<RegistryConsumer> | ||
{ ( registry ) => ( | ||
<ComponentWithDispatch | ||
ownProps={ ownProps } | ||
registry={ registry } | ||
/> | ||
) } | ||
</RegistryConsumer> | ||
); | ||
}, | ||
), | ||
'withDispatch' | ||
); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What purpose does
deps
serve if this is an internal hook, and the only place we use it, we always pass an empty array?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Right now with this being an internal only implementation it's obviously an unneeded variable. There may be potential for this hook being exposed at some point as a public api (if use-case demonstrates need) in which case there is utility for the deps arg.