Skip to content
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

Data Module: Expose state using selectors #4105

Merged
merged 8 commits into from
Jan 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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