Permalink
Browse files

State: Support exporting selectors from state/selectors

  • Loading branch information...
1 parent f2f9f9a commit ea4ed92787ce33c1c7a125ef90248c3b8c8cfa65 @aduth aduth committed Jun 10, 2016
View
@@ -11,6 +11,15 @@
"syntax-jsx",
"transform-react-jsx",
"transform-react-display-name",
- "lodash"
+ "lodash",
+ [
+ "transform-imports",
+ {
+ "state/selectors": {
+ "transform": "state/selectors/${member}",
+ "kebabCase": true
+ }
+ }
+ ]
]
}
@@ -0,0 +1,13 @@
+State Selectors
+===============
+
+This folder contains all available state selectors. Each file includes a single default exported function which can be used as a helper in retrieving derived data from the global state tree.
+
+To learn more about selectors, refer to the ["Our Approach to Data" document](../../../docs/our-approach-to-data.md#selectors).
+
+When adding a new selector to this directory, make note of the following details:
+
+- Each new selector exists in its own file, named with [kebab case](https://en.wikipedia.org/wiki/Kebab_case) (dash-delimited lowercase)
+- There should be no more than a single default exported function per selector file
+- Tests for each selector should exist in the [`test/` subdirectory](./test) with matching file name of the selector
+- Your selector must be exported from [`index.js`](./index.js) to enable named importing from the base `state/selectors` directory
@@ -0,0 +1,14 @@
+/**
+ * Every selector contained within this directory should have its default
+ * export included in the list below. Please keep this list alphabetized for
+ * easy scanning.
+ *
+ * For more information about how we use selectors, refer to the docs:
+ * - https://wpcalypso.wordpress.com/devdocs/docs/our-approach-to-data.md#selectors
+ *
+ * Studious observers may note that our project is not configured to support
+ * "tree shaking", and that importing from this file might unnecessarily bloat
+ * the size of bundles. Fear not! For we do not truly import from this file,
+ * but instead use a Babel plugin "transform-imports" to transform the import
+ * to its individual file.
+ */
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { expect } from 'chai';
+import kebabCase from 'lodash/kebabCase';
+import camelCase from 'lodash/camelCase';
+import each from 'lodash/each';
+import fs from 'fs';
+import path from 'path';
+
+/**
+ * Internal dependencies
+ */
+import * as selectors from '../';
+
+/**
+ * Constants
+ */
+
+/**
+ * Matches string which ends in ".js" file extension
+ *
+ * @type {RegExp}
+ */
+const RX_JS_EXTENSION = /\.js$/;
+
+describe( 'selectors', () => {
+ it( 'should match every selector to its default export', () => {
+ each( selectors, ( selector, key ) => {
+ expect( require( '../' + kebabCase( key ) ) ).to.equal( selector );
+ } );
+ } );
+
+ it( 'should export every selector file', ( done ) => {
+ fs.readdir( path.join( __dirname, '..' ), ( error, files ) => {
+ if ( error ) {
+ return done( error );
+ }
+
+ each( files, ( file ) => {
+ if ( ! RX_JS_EXTENSION.test( file ) || 'index.js' === file ) {
+ return;
+ }
+
+ const exportName = camelCase( file.replace( RX_JS_EXTENSION, '' ) );
+ const errorMessage = `Expected to find selector \`${ exportName }\` (${ file }) in index.js exports.`;
+ expect( selectors, errorMessage ).to.include.keys( exportName );
+ } );
+
+ done();
+ } );
+ } );
+} );
@@ -62,7 +62,7 @@ __Advantages:__
## Current Recommendations
-All new data requirements should be implemented as part of the global Redux state tree. The `client/state` directory contains all of the behavior describing the global application state. The folder structure of the `state` directory should directly mirror the sub-trees within the global state tree. Each sub-tree can include their own reducer, actions, and selectors.
+All new data requirements should be implemented as part of the global Redux state tree. The `client/state` directory contains all of the behavior describing the global application state. The folder structure of the `state` directory should directly mirror the sub-trees within the global state tree. Each sub-tree can include their own reducer and actions.
### Terminology
@@ -83,16 +83,14 @@ The root module of the `state` directory exports a single reducer function. We l
client/state/
├── index.js
├── action-types.js
+├── selectors/
└── { subject }/
├── actions.js
├── reducer.js
- ├── selectors.js
├── schema.js
- ├── README.md
└── test/
├── actions.js
- ├── reducer.js
- └── selectors.js
+ └── reducer.js
```
For example, the reducer responsible for maintaining the `state.sites` key within the global state can be found in `client/state/sites/reducer.js`. It's quite common that the subject reducer is itself a combined reducer. Just as it helps to split the global state into subdirectories responsible for their own part of the tree, as a subject grows, you may find that it's easier to maintain pieces as nested subdirectories. This ease of composability is one of Redux's strengths.
@@ -255,6 +253,16 @@ let posts = state.sites.sitePosts[ siteId ].map( ( postId ) => state.posts.items
You'll note in this example that the entire `state` object is passed to the selector. We've chosen to standardize on always sending the entire state object to any selector as the first parameter. This consistency should alleviate uncertainty in calling selectors, as you can always assume that it'll have a similar argument signature. More importantly, it's not uncommon for selectors to need to traverse different parts of the global state, as in the example above where we pull from both the `sites` and `posts` top-level state keys.
+Much like action types, because selectors operate on the entire global state object, we've chosen to place them one-per-file under the `state/selectors` directory. Not only does this reflect their global nature, it removes uncertainty on where selectors are to be found or created by providing a single location for them to exist.
+
+When using selectors, you can import directly from `state/selectors`. For example:
+
+```js
+import { canCurrentUser } from 'state/selectors';
+```
+
+In this example, the logic for the selector exists at the file `state/selectors/can-current-user.js`. When creating a selector, you must also include its default function export in the list of exports in `state/selectors/index.js`.
+
It's important that selectors always be pure functions, meaning that the function should always return the same result when passed identical arguments in sequence. There should be no side-effects of calling a selector. For example, in a selector you should never trigger an AJAX request or assign values to variables defined outside the scope of the function.
What are a few common use-cases for selectors?
View
@@ -330,6 +330,9 @@
"babel-plugin-transform-export-extensions": {
"version": "6.8.0"
},
+ "babel-plugin-transform-imports": {
+ "version": "1.1.0"
+ },
"babel-plugin-transform-object-rest-spread": {
"version": "6.20.2"
},
@@ -534,7 +537,7 @@
}
},
"caniuse-db": {
- "version": "1.0.30000600"
+ "version": "1.0.30000601"
},
"caseless": {
"version": "0.11.0"
@@ -1335,7 +1338,7 @@
"dev": true,
"dependencies": {
"acorn": {
- "version": "4.0.3",
+ "version": "4.0.4",
"dev": true
}
}
@@ -1464,7 +1467,7 @@
"version": "2.1.1"
},
"fbjs": {
- "version": "0.8.6",
+ "version": "0.8.7",
"dependencies": {
"core-js": {
"version": "1.2.7"
@@ -1528,7 +1531,7 @@
"version": "2.3.0"
},
"flat-cache": {
- "version": "1.2.1",
+ "version": "1.2.2",
"dev": true
},
"flatten": {
@@ -2385,6 +2388,9 @@
"version": "3.0.4",
"dev": true
},
+ "lodash.kebabcase": {
+ "version": "4.1.1"
+ },
"lodash.keys": {
"version": "3.1.2",
"dev": true
@@ -3019,7 +3025,7 @@
"version": "1.0.2"
},
"pump": {
- "version": "1.0.1"
+ "version": "1.0.2"
},
"punycode": {
"version": "1.4.1"
@@ -3475,6 +3481,9 @@
"set-immediate-shim": {
"version": "1.0.1"
},
+ "setimmediate": {
+ "version": "1.0.5"
+ },
"setprototypeof": {
"version": "1.0.2"
},
View
@@ -21,6 +21,7 @@
"babel-plugin-syntax-jsx": "6.8.0",
"babel-plugin-transform-class-properties": "6.9.1",
"babel-plugin-transform-export-extensions": "6.8.0",
+ "babel-plugin-transform-imports": "1.1.0",
"babel-plugin-transform-react-display-name": "6.8.0",
"babel-plugin-transform-react-jsx": "6.8.0",
"babel-plugin-transform-runtime": "6.9.0",

0 comments on commit ea4ed92

Please sign in to comment.