Skip to content

Commit

Permalink
Data: Refactor and optimize withSelect, withDispatch handling of regi…
Browse files Browse the repository at this point in the history
…stry change (#11027)

* Data: Remove redundant remountOnPropChange from withDispatch

* Data: Use internal key generation utility for withSelect

* Compose: Deprecate remountOnPropChange

* Data: Handle registry change within withSelect
  • Loading branch information
aduth authored Oct 26, 2018
1 parent e8de101 commit 1e53293
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 30 deletions.
8 changes: 5 additions & 3 deletions docs/reference/deprecated.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
Gutenberg's deprecation policy is intended to support backwards-compatibility for releases, when possible. The current deprecations are listed below and are grouped by _the version at which they will be removed completely_. If your plugin depends on these behaviors, you must update to the recommended alternative before the noted version.

# 4.4.0
- `wp.date.getSettings` has been removed. Please use `wp.date.__experimentalGetSettings` instead.
## 4.4.0

# 4.3.0
- `wp.date.getSettings` has been removed. Please use `wp.date.__experimentalGetSettings` instead.
- `wp.compose.remountOnPropChange` has been removed.

## 4.3.0

- `isEditorSidebarPanelOpened` selector (`core/edit-post`) has been removed. Please use `isEditorPanelEnabled` instead.
- `toggleGeneralSidebarEditorPanel` action (`core/edit-post`) has been removed. Please use `toggleEditorPanelOpened` instead.
Expand Down
8 changes: 7 additions & 1 deletion lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,13 @@ function gutenberg_register_scripts_and_styles() {
gutenberg_override_script(
'wp-compose',
gutenberg_url( 'build/compose/index.js' ),
array( 'lodash', 'wp-element', 'wp-is-shallow-equal', 'wp-polyfill' ),
array(
'lodash',
'wp-deprecated',
'wp-element',
'wp-is-shallow-equal',
'wp-polyfill',
),
filemtime( gutenberg_dir_path() . 'build/compose/index.js' ),
true
);
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/compose/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.1.0 (Unreleased)

### Deprecation

- `remountOnPropChange` has been deprecated.

## 2.0.5 (2018-10-19)

## 2.0.4 (2018-10-18)
Expand Down
1 change: 1 addition & 0 deletions packages/compose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"react-native": "src/index",
"dependencies": {
"@babel/runtime": "^7.0.0",
"@wordpress/deprecated": "file:../deprecated",
"@wordpress/element": "file:../element",
"@wordpress/is-shallow-equal": "file:../is-shallow-equal",
"lodash": "^4.17.10"
Expand Down
15 changes: 10 additions & 5 deletions packages/compose/src/remount-on-prop-change/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
Expand All @@ -16,8 +17,13 @@ import createHigherOrderComponent from '../create-higher-order-component';
*
* @return {Function} Higher-order component.
*/
const remountOnPropChange = ( propName ) => createHigherOrderComponent(
( WrappedComponent ) => class extends Component {
const remountOnPropChange = ( propName ) => createHigherOrderComponent( ( WrappedComponent ) => {
deprecated( 'remountOnPropChange', {
plugin: 'Gutenberg',
version: '4.4.0',
} );

return class extends Component {
constructor( props ) {
super( ...arguments );
this.state = {
Expand All @@ -40,8 +46,7 @@ const remountOnPropChange = ( propName ) => createHigherOrderComponent(
render() {
return <WrappedComponent key={ this.state.propChangeId } { ...this.props } />;
}
},
'remountOnPropChange'
);
};
}, 'remountOnPropChange' );

export default remountOnPropChange;
7 changes: 7 additions & 0 deletions packages/compose/src/remount-on-prop-change/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import TestRenderer from 'react-test-renderer';
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
*/
import remountOnPropChange from '../';

jest.mock( '@wordpress/deprecated', () => jest.fn() );

describe( 'remountOnPropChange', () => {
let count = 0;
class MountCounter extends Component {
Expand Down Expand Up @@ -52,6 +55,8 @@ describe( 'remountOnPropChange', () => {
<Wrapped monitor="unchanged" other="2" />
);
expect( testRenderer.toJSON() ).toBe( '1' );

expect( deprecated ).toHaveBeenCalled();
} );

it( 'Should remount the inner component if the prop value changes', () => {
Expand All @@ -67,5 +72,7 @@ describe( 'remountOnPropChange', () => {
<Wrapped monitor="updated" />
);
expect( testRenderer.toJSON() ).toBe( '2' );

expect( deprecated ).toHaveBeenCalled();
} );
} );
14 changes: 9 additions & 5 deletions packages/data/src/components/with-dispatch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { mapValues } from 'lodash';
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { pure, compose, remountOnPropChange, createHigherOrderComponent } from '@wordpress/compose';
import { pure, compose, createHigherOrderComponent } from '@wordpress/compose';

/**
* Internal dependencies
Expand All @@ -30,7 +30,7 @@ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent(
compose( [
pure,
( WrappedComponent ) => {
const ComponentWithDispatch = remountOnPropChange( 'registry' )( class extends Component {
class ComponentWithDispatch extends Component {
constructor( props ) {
super( ...arguments );

Expand All @@ -48,8 +48,12 @@ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent(
}

setProxyProps( props ) {
// Assign as instance property so that in reconciling subsequent
// renders, the assigned prop values are referentially equal.
// 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.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => {
// Prebind with prop name so we have reference to the original
Expand All @@ -66,7 +70,7 @@ const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent(
render() {
return <WrappedComponent { ...this.props.ownProps } { ...this.proxyProps } />;
}
} );
}

return ( ownProps ) => (
<RegistryConsumer>
Expand Down
55 changes: 55 additions & 0 deletions packages/data/src/components/with-dispatch/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,59 @@ describe( 'withDispatch', () => {

expect( store.getState() ).toBe( 2 );
} );

it( 'calls dispatch on the correct registry if updated', () => {
const reducer = ( state = null ) => 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 Component = withDispatch( ( _dispatch ) => {
const noopByReference = _dispatch( 'demo' ).noop;

return {
noop() {
_dispatch( 'demo' ).noop();
noopByReference();
},
};
} )( ( props ) => <button onClick={ props.noop } /> );

const testRenderer = TestRenderer.create(
<RegistryProvider value={ firstRegistry }>
<Component />
</RegistryProvider>
);
const testInstance = testRenderer.root;

testInstance.findByType( 'button' ).props.onClick();
expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 );
expect( secondRegistryAction ).toHaveBeenCalledTimes( 0 );

const secondRegistry = createRegistry();
secondRegistry.registerStore( 'demo', {
reducer,
actions: {
noop: secondRegistryAction,
},
} );

testRenderer.update(
<RegistryProvider value={ secondRegistry }>
<Component />
</RegistryProvider>
);

testInstance.findByType( 'button' ).props.onClick();
expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 );
expect( secondRegistryAction ).toHaveBeenCalledTimes( 2 );
} );
} );
45 changes: 29 additions & 16 deletions packages/data/src/components/with-select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { Component } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { remountOnPropChange, createHigherOrderComponent } from '@wordpress/compose';
import { createHigherOrderComponent } from '@wordpress/compose';

/**
* Internal dependencies
Expand Down Expand Up @@ -44,11 +44,11 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
);
}

const ComponentWithSelect = remountOnPropChange( 'registry' )( class extends Component {
class ComponentWithSelect extends Component {
constructor( props ) {
super( props );

this.subscribe();
this.subscribe( props.registry );

this.mergeProps = getNextMergeProps( props );
}
Expand All @@ -63,35 +63,48 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
}

shouldComponentUpdate( nextProps, nextState ) {
const hasPropsChanged = ! isShallowEqual( this.props.ownProps, nextProps.ownProps );
// Cycle subscription if registry changes.
const hasRegistryChanged = nextProps.registry !== this.props.registry;
if ( hasRegistryChanged ) {
this.unsubscribe();
this.subscribe( nextProps.registry );
}

// Treat a registry change as equivalent to `ownProps`, to reflect
// `mergeProps` to rendered component if and only if updated.
const hasPropsChanged = (
hasRegistryChanged ||
! 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.
// If merge props change as a result of the incoming props,
// they should be reflected as such in the upcoming render.
// While side effects are discouraged in lifecycle methods,
// this component is used heavily, and prior efforts to use
// `getDerivedStateFromProps` had demonstrated miserable
// performance.
this.mergeProps = nextMergeProps;
}

// Regardless whether merge props are changing, fall through to
// incur the render since the component will need to receive
// the changed `ownProps`.
}

return true;
}

subscribe() {
const { subscribe } = this.props.registry;

this.unsubscribe = subscribe( () => {
subscribe( registry ) {
this.unsubscribe = registry.subscribe( () => {
if ( ! this.canRunSelection ) {
return;
}
Expand All @@ -118,7 +131,7 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
render() {
return <WrappedComponent { ...this.props.ownProps } { ...this.mergeProps } />;
}
} );
}

return ( ownProps ) => (
<RegistryConsumer>
Expand Down
55 changes: 55 additions & 0 deletions packages/data/src/components/with-select/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,59 @@ describe( 'withSelect', () => {
expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 );
expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 2 );
} );

it( 'should rerun selection on registry change', () => {
const firstRegistry = registry;
firstRegistry.registerStore( 'demo', {
reducer: ( state = 'first' ) => state,
selectors: {
getValue: ( state ) => state,
},
} );

const mapSelectToProps = jest.fn().mockImplementation( ( _select ) => ( {
value: _select( 'demo' ).getValue(),
} ) );

const OriginalComponent = jest.fn().mockImplementation( ( props ) => (
<div>{ props.value }</div>
) );

const Component = withSelect( mapSelectToProps )( OriginalComponent );

const testRenderer = TestRenderer.create(
<RegistryProvider value={ firstRegistry }>
<Component />
</RegistryProvider>
);
const testInstance = testRenderer.root;

expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 );
expect( OriginalComponent ).toHaveBeenCalledTimes( 1 );

expect( testInstance.findByType( 'div' ).props ).toEqual( {
children: 'first',
} );

const secondRegistry = createRegistry();
secondRegistry.registerStore( 'demo', {
reducer: ( state = 'second' ) => state,
selectors: {
getValue: ( state ) => state,
},
} );

testRenderer.update(
<RegistryProvider value={ secondRegistry }>
<Component />
</RegistryProvider>
);

expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 );
expect( OriginalComponent ).toHaveBeenCalledTimes( 2 );

expect( testInstance.findByType( 'div' ).props ).toEqual( {
children: 'second',
} );
} );
} );

0 comments on commit 1e53293

Please sign in to comment.