From c65f45fb7c3a8e43c4e634cf607771c30dc2cb01 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Sat, 25 May 2019 15:10:32 -0400 Subject: [PATCH] code cleanup and add documentation --- packages/data/README.md | 167 +++++++++++++- .../components/registry-provider/context.js | 37 ++++ .../registry-provider/use-registry.js | 39 ++++ .../data/src/components/use-select/index.js | 43 ++++ .../data/src/components/with-select/index.js | 66 +++++- .../src/components/with-select/test/index.js | 2 +- .../components/with-select/with-select-new.js | 27 --- .../components/with-select/with-select-old.js | 208 ------------------ packages/data/src/index.js | 10 +- 9 files changed, 354 insertions(+), 245 deletions(-) delete mode 100644 packages/data/src/components/with-select/with-select-new.js delete mode 100644 packages/data/src/components/with-select/with-select-old.js diff --git a/packages/data/README.md b/packages/data/README.md index 83aeb72e4513e..8984e86b1c577 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -379,11 +379,43 @@ _Returns_ # **RegistryConsumer** -Undocumented declaration. +A custom react Context consumer exposing the provided `registry` to +children components. Used along with the RegistryProvider. + +You can read more about the react context api here: + + +_Usage_ + +````js +const { + RegistryProvider, + RegistryConsumer, + createRegistry +} = wp.data; + +const registry = createRegistry( {} ); + +const App = ( { props } ) => { + return +
Hello There
+ + { ( registry ) => ( + +
+} # **RegistryProvider** -Undocumented declaration. +A custom Context provider for exposing the provided `registry` to children +components via a consumer. + +See RegistryConsumer documentation for +example. # **select** @@ -391,13 +423,13 @@ Given the name of a registered store, returns an object of the store's selectors The selector functions are been pre-bound to pass the current state automatically. As a consumer, you need only pass arguments of the selector, if applicable. -_Usage_ +*Usage* ```js const { select } = wp.data; select( 'my-shop' ).getPrice( 'hammer' ); -``` +```` _Parameters_ @@ -435,6 +467,91 @@ _Parameters_ Undocumented declaration. +# **useRegistry** + +A custom react hook exposing the registry context for use. + +This exposes the `registry` value provided via the +Registry Provider to a component implementing +this hook. + +It acts similarly to the `useContext` react hook. + +Note: Generally speaking, `useRegistry` is a low level hook that in most cases +won't be needed for implementation. Most interactions with the wp.data api +can be performed via the `useSelect` hook, or the `withSelect` and +`withDispatch` higher order components. + +_Usage_ + +```js +const { + RegistryProvider, + createRegistry, + useRegistry, +} = wp.data + +const registry = createRegistry( {} ); + +const SomeChildUsingRegistry = ( props ) => { + const registry = useRegistry( registry ); + // ...logic implementing the registry in other react hooks. +}; + + +const ParentProvidingRegistry = ( props ) => { + return + + +}; +``` + +_Returns_ + +- `Function`: A custom react hook exposing the registry context value. + +# **useSelect** + +Custom react hook for retrieving props from registered selectors. + +In general, this custom React hook follows the +[rules of hooks](https://reactjs.org/docs/hooks-rules.html). + +_Usage_ + +```js +const { useSelect } = wp.data; + +function HammerPriceDisplay( { currency } ) { + const price = useSelect( ( select ) => { + return select( 'my-shop' ).getPrice( 'hammer', currency ) + }, [ currency ] ); + return new Intl.NumberFormat( 'en-US', { + style: 'currency', + currency, + } ).format( price ); +} + +// Rendered in the application: +// +``` + +In the above example, when `HammerPriceDisplay` is rendered into an +application, the price will be retrieved from the store state using the +`mapSelect` callback on `useSelect`. If the currency prop changes then +any price in the state for that currency is retrieved. If the currency prop +doesn't change and other props are passed in that do change, the price will +not change because the dependency is just the currency. + +_Parameters_ + +- _\_mapSelect_ `Function`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the `registry.select` method on the first argument and the `registry` on the second argument. +- _deps_ `Array`: If provided, this memoizes the mapSelect so the same `mapSelect` is invoked on every state change unless the dependencies change. + +_Returns_ + +- `Function`: A custom react hook. + # **withDispatch** Higher-order component used to add dispatch props using registered action creators. @@ -516,7 +633,47 @@ _Returns_ # **withSelect** -Undocumented declaration. +Higher-order component used to inject state-derived props using registered +selectors. + +_Usage_ + +```js +function PriceDisplay( { price, currency } ) { + return new Intl.NumberFormat( 'en-US', { + style: 'currency', + currency, + } ).format( price ); +} + +const { withSelect } = wp.data; + +const HammerPriceDisplay = withSelect( ( select, ownProps ) => { + const { getPrice } = select( 'my-shop' ); + const { currency } = ownProps; + + return { + price: getPrice( 'hammer', currency ), + }; +} )( PriceDisplay ); + +// Rendered in the application: +// +// +``` + +In the above example, when `HammerPriceDisplay` is rendered into an +application, it will pass the price into the underlying `PriceDisplay` +component and update automatically if the price of a hammer ever changes in +the store. + +_Parameters_ + +- _mapSelectToProps_ `Function`: Function called on every state change, expected to return object of props to merge with the component's own props. + +_Returns_ + +- `Component`: Enhanced component with merged state data props. diff --git a/packages/data/src/components/registry-provider/context.js b/packages/data/src/components/registry-provider/context.js index 825b89ab9fcc5..a1fe1a9e56183 100644 --- a/packages/data/src/components/registry-provider/context.js +++ b/packages/data/src/components/registry-provider/context.js @@ -12,6 +12,43 @@ export const Context = createContext( defaultRegistry ); const { Consumer, Provider } = Context; +/** + * A custom react Context consumer exposing the provided `registry` to + * children components. Used along with the RegistryProvider. + * + * You can read more about the react context api here: + * https://reactjs.org/docs/context.html#contextprovider + * + * @example + * ```js + * const { + * RegistryProvider, + * RegistryConsumer, + * createRegistry + * } = wp.data; + * + * const registry = createRegistry( {} ); + * + * const App = ( { props } ) => { + * return + *
Hello There
+ * + * { ( registry ) => ( + * + *
+ * } + */ export const RegistryConsumer = Consumer; +/** + * A custom Context provider for exposing the provided `registry` to children + * components via a consumer. + * + * See RegistryConsumer documentation for + * example. + */ export default Provider; diff --git a/packages/data/src/components/registry-provider/use-registry.js b/packages/data/src/components/registry-provider/use-registry.js index 253737a14782e..846cb5627fd2f 100644 --- a/packages/data/src/components/registry-provider/use-registry.js +++ b/packages/data/src/components/registry-provider/use-registry.js @@ -8,6 +8,45 @@ import { useContext } from '@wordpress/element'; */ import { Context } from './context'; +/** + * A custom react hook exposing the registry context for use. + * + * This exposes the `registry` value provided via the + * Registry Provider to a component implementing + * this hook. + * + * It acts similarly to the `useContext` react hook. + * + * Note: Generally speaking, `useRegistry` is a low level hook that in most cases + * won't be needed for implementation. Most interactions with the wp.data api + * can be performed via the `useSelect` hook, or the `withSelect` and + * `withDispatch` higher order components. + * + * @example + * ```js + * const { + * RegistryProvider, + * createRegistry, + * useRegistry, + * } = wp.data + * + * const registry = createRegistry( {} ); + * + * const SomeChildUsingRegistry = ( props ) => { + * const registry = useRegistry( registry ); + * // ...logic implementing the registry in other react hooks. + * }; + * + * + * const ParentProvidingRegistry = ( props ) => { + * return + * + * + * }; + * ``` + * + * @return {Function} A custom react hook exposing the registry context value. + */ export default function useRegistry() { return useContext( Context ); } diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 64b975f7baeaa..d71f72b21c917 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -31,6 +31,49 @@ const useIsomorphicLayoutEffect = const renderQueue = createQueue(); +/** + * Custom react hook for retrieving props from registered selectors. + * + * In general, this custom React hook follows the + * [rules of hooks](https://reactjs.org/docs/hooks-rules.html). + * + * @param {Function} _mapSelect Function called on every state change. The + * returned value is exposed to the component + * implementing this hook. The function receives + * the `registry.select` method on the first + * argument and the `registry` on the second + * argument. + * @param {Array} deps If provided, this memoizes the mapSelect so the + * same `mapSelect` is invoked on every state + * change unless the dependencies change. + * + * @example + * ```js + * const { useSelect } = wp.data; + * + * function HammerPriceDisplay( { currency } ) { + * const price = useSelect( ( select ) => { + * return select( 'my-shop' ).getPrice( 'hammer', currency ) + * }, [ currency ] ); + * return new Intl.NumberFormat( 'en-US', { + * style: 'currency', + * currency, + * } ).format( price ); + * } + * + * // Rendered in the application: + * // + * ``` + * + * In the above example, when `HammerPriceDisplay` is rendered into an + * application, the price will be retrieved from the store state using the + * `mapSelect` callback on `useSelect`. If the currency prop changes then + * any price in the state for that currency is retrieved. If the currency prop + * doesn't change and other props are passed in that do change, the price will + * not change because the dependency is just the currency. + * + * @return {Function} A custom react hook. + */ export default function useSelect( _mapSelect, deps ) { const mapSelect = useCallback( _mapSelect, deps ); const registry = useRegistry(); diff --git a/packages/data/src/components/with-select/index.js b/packages/data/src/components/with-select/index.js index e1d04c6b48dc2..f9659e7e865a8 100644 --- a/packages/data/src/components/with-select/index.js +++ b/packages/data/src/components/with-select/index.js @@ -1,2 +1,66 @@ -export { default as withSelect } from './with-select-new'; +/** + * WordPress dependencies + */ +import { createHigherOrderComponent, pure } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import useSelect from '../use-select'; + +/** + * Higher-order component used to inject state-derived props using registered + * selectors. + * + * @param {Function} mapSelectToProps Function called on every state change, + * expected to return object of props to + * merge with the component's own props. + * + * @example + * ```js + * function PriceDisplay( { price, currency } ) { + * return new Intl.NumberFormat( 'en-US', { + * style: 'currency', + * currency, + * } ).format( price ); + * } + * + * const { withSelect } = wp.data; + * + * const HammerPriceDisplay = withSelect( ( select, ownProps ) => { + * const { getPrice } = select( 'my-shop' ); + * const { currency } = ownProps; + * + * return { + * price: getPrice( 'hammer', currency ), + * }; + * } )( PriceDisplay ); + * + * // Rendered in the application: + * // + * // + * ``` + * In the above example, when `HammerPriceDisplay` is rendered into an + * application, it will pass the price into the underlying `PriceDisplay` + * component and update automatically if the price of a hammer ever changes in + * the store. + * + * @return {Component} Enhanced component with merged state data props. + */ +const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( + ( WrappedComponent ) => pure( + ( ownProps ) => { + const mapSelect = + ( select, registry ) => mapSelectToProps( + select, + ownProps, + registry + ); + const mergeProps = useSelect( mapSelect ); + return ; + } + ), + 'withSelect' +); + +export default withSelect; diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index 8149e661e6a48..3b2049826c63b 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -12,7 +12,7 @@ import { Component } from '@wordpress/element'; /** * Internal dependencies */ -import { withSelect } from '../'; +import withSelect from '../'; import withDispatch from '../../with-dispatch'; import { createRegistry } from '../../../registry'; import { RegistryProvider } from '../../registry-provider'; diff --git a/packages/data/src/components/with-select/with-select-new.js b/packages/data/src/components/with-select/with-select-new.js deleted file mode 100644 index 2476d7a784d88..0000000000000 --- a/packages/data/src/components/with-select/with-select-new.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * WordPress dependencies - */ -import { createHigherOrderComponent, pure } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import useSelect from '../use-select'; - -const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( - ( WrappedComponent ) => pure( - ( ownProps ) => { - const mapSelect = - ( select, registry ) => mapSelectToProps( - select, - ownProps, - registry - ); - const mergeProps = useSelect( mapSelect ); - return ; - } - ), - 'withSelect' -); - -export default withSelect; diff --git a/packages/data/src/components/with-select/with-select-old.js b/packages/data/src/components/with-select/with-select-old.js deleted file mode 100644 index d2fd4ae462b3b..0000000000000 --- a/packages/data/src/components/with-select/with-select-old.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { isShallowEqualObjects } from '@wordpress/is-shallow-equal'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { createQueue } from '@wordpress/priority-queue'; - -/** - * Internal dependencies - */ -import { RegistryConsumer } from '../registry-provider'; -import { AsyncModeConsumer } from '../async-mode-provider'; - -const renderQueue = createQueue(); - -/** - * Higher-order component used to inject state-derived props using registered - * selectors. - * - * @param {Function} mapSelectToProps Function called on every state change, - * expected to return object of props to - * merge with the component's own props. - * - * @example - * ```js - * function PriceDisplay( { price, currency } ) { - * return new Intl.NumberFormat( 'en-US', { - * style: 'currency', - * currency, - * } ).format( price ); - * } - * - * const { withSelect } = wp.data; - * - * const HammerPriceDisplay = withSelect( ( select, ownProps ) => { - * const { getPrice } = select( 'my-shop' ); - * const { currency } = ownProps; - * - * return { - * price: getPrice( 'hammer', currency ), - * }; - * } )( PriceDisplay ); - * - * // Rendered in the application: - * // - * // - * ``` - * In the above example, when `HammerPriceDisplay` is rendered into an application, it will pass the price into the underlying `PriceDisplay` component and update automatically if the price of a hammer ever changes in the store. - * - * @return {Component} Enhanced component with merged state data props. - */ -const withSelect = ( mapSelectToProps ) => 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 mapSelectToProps. - * - * @param {Object} props Props to pass as argument to mapSelectToProps. - * - * @return {Object} Props to merge into rendered wrapped element. - */ - function getNextMergeProps( props ) { - return ( - mapSelectToProps( props.registry.select, props.ownProps, props.registry ) || - DEFAULT_MERGE_PROPS - ); - } - - class ComponentWithSelect extends Component { - constructor( props ) { - super( props ); - - this.onStoreChange = this.onStoreChange.bind( this ); - - this.subscribe( props.registry ); - - this.mergeProps = getNextMergeProps( props ); - } - - componentDidMount() { - this.canRunSelection = true; - - // A state change may have occurred between the constructor and - // mount of the component (e.g. during the wrapped component's own - // constructor), in which case selection should be rerun. - if ( this.hasQueuedSelection ) { - this.hasQueuedSelection = false; - this.onStoreChange(); - } - } - - componentWillUnmount() { - this.canRunSelection = false; - this.unsubscribe(); - renderQueue.flush( this ); - } - - shouldComponentUpdate( nextProps, nextState ) { - // Cycle subscription if registry changes. - const hasRegistryChanged = nextProps.registry !== this.props.registry; - const hasSyncRenderingChanged = nextProps.isAsync !== this.props.isAsync; - - if ( hasRegistryChanged ) { - this.unsubscribe(); - this.subscribe( nextProps.registry ); - } - - if ( hasSyncRenderingChanged ) { - renderQueue.flush( this ); - } - - // Treat a registry change as equivalent to `ownProps`, to reflect - // `mergeProps` to rendered component if and only if updated. - const hasPropsChanged = ( - hasRegistryChanged || - ! isShallowEqualObjects( 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 && ! hasSyncRenderingChanged ) { - return false; - } - - if ( hasPropsChanged || hasSyncRenderingChanged ) { - const nextMergeProps = getNextMergeProps( nextProps ); - if ( ! isShallowEqualObjects( this.mergeProps, nextMergeProps ) ) { - // 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; - } - - onStoreChange() { - if ( ! this.canRunSelection ) { - this.hasQueuedSelection = true; - return; - } - - const nextMergeProps = getNextMergeProps( this.props ); - if ( isShallowEqualObjects( this.mergeProps, nextMergeProps ) ) { - return; - } - - this.mergeProps = nextMergeProps; - - // Schedule an update. Merge props are not assigned to state since - // 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( {} ); - } - - subscribe( registry ) { - this.unsubscribe = registry.subscribe( () => { - if ( this.props.isAsync ) { - renderQueue.add( this, this.onStoreChange ); - } else { - this.onStoreChange(); - } - } ); - } - - render() { - return ; - } - } - - return ( ownProps ) => ( - - { ( isAsync ) => ( - - { ( registry ) => ( - - ) } - - ) } - - ); -}, 'withSelect' ); - -export default withSelect; diff --git a/packages/data/src/index.js b/packages/data/src/index.js index d9c7c32ea75fc..46c830289b91d 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -9,13 +9,17 @@ import combineReducers from 'turbo-combine-reducers'; import defaultRegistry from './default-registry'; import * as plugins from './plugins'; -export { withSelect } from './components/with-select'; +export { default as withSelect } from './components/with-select'; export { default as withDispatch } from './components/with-dispatch'; export { default as withRegistry } from './components/with-registry'; -export { RegistryProvider, RegistryConsumer } from './components/registry-provider'; +export { + RegistryProvider, + RegistryConsumer, + useRegistry, +} from './components/registry-provider'; +export { default as useSelect } from './components/use-select'; export { AsyncModeProvider as __experimentalAsyncModeProvider, - useAsyncMode as __experimentalUseAsyncMode, } from './components/async-mode-provider'; export { createRegistry } from './registry'; export { plugins };