Skip to content

Commit

Permalink
Data: Move registry into own file
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Jul 11, 2018
1 parent f3b6379 commit 77f19ca
Show file tree
Hide file tree
Showing 13 changed files with 1,688 additions and 1,508 deletions.
15 changes: 15 additions & 0 deletions packages/data/src/components/registry-provider/index.js
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;
90 changes: 90 additions & 0 deletions packages/data/src/components/with-dispatch/index.js
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;
64 changes: 64 additions & 0 deletions packages/data/src/components/with-dispatch/test/index.js
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 );
} );
} );
137 changes: 137 additions & 0 deletions packages/data/src/components/with-select/index.js
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;
Loading

0 comments on commit 77f19ca

Please sign in to comment.