-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Framework: Support registering and invoking actions in the data module #5137
Changes from all commits
4260a57
4bef28e
b49f61b
802b960
6521466
34b7e21
a1805f1
c6846f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,15 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { connect } from 'react-redux'; | ||
import isEqualShallow from 'is-equal-shallow'; | ||
import { createStore } from 'redux'; | ||
import { flowRight, without, mapValues } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { deprecated } from '@wordpress/utils'; | ||
import { Component, getWrapperDisplayName } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
|
@@ -20,6 +21,7 @@ export { loadAndPersist, withRehydratation } from './persist'; | |
*/ | ||
const stores = {}; | ||
const selectors = {}; | ||
const actions = {}; | ||
let listeners = []; | ||
|
||
/** | ||
|
@@ -29,21 +31,6 @@ export function globalListener() { | |
listeners.forEach( listener => listener() ); | ||
} | ||
|
||
/** | ||
* Subscribe to changes to any data. | ||
* | ||
* @param {Function} listener Listener function. | ||
* | ||
* @return {Function} Unsubscribe function. | ||
*/ | ||
export const subscribe = ( listener ) => { | ||
listeners.push( listener ); | ||
|
||
return () => { | ||
listeners = without( listeners, listener ); | ||
}; | ||
}; | ||
|
||
/** | ||
* Registers a new sub-reducer to the global state and returns a Redux-like store object. | ||
* | ||
|
@@ -79,6 +66,34 @@ export function registerSelectors( reducerKey, newSelectors ) { | |
selectors[ reducerKey ] = mapValues( newSelectors, createStateSelector ); | ||
} | ||
|
||
/** | ||
* Registers actions for external usage. | ||
* | ||
* @param {string} reducerKey Part of the state shape to register the | ||
* selectors for. | ||
* @param {Object} newActions Actions to register. | ||
*/ | ||
export function registerActions( reducerKey, newActions ) { | ||
const store = stores[ reducerKey ]; | ||
const createBoundAction = ( action ) => ( ...args ) => store.dispatch( action( ...args ) ); | ||
actions[ reducerKey ] = mapValues( newActions, createBoundAction ); | ||
} | ||
|
||
/** | ||
* Subscribe to changes to any data. | ||
* | ||
* @param {Function} listener Listener function. | ||
* | ||
* @return {Function} Unsubscribe function. | ||
*/ | ||
export const subscribe = ( listener ) => { | ||
listeners.push( listener ); | ||
|
||
return () => { | ||
listeners = without( listeners, listener ); | ||
}; | ||
}; | ||
|
||
/** | ||
* Calls a selector given the current state and extra arguments. | ||
* | ||
|
@@ -102,33 +117,142 @@ export function select( reducerKey ) { | |
} | ||
|
||
/** | ||
* Higher Order Component used to inject data using the registered selectors. | ||
* Returns the available actions for a part of the state. | ||
* | ||
* @param {Function} mapSelectorsToProps Gets called with the selectors object | ||
* to determine the data for the | ||
* component. | ||
* @param {string} reducerKey Part of the state shape to dispatch the | ||
* action for. | ||
* | ||
* @return {Function} Renders the wrapped component and passes it data. | ||
* @return {*} The action's returned value. | ||
*/ | ||
export const query = ( mapSelectorsToProps ) => ( WrappedComponent ) => { | ||
const store = { | ||
getState() { | ||
return mapValues( stores, subStore => subStore.getState() ); | ||
}, | ||
subscribe, | ||
dispatch() { | ||
// eslint-disable-next-line no-console | ||
console.warn( 'Dispatch is not supported.' ); | ||
}, | ||
}; | ||
const connectWithStore = ( ...args ) => { | ||
const ConnectedWrappedComponent = connect( ...args )( WrappedComponent ); | ||
return ( props ) => { | ||
return <ConnectedWrappedComponent { ...props } store={ store } />; | ||
}; | ||
}; | ||
export function dispatch( reducerKey ) { | ||
return actions[ reducerKey ]; | ||
} | ||
|
||
/** | ||
* 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. | ||
*/ | ||
export const withSelect = ( mapStateToProps ) => ( WrappedComponent ) => { | ||
class ComponentWithSelect extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.runSelection = this.runSelection.bind( this ); | ||
|
||
this.state = {}; | ||
} | ||
|
||
componentWillMount() { | ||
this.subscribe(); | ||
|
||
// Populate initial state. | ||
this.runSelection(); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think it would be I'd also also it would only need to be run if the props have changed. |
||
|
||
componentWillReceiveProps( nextProps ) { | ||
if ( ! isEqualShallow( nextProps, this.props ) ) { | ||
this.runSelection( nextProps ); | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
this.unsubscribe(); | ||
} | ||
|
||
subscribe() { | ||
this.unsubscribe = subscribe( this.runSelection ); | ||
} | ||
|
||
runSelection( props = this.props ) { | ||
const newState = mapStateToProps( select, props ); | ||
if ( ! isEqualShallow( newState, this.state ) ) { | ||
this.setState( newState ); | ||
} | ||
} | ||
|
||
render() { | ||
return <WrappedComponent { ...this.props } { ...this.state } />; | ||
} | ||
} | ||
|
||
ComponentWithSelect.displayName = getWrapperDisplayName( WrappedComponent, 'select' ); | ||
|
||
return ComponentWithSelect; | ||
}; | ||
|
||
/** | ||
* 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. | ||
*/ | ||
export const withDispatch = ( mapDispatchToProps ) => ( WrappedComponent ) => { | ||
class ComponentWithDispatch extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.proxyProps = {}; | ||
} | ||
|
||
componentWillMount() { | ||
this.setProxyProps( this.props ); | ||
} | ||
|
||
componentWillUpdate( nextProps ) { | ||
this.setProxyProps( nextProps ); | ||
} | ||
|
||
proxyDispatch( propName, ...args ) { | ||
// Original dispatcher is a pre-bound (dispatching) action creator. | ||
mapDispatchToProps( dispatch, this.props )[ propName ]( ...args ); | ||
} | ||
|
||
setProxyProps( props ) { | ||
// Assign as instance property so that in reconciling subsequent | ||
// renders, the assigned prop values are referentially equal. | ||
const propsToDispatchers = mapDispatchToProps( dispatch, props ); | ||
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 } { ...this.proxyProps } />; | ||
} | ||
} | ||
|
||
ComponentWithDispatch.displayName = getWrapperDisplayName( WrappedComponent, 'dispatch' ); | ||
|
||
return ComponentWithDispatch; | ||
}; | ||
|
||
export const query = ( mapSelectToProps ) => { | ||
deprecated( 'wp.data.query', { | ||
version: '2.5', | ||
alternative: 'wp.data.withSelect', | ||
plugin: 'Gutenberg', | ||
} ); | ||
|
||
return connectWithStore( ( state, ownProps ) => { | ||
return mapSelectorsToProps( select, ownProps ); | ||
return withSelect( ( props ) => { | ||
return mapSelectToProps( select, props ); | ||
} ); | ||
}; |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the moment every
registerActions
call overwrites stored action for the given reducer key. Should we prevent calling this method more than once or allow to merge actions? The same applies toregisterSelectors
andregisterReducer
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I said above, we could consolidate all these three functions with a single
registerState