Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 3 additions & 45 deletions packages/data/src/components/with-dispatch/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
/**
* External dependencies
*/
import { mapValues } from 'lodash';

/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { pure, compose, createHigherOrderComponent } from '@wordpress/compose';

/**
Expand All @@ -30,46 +24,10 @@ 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 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
// 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 );
} );
}
function ComponentWithDispatch( { registry, ownProps } ) {
const mergeProps = mapDispatchToProps( registry.dispatch, ownProps );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any concerns about the new props we keep creating with this? (new instances of functions on each call?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any concerns about the new props we keep creating with this? (new instances of functions on each call?)

Yep, it's a trade-off between that and iterating to create proxyProps. I think there's a middle-ground option where we use static getDerivedStateFromProps and still defer to a proxied implementation. The proxying in general adds a bit of complexity, and in the end I'm not sure if it's saving us much in the end.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(FYI this is also expanded upon in the original comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, it's your call, you're more aware of the details of these HoC than anyone else. My concern would be for a component like block-list/block where we'd rerender on each subscribe because of the props created on each subscribe, but maybe we don't even hit withDispatch because we don't receive new props?


render() {
return <WrappedComponent { ...this.props.ownProps } { ...this.proxyProps } />;
}
return <WrappedComponent { ...ownProps } { ...mergeProps } />;
}

return ( ownProps ) => (
Expand Down
34 changes: 26 additions & 8 deletions packages/data/src/components/with-dispatch/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import TestRenderer from 'react-test-renderer';
import { omit } from 'lodash';

/**
* Internal dependencies
Expand Down Expand Up @@ -47,20 +48,13 @@ describe( 'withDispatch', () => {
);
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();
testInstance.findByType( 'button' ).props.onClick();

expect( store.getState() ).toBe( 2 );
} );
Expand Down Expand Up @@ -119,4 +113,28 @@ describe( 'withDispatch', () => {
expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 );
expect( secondRegistryAction ).toHaveBeenCalledTimes( 2 );
} );

it( 'passes new merge props if mapDispatchToProps varies on ownProps', () => {
const Component = withDispatch( ( _dispatch, ownProps ) => {
return { [ ownProps.propName ]() {} };
} )( ( props ) => <button { ...omit( props, 'propName' ) } /> );

const testRenderer = TestRenderer.create(
<RegistryProvider value={ registry }>
<Component propName="onClick" />
</RegistryProvider>
);

const instance = testRenderer.root;

expect( Object.keys( instance.findByType( 'button' ).props ) ).toEqual( [ 'onClick' ] );

testRenderer.update(
<RegistryProvider value={ registry }>
<Component propName="onKeyDown" />
</RegistryProvider>
);

expect( Object.keys( instance.findByType( 'button' ).props ) ).toEqual( [ 'onKeyDown' ] );
} );
} );