├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src └── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | dist 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-selector 2 | 3 | ![](https://img.shields.io/npm/dm/create-selector.svg)![](https://img.shields.io/npm/v/create-selector.svg)![](https://img.shields.io/npm/l/create-selector.svg)[![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | 5 | Simple util that wraps reselect's `createSelector` but adds the ability to define selector dependencies as strings, then resolve them later. 6 | 7 | This makes it easier to compose selectors from different parts of your app or combine functionality from different app bundles without having to have direct references to input functions when you're defining them. 8 | 9 | In this way you can defer the creation of a selector and populate it later without needing to have direct references to the input selectors when you're first defining it. 10 | 11 | Just like reselect, it also attaches the last function as a `.resultFunc` property for easy testing without needing all the input functions. 12 | 13 | ## example 14 | 15 | ```js 16 | import { createSelector, resolveSelectors } from 'create-selector' 17 | 18 | export const selectUserData = state => state.user 19 | export const shouldFetchData = createSelector( 20 | 'selectIsLoggedIn', 21 | selectUserData, 22 | (loggedIn, userData) => { 23 | if (loggedIn && !userData) { 24 | return true 25 | } 26 | } 27 | ) 28 | ``` 29 | 30 | ```js 31 | import { shouldFetchData } from './other-selectors' 32 | import { selectIsLoggedIn } from './auth-selectors' 33 | 34 | // later, you can aggregate them 35 | const selectorAggregator = { 36 | selectIsLoggedIn, 37 | shouldFetchData 38 | } 39 | 40 | // resolves all the string references with the real ones recursively 41 | // until you've got an object with all your selectors combined 42 | resolveSelectors(selectorAggregator) 43 | ``` 44 | 45 | that's it! 46 | 47 | ## Notes 48 | 49 | - There's some tests to show this does what it's supposed to but most of the actual work happens in reselect. 50 | - It tolerates mixing in _real_ selectors too (even if they were created with reselect, directly). 51 | 52 | ## install 53 | 54 | ``` 55 | npm install create-selector 56 | ``` 57 | 58 | ## changes 59 | 60 | - `5.0.0` Removed source maps from build. Fixed npm security warnings w/ audit. 61 | - `4.0.3` Optimizing selector resolution algorithm. Huge thanks to [@rudionrails](https://github.com/rudionrails) and [@layflags](https://github.com/layflags). 62 | - `4.0.1` building with microbundle (should fix issues with module field in package.json) 63 | - `2.2.0` 64 | - added support for fully resolved input selectors not having to be on the final object 65 | - improved error handling 66 | - more test coverage 67 | - updated dependencies 68 | 69 | ## credits 70 | 71 | If you like this follow [@HenrikJoreteg](http://twitter.com/henrikjoreteg) on twitter. But in terms of credit, this is just a simple util on top of [reselect](https://github.com/reactjs/reselect) all the real magic is in there. 72 | 73 | ## license 74 | 75 | [MIT](http://mit.joreteg.com/) 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-selector", 3 | "description": "Wrapper for reselect to allow deferring creation of selectors.", 4 | "version": "5.0.0", 5 | "author": "Henrik Joreteg (joreteg.com)", 6 | "dependencies": { 7 | "reselect": "3.0.1" 8 | }, 9 | "devDependencies": { 10 | "microbundle": "0.11.0", 11 | "nodemon": "1.18.10", 12 | "standard": "12.0.1", 13 | "tape": "4.10.1" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "license": "MIT", 19 | "main": "dist/index.js", 20 | "module": "dist/index.m.js", 21 | "prettier": { 22 | "singleQuote": true, 23 | "semi": false 24 | }, 25 | "scripts": { 26 | "build": "rm -rf ./dist && standard --fix && microbundle --no-compress --no-sourcemap", 27 | "test": "npm run build && node test.js", 28 | "test-watch": "nodemon test.js" 29 | }, 30 | "standard": { 31 | "ignore": [ 32 | "index.js" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createSelector as realCreateSelector } from 'reselect' 2 | 3 | const ensureFn = (obj, name) => { 4 | if (typeof name !== 'string') { 5 | return name 6 | } 7 | const found = obj[name] 8 | if (!found) { 9 | throw Error('No selector ' + name + ' found on the obj.') 10 | } 11 | return found 12 | } 13 | 14 | export const createSelector = (...fns) => { 15 | const resultFunc = fns.slice(-1)[0] 16 | const deferredSelector = (obj, deps) => { 17 | const newArgs = deps.map(fn => ensureFn(obj, fn)) 18 | newArgs.push(resultFunc) 19 | return realCreateSelector(...newArgs) 20 | } 21 | deferredSelector.deps = fns.slice(0, -1) 22 | deferredSelector.resultFunc = resultFunc 23 | return deferredSelector 24 | } 25 | 26 | export const resolveSelectors = obj => { 27 | // an item is resolved if it is either a 28 | // function with no dependencies or if 29 | // it's on the object with no dependencies 30 | const isResolved = name => (name.call && !name.deps) || !obj[name].deps 31 | 32 | // flag for checking if we have *any* 33 | let hasAtLeastOneResolved = false 34 | 35 | // extract all deps and any resolved items 36 | for (const selectorName in obj) { 37 | const fn = obj[selectorName] 38 | if (!isResolved(selectorName)) { 39 | fn.deps = fn.deps.map((val, index) => { 40 | // if it is a function not a string 41 | if (val.call) { 42 | // look for it already on the object 43 | for (const key in obj) { 44 | if (obj[key] === val) { 45 | // return its name if found 46 | return key 47 | } 48 | } 49 | // we didn't find it and it doesn't have a name 50 | // but if it's a fully resolved selector that's ok 51 | if (!val.deps) { 52 | hasAtLeastOneResolved = true 53 | return val 54 | } 55 | } 56 | 57 | // the `val` is a string that exists on the object return the string 58 | // we'll resolve it later 59 | if (obj[val]) return val 60 | 61 | // if we get here, its a string that doesn't exist on the object 62 | // which won't work, so we throw a helpful error 63 | throw Error( 64 | `The input selector at index ${index} for '${selectorName}' is missing from the object passed to resolveSelectors()` 65 | ) 66 | }) 67 | } else { 68 | hasAtLeastOneResolved = true 69 | } 70 | } 71 | 72 | if (!hasAtLeastOneResolved) { 73 | throw Error( 74 | `You must pass at least one real selector. If they're all string references there's no` 75 | ) 76 | } 77 | 78 | const depsAreResolved = deps => deps.every(isResolved) 79 | 80 | const resolve = () => { 81 | let hasUnresolved = false 82 | for (const selectorName in obj) { 83 | const fn = obj[selectorName] 84 | if (!isResolved(selectorName)) { 85 | hasUnresolved = true 86 | if (depsAreResolved(fn.deps)) { 87 | // we could just use `obj[selectorName] = fn(obj, fn.deps)`, but that 88 | // has a significant performance impact when trying to perform this 89 | // on a large object (> 1000). More on this here: 90 | // http://2ality.com/2014/01/object-assign.html 91 | const selectorFn = fn(obj, fn.deps) 92 | delete obj[selectorName] 93 | obj[selectorName] = selectorFn 94 | } 95 | } 96 | } 97 | return hasUnresolved 98 | } 99 | 100 | let startTime 101 | while (resolve()) { 102 | if (!startTime) startTime = Date.now() 103 | const duration = Date.now() - startTime 104 | if (duration > 500) { 105 | throw Error('Could not resolve selector dependencies.') 106 | } 107 | } 108 | 109 | return obj 110 | } 111 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const realCreateSelector = require('reselect').createSelector 2 | const test = require('tape') 3 | const { createSelector } = require('./dist') 4 | const { resolveSelectors } = require('./dist') 5 | 6 | function bm (fn) { 7 | const start = new Date().getTime() 8 | fn() 9 | 10 | // console.log('Finished in:', new Date().getTime() - start, ' ms') 11 | return new Date().getTime() - start 12 | } 13 | 14 | test('returns a deferred selector', t => { 15 | const lastFunc = (someId, sameId) => someId === sameId 16 | const deferredSelector = createSelector( 17 | 'someString', 18 | 'someOtherString', 19 | lastFunc 20 | ) 21 | t.ok(deferredSelector.deps, 'should have deps flag') 22 | t.ok( 23 | deferredSelector.resultFunc === lastFunc, 24 | 'should still have resultFunc prop' 25 | ) 26 | 27 | const id = id => id 28 | const sel = deferredSelector({ someString: id, someOtherString: id }, [ 29 | 'someString', 30 | 'someOtherString' 31 | ]) 32 | t.ok(!sel.deps, 'should not have deps') 33 | t.ok(sel.resultFunc === lastFunc, 'should have resultFunc prop') 34 | t.ok(sel() === true, 'returns true when ran') 35 | t.end() 36 | }) 37 | 38 | test('resolves multiple levels down', t => { 39 | const idSelector = state => state.id 40 | 41 | const dep1 = createSelector( 42 | 'idSelector', 43 | id => id 44 | ) 45 | 46 | const dep2 = createSelector( 47 | 'dep1', 48 | id => id 49 | ) 50 | 51 | // mix in a selector created by reselect 52 | const dep3 = realCreateSelector(idSelector, id => id) 53 | 54 | // mix in a selector created by reselect 55 | const dep4 = realCreateSelector(idSelector, id => id) 56 | 57 | const final = createSelector( 58 | 'dep1', 59 | dep2, 60 | 'dep3', 61 | dep4, 62 | (id, thing, other, stuff) => 63 | id === 'hi' && id === thing && id === other && id === stuff 64 | ) 65 | 66 | const obj = { 67 | idSelector, 68 | dep1, 69 | dep2, 70 | dep3, 71 | dep4, 72 | final 73 | } 74 | 75 | resolveSelectors(obj) 76 | 77 | t.ok(obj.final({ id: 'hi' }) === true, 'as') 78 | t.end() 79 | }) 80 | 81 | test('resolves large set of nested selectors', t => { 82 | const idSelector = state => state.id 83 | const dep0 = createSelector( 84 | idSelector, 85 | id => id 86 | ) 87 | const obj = { idSelector, dep0 } 88 | 89 | let count = 1 90 | while (count < 1000) { 91 | const prevSelector = obj[`dep${count - 1}`] 92 | obj[`dep${count}`] = createSelector( 93 | prevSelector, 94 | id => id 95 | ) 96 | count++ 97 | } 98 | 99 | obj.final = createSelector( 100 | ...Object.values(obj), 101 | (id, ...rest) => id === 'hi' && rest.every(i => i === id) 102 | ) 103 | 104 | const duration = bm(() => resolveSelectors(obj)) 105 | 106 | t.ok(obj.final({ id: 'hi' }) === true, 'as') 107 | t.ok(duration < 150, `should not take too long (took ${duration} ms)`) 108 | t.end() 109 | }) 110 | 111 | test('resolves large set of flat selectors', t => { 112 | const idSelector = state => state.id 113 | const obj = { idSelector } 114 | 115 | let count = 0 116 | while (count < 1000) { 117 | obj[`dep${count}`] = createSelector( 118 | idSelector, 119 | id => id 120 | ) 121 | count++ 122 | } 123 | 124 | obj.final = createSelector( 125 | ...Object.values(obj), 126 | (id, ...rest) => id === 'hi' && rest.every(i => i === id) 127 | ) 128 | 129 | const duration = bm(() => resolveSelectors(obj)) 130 | 131 | t.ok(obj.final({ id: 'hi' }) === true, 'as') 132 | t.ok(duration < 100, `should not take too long (took ${duration} ms)`) 133 | t.end() 134 | }) 135 | 136 | test('throws error if cannot resolve selectors because all string references', t => { 137 | t.throws(() => { 138 | resolveSelectors({ 139 | dep1: createSelector( 140 | 'dep1', 141 | one => one 142 | ) 143 | }) 144 | }) 145 | t.end() 146 | }) 147 | 148 | test('throws if unresolvable', t => { 149 | t.throws(() => { 150 | resolveSelectors({ 151 | dep0: id => id, 152 | dep1: createSelector( 153 | 'dep0', 154 | one => one 155 | ), 156 | dep2: createSelector( 157 | 'somethingBogus', 158 | one => one 159 | ) 160 | }) 161 | }) 162 | t.end() 163 | }) 164 | 165 | test("tolerate selectors that don't exist on the shared object if not deferred", t => { 166 | const idSelector = state => state.id 167 | 168 | const dep1 = createSelector( 169 | idSelector, 170 | id => id 171 | ) 172 | 173 | const dep2 = createSelector( 174 | idSelector, 175 | 'dep1', 176 | (val1, val2) => val1 === 'hi' && val1 === val2 177 | ) 178 | 179 | // note: this is missing the `idSelector` 180 | const obj = { 181 | dep1, 182 | dep2 183 | } 184 | 185 | t.doesNotThrow(() => { 186 | resolveSelectors(obj) 187 | }) 188 | 189 | resolveSelectors(obj) 190 | 191 | t.ok(obj.dep2({ id: 'hi' }) === true, 'as') 192 | t.end() 193 | }) 194 | --------------------------------------------------------------------------------