Skip to content

Commit

Permalink
Data Module: Expose state using selectors (#4105)
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad authored Jan 17, 2018
1 parent 47c36a5 commit add336f
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 11 deletions.
41 changes: 40 additions & 1 deletion data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,43 @@ Registers a [`listener`](https://redux.js.org/docs/api/Store.html#subscribe) fun

#### `store.dispatch( action: object )`

The dispatch function should be called to trigger the registered reducers function and update the state. An [`action`](https://redux.js.org/docs/api/Store.html#dispatch)object should be passed to this action. This action is passed to the registered reducers in addition to the previous state.
The dispatch function should be called to trigger the registered reducers function and update the state. An [`action`](https://redux.js.org/docs/api/Store.html#dispatch) object should be passed to this function. This action is passed to the registered reducers in addition to the previous state.


### `wp.data.registerSelectors( reducerKey: string, newSelectors: object )`

If your module or plugin needs to expose its state to other modules and plugins, you'll have to register state selectors.

A selector is a function that takes the current state value as a first argument and extra arguments if needed and returns any data extracted from the state.

#### Example:

Let's say the state of our plugin (registered with the key `myPlugin`) has the following shape: `{ title: 'My post title' }`. We can register a `getTitle` selector to make this state value available like so:

```js
wp.data.registerSelectors( 'myPlugin', { getTitle: ( state ) => state.title } );
```

### `wp.data.select( key: string, selectorName: string, ...args )`

This function allows calling any registered selector. Given a module's key, a selector's name and extra arguments passed to the selector, this function calls the selector passing it the current state and the extra arguments provided.

#### Example:

```js
wp.data.select( 'myPlugin', 'getTitle' ); // Returns "My post title"
```

### `wp.data.query( mapSelectorsToProps: func )( WrappedComponent: Component )`

If you use a React or WordPress Element, a Higher Order Component is made available to inject data into your components like so:

```js
const Component = ( { title } ) => <div>{ title }</div>;

wp.data.query( select => {
return {
title: select( 'myPlugin', 'getTitle' ),
};
} )( Component );
```
68 changes: 61 additions & 7 deletions data/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/**
* External dependencies
*/
import { connect } from 'react-redux';
import { createStore, combineReducers } from 'redux';
import { flowRight } from 'lodash';

/**
* Module constants
*/
const reducers = {};
const selectors = {};
const enhancers = [];
if ( window.__REDUX_DEVTOOLS_EXTENSION__ ) {
enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__() );
Expand All @@ -17,17 +19,17 @@ const initialReducer = () => ( {} );
const store = createStore( initialReducer, {}, flowRight( enhancers ) );

/**
* Registers a new sub reducer to the global state and returns a Redux-like store object.
* Registers a new sub-reducer to the global state and returns a Redux-like store object.
*
* @param {String} key Reducer key
* @param {Object} reducer Reducer function
* @param {string} reducerKey Reducer key.
* @param {Object} reducer Reducer function.
*
* @returns {Object} Store Object.
* @returns {Object} Store Object.
*/
export function registerReducer( key, reducer ) {
reducers[ key ] = reducer;
export function registerReducer( reducerKey, reducer ) {
reducers[ reducerKey ] = reducer;
store.replaceReducer( combineReducers( reducers ) );
const getState = () => store.getState()[ key ];
const getState = () => store.getState()[ reducerKey ];

return {
dispatch: store.dispatch,
Expand All @@ -46,3 +48,55 @@ export function registerReducer( key, reducer ) {
getState,
};
}

/**
* Registers selectors for external usage.
*
* @param {string} reducerKey Part of the state shape to register the
* selectors for.
* @param {Object} newSelectors Selectors to register. Keys will be used
* as the public facing API. Selectors will
* get passed the state as first argument.
*/
export function registerSelectors( reducerKey, newSelectors ) {
selectors[ reducerKey ] = newSelectors;
}

/**
* Higher Order Component used to inject data using the registered selectors.
*
* @param {Function} mapSelectorsToProps Gets called with the selectors object
* to determine the data for the component.
*
* @returns {Func} Renders the wrapped component and passes it data.
*/
export const query = ( mapSelectorsToProps ) => ( WrappedComponent ) => {
const connectWithStore = ( ...args ) => {
const ConnectedWrappedComponent = connect( ...args )( WrappedComponent );
return ( props ) => {
return <ConnectedWrappedComponent { ...props } store={ store } />;
};
};

return connectWithStore( ( state, ownProps ) => {
const select = ( key, selectorName, ...args ) => {
return selectors[ key ][ selectorName ]( state[ key ], ...args );
};

return mapSelectorsToProps( select, ownProps );
} );
};

/**
* Calls a selector given the current state and extra arguments.
*
* @param {string} reducerKey Part of the state shape to register the
* selectors for.
* @param {string} selectorName Selector name.
* @param {*} args Selectors arguments.
*
* @returns {*} The selector's returned value.
*/
export const select = ( reducerKey, selectorName, ...args ) => {
return selectors[ reducerKey ][ selectorName ]( store.getState()[ reducerKey ], ...args );
};
7 changes: 7 additions & 0 deletions data/test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`query passes the relevant data to the component 1`] = `
<div>
reactState
</div>
`;
49 changes: 48 additions & 1 deletion data/test/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { registerReducer } from '../';
/**
* External dependencies
*/
import { render } from 'enzyme';

/**
* Internal dependencies
*/
import { registerReducer, registerSelectors, select, query } from '../';

describe( 'store', () => {
it( 'Should append reducers to the state', () => {
Expand All @@ -12,3 +20,42 @@ describe( 'store', () => {
expect( store2.getState() ).toEqual( 'ribs' );
} );
} );

describe( 'select', () => {
it( 'registers multiple selectors to the public API', () => {
const store = registerReducer( 'reducer1', () => 'state1' );
const selector1 = jest.fn( () => 'result1' );
const selector2 = jest.fn( () => 'result2' );

registerSelectors( 'reducer1', {
selector1,
selector2,
} );

expect( select( 'reducer1', 'selector1' ) ).toEqual( 'result1' );
expect( selector1 ).toBeCalledWith( store.getState() );

expect( select( 'reducer1', 'selector2' ) ).toEqual( 'result2' );
expect( selector2 ).toBeCalledWith( store.getState() );
} );
} );

describe( 'query', () => {
it( 'passes the relevant data to the component', () => {
registerReducer( 'reactReducer', () => ( { reactKey: 'reactState' } ) );
registerSelectors( 'reactReducer', {
reactSelector: ( state, key ) => state[ key ],
} );
const Component = query( ( selectFunc, ownProps ) => {
return {
data: selectFunc( 'reactReducer', 'reactSelector', ownProps.keyName ),
};
} )( ( props ) => {
return <div>{ props.data }</div>;
} );

const tree = render( <Component keyName="reactKey" /> );

expect( tree ).toMatchSnapshot();
} );
} );
6 changes: 5 additions & 1 deletion editor/store/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress Dependencies
*/
import { registerReducer } from '@wordpress/data';
import { registerReducer, registerSelectors } from '@wordpress/data';

/**
* Internal dependencies
Expand All @@ -12,16 +12,20 @@ import { withRehydratation, loadAndPersist } from './persist';
import enhanceWithBrowserSize from './mobile';
import applyMiddlewares from './middlewares';
import { BREAK_MEDIUM } from './constants';
import { getEditedPostTitle } from './selectors';

/**
* Module Constants
*/
const STORAGE_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings.uid }`;
const MODULE_KEY = 'core/editor';

const store = applyMiddlewares(
registerReducer( 'core/editor', withRehydratation( reducer, 'preferences' ) )
);
loadAndPersist( store, 'preferences', STORAGE_KEY, PREFERENCES_DEFAULTS );
enhanceWithBrowserSize( store, BREAK_MEDIUM );

registerSelectors( MODULE_KEY, { getEditedPostTitle } );

export default store;
2 changes: 1 addition & 1 deletion lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function gutenberg_register_scripts_and_styles() {
wp_register_script(
'wp-data',
gutenberg_url( 'data/build/index.js' ),
array(),
array( 'wp-element' ),
filemtime( gutenberg_dir_path() . 'data/build/index.js' )
);
wp_register_script(
Expand Down

0 comments on commit add336f

Please sign in to comment.