-
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.
Data Module: Introduce the "registry" concept (#7527)
* Data: Move registry into own file * Export the "createRegistry" function * Adding remountOnPropChange HoC
- Loading branch information
1 parent
2d08751
commit 5fb24e4
Showing
14 changed files
with
1,800 additions
and
1,501 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,42 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { createHigherOrderComponent, Component } from '@wordpress/element'; | ||
|
||
/** | ||
* Higher-order component creator, creating a new component that remounts | ||
* the wrapped component each time a given prop value changes. | ||
* | ||
* @param {string} propName Prop name to monitor. | ||
* | ||
* @return {Function} Higher-order component. | ||
*/ | ||
const remountOnPropChange = ( propName ) => createHigherOrderComponent( | ||
( WrappedComponent ) => class extends Component { | ||
constructor( props ) { | ||
super( ...arguments ); | ||
this.state = { | ||
propChangeId: 0, | ||
propValue: props[ propName ], | ||
}; | ||
} | ||
|
||
static getDerivedStateFromProps( props, state ) { | ||
if ( props[ propName ] === state.propValue ) { | ||
return null; | ||
} | ||
|
||
return { | ||
propChangeId: state.propChangeId + 1, | ||
propValue: props[ propName ], | ||
}; | ||
} | ||
|
||
render() { | ||
return <WrappedComponent key={ this.state.propChangeId } { ...this.props } />; | ||
} | ||
}, | ||
'remountOnPropChange' | ||
); | ||
|
||
export default remountOnPropChange; |
71 changes: 71 additions & 0 deletions
71
packages/data/src/components/remountOnPropChange/test/index.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 TestRenderer from 'react-test-renderer'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Component } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import remountOnPropChange from '../'; | ||
|
||
describe( 'remountOnPropChange', () => { | ||
let count = 0; | ||
class MountCounter extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.state = { | ||
count: 0, | ||
}; | ||
} | ||
|
||
componentDidMount() { | ||
count++; | ||
this.setState( { | ||
count: count, | ||
} ); | ||
} | ||
|
||
render() { | ||
return this.state.count; | ||
} | ||
} | ||
|
||
beforeEach( () => { | ||
count = 0; | ||
} ); | ||
|
||
it( 'Should not remount the inner component if the prop value doesn\'t change', () => { | ||
const Wrapped = remountOnPropChange( 'monitor' )( MountCounter ); | ||
const testRenderer = TestRenderer.create( | ||
<Wrapped monitor="unchanged" other="1" /> | ||
); | ||
|
||
expect( testRenderer.toJSON() ).toBe( '1' ); | ||
|
||
// Changing an unmonitored prop | ||
testRenderer.update( | ||
<Wrapped monitor="unchanged" other="2" /> | ||
); | ||
expect( testRenderer.toJSON() ).toBe( '1' ); | ||
} ); | ||
|
||
it( 'Should remount the inner component if the prop value changes', () => { | ||
const Wrapped = remountOnPropChange( 'monitor' )( MountCounter ); | ||
const testRenderer = TestRenderer.create( | ||
<Wrapped monitor="initial" /> | ||
); | ||
|
||
expect( testRenderer.toJSON() ).toBe( '1' ); | ||
|
||
// Changing an the monitored prop remounts the component | ||
testRenderer.update( | ||
<Wrapped monitor="updated" /> | ||
); | ||
expect( testRenderer.toJSON() ).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,91 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { mapValues } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { | ||
Component, | ||
compose, | ||
createHigherOrderComponent, | ||
pure, | ||
} from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import remountOnPropChange from '../remountOnPropChange'; | ||
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 ) => { | ||
const ComponentWithDispatch = remountOnPropChange( 'registry' )( class 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 ); | ||
} ); | ||
} ); |
Oops, something went wrong.