Skip to content

Commit

Permalink
Add useStoreSelectors hook (#26692)
Browse files Browse the repository at this point in the history
* Allow useSelect per store

* New signature: useSelect( 'store', mapSelect, deps )

* Hopefully fix mobile tests

* Propose useStoreSelectors

* Revert README

* Add comment

* Remove shorthand notation and add unit tests

* Update README

* Remove now unrelated change

* Updated docstring

* Updated tests

* Use renderHook from react testing library

* Add usage example

* Updated doc

* Update CHANGELOG.md
  • Loading branch information
adamziel authored Nov 6, 2020
1 parent 26dc308 commit 24a4f44
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 0 deletions.
28 changes: 28 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"@storybook/addon-viewport": "5.3.2",
"@storybook/react": "6.0.21",
"@testing-library/react": "10.0.2",
"@testing-library/react-hooks": "3.4.2",
"@types/classnames": "2.2.10",
"@types/eslint": "6.8.0",
"@types/estree": "0.0.44",
Expand Down
4 changes: 4 additions & 0 deletions packages/data/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Feature

- Expose `useStoreSelectors` hook for usage in functional components. ([#26692](https://github.com/WordPress/gutenberg/pull/26692))

## 4.6.0 (2019-06-12)

### New Feature
Expand Down
42 changes: 42 additions & 0 deletions packages/data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,48 @@ _Returns_

- `Function`: A custom react hook.

<a name="useStoreSelectors" href="#useStoreSelectors">#</a> **useStoreSelectors**

Custom react hook for retrieving selectors from registered stores

_Usage_

```js
const { useStoreSelectors } = wp.data;

function HammerPriceDisplay( { currency } ) {
const price = useStoreSelectors(
'my-shop',
( { getPrice } ) => getPrice( 'hammer', currency ),
[ currency ]
);
return new Intl.NumberFormat( 'en-US', {
style: 'currency',
currency,
} ).format( price );
}

// Rendered in the application:
// <HammerPriceDisplay currency="USD" />
```

In the above example, when `HammerPriceDisplay` is rendered into an
application, the price will be retrieved from the store state using the
`mapSelectors` callback on `useStoreSelectors`. 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_

- _storeKey_ `string`: Store to return selectors from.
- _mapSelectors_ `Function`: Function called on every state change. The returned value is exposed to the component implementing this hook. The function receives the object with all the store selectors as its only 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.

<a name="withDispatch" href="#withDispatch">#</a> **withDispatch**

Higher-order component used to add dispatch props using registered action
Expand Down
50 changes: 50 additions & 0 deletions packages/data/src/components/use-store-selectors/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Internal dependencies
*/
import useSelect from '../use-select';

/**
* Custom react hook for retrieving selectors from registered stores
*
* @param {string} storeKey Store to return selectors from.
* @param {Function} mapSelectors Function called on every state change. The
* returned value is exposed to the component
* implementing this hook. The function receives
* the object with all the store selectors as
* its only 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 { useStoreSelectors } = wp.data;
*
* function HammerPriceDisplay( { currency } ) {
* const price = useStoreSelectors(
* 'my-shop',
* ( { getPrice } ) => getPrice( 'hammer', currency ),
* [ currency ]
* );
* return new Intl.NumberFormat( 'en-US', {
* style: 'currency',
* currency,
* } ).format( price );
* }
*
* // Rendered in the application:
* // <HammerPriceDisplay currency="USD" />
* ```
*
* In the above example, when `HammerPriceDisplay` is rendered into an
* application, the price will be retrieved from the store state using the
* `mapSelectors` callback on `useStoreSelectors`. 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 useStoreSelectors( storeKey, mapSelectors, deps = [] ) {
return useSelect( ( select ) => mapSelectors( select( storeKey ) ), deps );
}
45 changes: 45 additions & 0 deletions packages/data/src/components/use-store-selectors/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { renderHook } from '@testing-library/react-hooks';

/**
* WordPress dependencies
*/
import { RegistryProvider } from '@wordpress/data';

/**
* Internal dependencies
*/
import { createRegistry } from '../../../registry';
import useStoreSelectors from '../index';

describe( 'useStoreSelectors', () => {
let registry;
beforeEach( () => {
registry = createRegistry();
} );

it( 'wraps useSelect', () => {
registry.registerStore( 'testStore', {
reducer: () => ( { foo: 'bar' } ),
selectors: {
testSelector: ( state, key ) => state[ key ],
},
} );

const wrapper = ( { children } ) => (
<RegistryProvider value={ registry }>{ children }</RegistryProvider>
);

const useHook = () =>
useStoreSelectors( 'testStore', ( { testSelector } ) =>
testSelector( 'foo' )
);

const { result } = renderHook( useHook, { wrapper } );

// ensure expected state was rendered
expect( result.current ).toEqual( 'bar' );
} );
} );
1 change: 1 addition & 0 deletions packages/data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
useRegistry,
} from './components/registry-provider';
export { default as useSelect } from './components/use-select';
export { default as useStoreSelectors } from './components/use-store-selectors';
export { useDispatch } from './components/use-dispatch';
export { AsyncModeProvider } from './components/async-mode-provider';
export { createRegistry } from './registry';
Expand Down

0 comments on commit 24a4f44

Please sign in to comment.