-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
1,688 additions
and
1,508 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { createContext } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import defaultRegistry from '../../default-registry'; | ||
|
||
const { Consumer, Provider } = createContext( defaultRegistry ); | ||
|
||
export const RegistryConsumer = Consumer; | ||
|
||
export default Provider; |
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,90 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { mapValues } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
Component, | ||
compose, | ||
createHigherOrderComponent, | ||
pure, | ||
} from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { RegistryConsumer } from '../registry-provider'; | ||
|
||
/** | ||
* Higher-order component used to add dispatch props using registered action | ||
* creators. | ||
* | ||
* @param {Object} mapDispatchToProps Object of prop names where value is a | ||
* dispatch-bound action creator, or a | ||
* function to be called with with the | ||
* component's props and returning an | ||
* action creator. | ||
* | ||
* @return {Component} Enhanced component with merged dispatcher props. | ||
*/ | ||
const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent( | ||
compose( [ | ||
pure, | ||
( WrappedComponent ) => { | ||
class ComponentWithDispatch extends Component { | ||
constructor( props ) { | ||
super( ...arguments ); | ||
|
||
this.proxyProps = {}; | ||
this.setProxyProps( props ); | ||
} | ||
|
||
componentDidUpdate() { | ||
this.setProxyProps( this.props ); | ||
} | ||
|
||
proxyDispatch( propName, ...args ) { | ||
// Original dispatcher is a pre-bound (dispatching) action creator. | ||
mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps )[ propName ]( ...args ); | ||
} | ||
|
||
setProxyProps( props ) { | ||
// Assign as instance property so that in reconciling subsequent | ||
// renders, the assigned prop values are referentially equal. | ||
const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps ); | ||
this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => { | ||
// 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 } />; | ||
} | ||
} | ||
|
||
return ( ownProps ) => ( | ||
<RegistryConsumer> | ||
{ ( registry ) => ( | ||
<ComponentWithDispatch | ||
ownProps={ ownProps } | ||
registry={ registry } | ||
/> | ||
) } | ||
</RegistryConsumer> | ||
); | ||
}, | ||
] ), | ||
'withDispatch' | ||
); | ||
|
||
export default withDispatch; |
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,64 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import TestRenderer from 'react-test-renderer'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import withDispatch from '../'; | ||
import { createRegistry } from '../../../registry'; | ||
import RegistryProvider from '../../registry-provider'; | ||
|
||
describe( 'withDispatch', () => { | ||
let registry; | ||
beforeEach( () => { | ||
registry = createRegistry(); | ||
} ); | ||
|
||
it( 'passes the relevant data to the component', () => { | ||
const store = registry.registerStore( 'counter', { | ||
reducer: ( state = 0, action ) => { | ||
if ( action.type === 'increment' ) { | ||
return state + action.count; | ||
} | ||
return state; | ||
}, | ||
actions: { | ||
increment: ( count = 1 ) => ( { type: 'increment', count } ), | ||
}, | ||
} ); | ||
|
||
const Component = withDispatch( ( _dispatch, ownProps ) => { | ||
const { count } = ownProps; | ||
|
||
return { | ||
increment: () => _dispatch( 'counter' ).increment( count ), | ||
}; | ||
} )( ( props ) => <button onClick={ props.increment } /> ); | ||
|
||
const testRenderer = TestRenderer.create( | ||
<RegistryProvider value={ registry }> | ||
<Component count={ 0 } /> | ||
</RegistryProvider> | ||
); | ||
const testInstance = testRenderer.root; | ||
|
||
const incrementBeforeSetProps = testInstance.findByType( 'button' ).props.onClick; | ||
|
||
// Verify that dispatch respects props at the time of being invoked by | ||
// changing props after the initial mount. | ||
testRenderer.update( | ||
<RegistryProvider value={ registry }> | ||
<Component count={ 2 } /> | ||
</RegistryProvider> | ||
); | ||
|
||
// Function value reference should not have changed in props update. | ||
expect( testInstance.findByType( 'button' ).props.onClick ).toBe( incrementBeforeSetProps ); | ||
|
||
incrementBeforeSetProps(); | ||
|
||
expect( store.getState() ).toBe( 2 ); | ||
} ); | ||
} ); |
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 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
Component, | ||
createHigherOrderComponent, | ||
} from '@wordpress/element'; | ||
import isShallowEqual from '@wordpress/is-shallow-equal'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { RegistryConsumer } from '../registry-provider'; | ||
|
||
/** | ||
* Higher-order component used to inject state-derived props using registered | ||
* selectors. | ||
* | ||
* @param {Function} mapStateToProps Function called on every state change, | ||
* expected to return object of props to | ||
* merge with the component's own props. | ||
* | ||
* @return {Component} Enhanced component with merged state data props. | ||
*/ | ||
const withSelect = ( mapStateToProps ) => createHigherOrderComponent( ( WrappedComponent ) => { | ||
/** | ||
* Default merge props. A constant value is used as the fallback since it | ||
* can be more efficiently shallow compared in case component is repeatedly | ||
* rendered without its own merge props. | ||
* | ||
* @type {Object} | ||
*/ | ||
const DEFAULT_MERGE_PROPS = {}; | ||
|
||
/** | ||
* Given a props object, returns the next merge props by mapStateToProps. | ||
* | ||
* @param {Object} props Props to pass as argument to mapStateToProps. | ||
* | ||
* @return {Object} Props to merge into rendered wrapped element. | ||
*/ | ||
function getNextMergeProps( props ) { | ||
return ( | ||
mapStateToProps( props.registry.select, props.ownProps ) || | ||
DEFAULT_MERGE_PROPS | ||
); | ||
} | ||
|
||
class ComponentWithSelect extends Component { | ||
constructor( props ) { | ||
super( props ); | ||
|
||
this.subscribe(); | ||
|
||
this.mergeProps = getNextMergeProps( props ); | ||
} | ||
|
||
componentDidMount() { | ||
this.canRunSelection = true; | ||
} | ||
|
||
componentWillUnmount() { | ||
this.canRunSelection = false; | ||
this.unsubscribe(); | ||
} | ||
|
||
shouldComponentUpdate( nextProps, nextState ) { | ||
const hasPropsChanged = ! isShallowEqual( this.props.ownProps, nextProps.ownProps ); | ||
|
||
// Only render if props have changed or merge props have been updated | ||
// from the store subscriber. | ||
if ( this.state === nextState && ! hasPropsChanged ) { | ||
return false; | ||
} | ||
|
||
// If merge props change as a result of the incoming props, they | ||
// should be reflected as such in the upcoming render. | ||
if ( hasPropsChanged ) { | ||
const nextMergeProps = getNextMergeProps( nextProps ); | ||
if ( ! isShallowEqual( this.mergeProps, nextMergeProps ) ) { | ||
// Side effects are typically discouraged in lifecycle methods, but | ||
// this component is heavily used and this is the most performant | ||
// code we've found thus far. | ||
// Prior efforts to use `getDerivedStateFromProps` have demonstrated | ||
// miserable performance. | ||
this.mergeProps = nextMergeProps; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
subscribe() { | ||
const { subscribe } = this.props.registry; | ||
|
||
this.unsubscribe = subscribe( () => { | ||
if ( ! this.canRunSelection ) { | ||
return; | ||
} | ||
|
||
const nextMergeProps = getNextMergeProps( this.props ); | ||
if ( isShallowEqual( this.mergeProps, nextMergeProps ) ) { | ||
return; | ||
} | ||
|
||
this.mergeProps = nextMergeProps; | ||
|
||
// Schedule an update. Merge props are not assigned to state | ||
// because derivation of merge props from incoming props occurs | ||
// within shouldComponentUpdate, where setState is not allowed. | ||
// setState is used here instead of forceUpdate because forceUpdate | ||
// bypasses shouldComponentUpdate altogether, which isn't desireable | ||
// if both state and props change within the same render. | ||
// Unfortunately this requires that next merge props are generated | ||
// twice. | ||
this.setState( {} ); | ||
} ); | ||
} | ||
|
||
render() { | ||
return <WrappedComponent { ...this.props.ownProps } { ...this.mergeProps } />; | ||
} | ||
} | ||
|
||
return ( ownProps ) => ( | ||
<RegistryConsumer> | ||
{ ( registry ) => ( | ||
<ComponentWithSelect | ||
ownProps={ ownProps } | ||
registry={ registry } | ||
/> | ||
) } | ||
</RegistryConsumer> | ||
); | ||
}, 'withSelect' ); | ||
|
||
export default withSelect; |
Oops, something went wrong.