├── .npmignore ├── .gitignore ├── package.json ├── LICENSE ├── src └── index.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /node_modules 3 | 4 | lib 5 | /lib -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reselectie", 3 | "version": "1.0.1", 4 | "description": "Reselect and re-reselect alternative. Smaller, faster, greater flexibility, and better interface.", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "url": "https://github.com/ralusek/reselectie", 8 | "type": "git" 9 | }, 10 | "keywords": [ 11 | "reselect", 12 | "re-reselect", 13 | "memoize", 14 | "memoization", 15 | "cache", 16 | "key", 17 | "conditional", 18 | "store", 19 | "redux", 20 | "selector", 21 | "selectors", 22 | "computed", 23 | "compute", 24 | "computation", 25 | "immutable", 26 | "immutability" 27 | ], 28 | "scripts": { 29 | "compile": "babel --presets es2015,stage-0 -d lib/ src/", 30 | "prepublish": "npm run compile" 31 | }, 32 | "author": "Tomas Savigliano", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "babel-cli": "^6.26.0", 36 | "babel-preset-es2015": "^6.24.1", 37 | "babel-preset-stage-0": "^6.24.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Tomas Savigliano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const memoize = (...selectors) => { 2 | const compute = selectors.pop(); 3 | // If no initial selector is provided other than compute, assume we just 4 | // want to memoize on state 5 | if (!selectors.length) selectors.push(state => state); 6 | let previousSelectorResults = []; 7 | let previousComputeResult; 8 | 9 | const len = selectors.length; 10 | 11 | return function() { 12 | const current = []; 13 | let unchanged = true; 14 | for (let i = 0; i < len; i++) { 15 | ((current[i] = selectors[i].apply(null, arguments)) !== previousSelectorResults[i]) && (unchanged = false) 16 | } 17 | 18 | previousSelectorResults = current; 19 | if (unchanged) return previousComputeResult; 20 | return previousComputeResult = compute.apply(null, current); 21 | } 22 | }; 23 | 24 | 25 | const memoizeHandlerAs = (handler) => { 26 | const keyed = new Map(); 27 | 28 | return (key) => (state) => { 29 | if (key && (typeof(key) === 'function')) key = key(state); 30 | if (!keyed.has(key)) keyed.set(key, handler(key)); 31 | 32 | return keyed.get(key)(state); 33 | } 34 | }; 35 | 36 | 37 | const memoizeAs = (...selectors) => { 38 | return memoizeHandlerAs(key => { 39 | selectors = selectors.map(selector => (...args) => selector(...args, key)); 40 | return memoize(...selectors); 41 | }); 42 | }; 43 | 44 | 45 | module.exports = Object.freeze({ 46 | memoize, 47 | memoizeHandlerAs, 48 | memoizeAs 49 | }); 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reselectie 2 | 3 | `$ npm install --save reselectie` 4 | 5 | Memoized selector library for any immutable data structure (such as a `redux` immutable store). 6 | This library serves as a smaller, faster alternative to `reselect` AND `re-reselect`. 7 | 8 | As with `reselect` (for which `memoize` function shares an interface): 9 | 10 | * Selectors can compute derived data, allowing Redux to store the minimal possible state. 11 | * Selectors are efficient. A selector is not recomputed unless one of its arguments change. 12 | * Selectors are composable. They can be used as input to other selectors. 13 | 14 | ### `memoize` 15 | 16 | ```js 17 | import { memoize, memoizeAs } from 'reselectie'; 18 | 19 | const shopItemsSelector = state => state.shop.items; 20 | const taxPercentSelector = state => state.shop.taxPercent; 21 | 22 | const subtotalSelector = memoize( 23 | shopItemsSelector, 24 | items => items.reduce((acc, item) => acc + item.value, 0) 25 | ); 26 | 27 | const taxSelector = memoize( 28 | subtotalSelector, 29 | taxPercentSelector, 30 | // The last function provided is passed in is the "compute" functionality. 31 | // This is only called if any of the previous selector's values changed from 32 | // the last invocation. 33 | // The arguments passed in are the returned values (in order) of the previous 34 | // selectors. 35 | (subtotal, taxPercent) => subtotal * (taxPercent / 100) 36 | ); 37 | 38 | const totalSelector = memoize( 39 | subtotalSelector, 40 | taxSelector, 41 | (subtotal, tax) => ({ total: subtotal + tax }) 42 | ); 43 | 44 | const exampleState = { 45 | shop: { 46 | taxPercent: 8, 47 | items: [ 48 | { name: 'apple', value: 1.20 }, 49 | { name: 'orange', value: 0.95 }, 50 | ] 51 | } 52 | }; 53 | 54 | console.log(subtotalSelector(exampleState)); // 2.15 55 | console.log(taxSelector(exampleState)); // 0.172 56 | console.log(totalSelector(exampleState)); // { total: 2.322 } 57 | ``` 58 | 59 | 60 | ### `memoizeAs` 61 | 62 | There are many cases where we want to provide a key identifier or conditional 63 | that triggers a recompute that should be memoized in the context of that key. 64 | An example of this might be that I have a list of items and column headers 65 | capable of sorting the data. If I provide the column name as a key, the sort 66 | could be memoized in the context of the key. If I were to sort by `Column A`, then 67 | `Column B`, at this point both `A` and `B` would be memoized to avoid unecessary recomputes. 68 | 69 | ```js 70 | const getItems = state => state.items; 71 | 72 | const getSortedItems = memoizeAs( 73 | // Whatever we use as a key for `memoizeAs` is tacked on the selector arguments 74 | // as the last value. In this case, the `column` property that is here as the 75 | // final argument is the key we're using to identify a table column to sort by. 76 | (state, column) => getItems(state), 77 | (items, column) => _.sortBy(items, column) 78 | ); 79 | 80 | 81 | // Usage: 82 | // The first function call is to pass in the key under which we're memoizing. 83 | // The function returned is the normal selector expecting state. 84 | getSortedItems('date')(exampleState); 85 | 86 | // The pattern is extremely useful for dynamic keys, but also offers a good 87 | // mechanism by which to define non-dynamic variants of a selector. 88 | // For example: 89 | const sortedByDate = getSortedItems('date'); 90 | const sortedByAge = getSortedItems('age'); 91 | 92 | sortedByDate(exampleState); 93 | sortedByAge(exampleState); 94 | sortedByDate(exampleState); // Memoized response. 95 | ``` 96 | 97 | This functionality is similar to what is offered in the `re-reselect` libary, but 98 | with a different interface (and less code). Another benefit over `re-reselect` 99 | is that the key is kept on an `ES6` map, allowing a key of any value type. If the 100 | provided key is a function, the function will be called and passed in state, 101 | whose returned value will be used as the map key. 102 | 103 | 104 | ### `memoizeHandlerAs` 105 | 106 | This is less likely to be used, but works very much like `memoizeAs` in that it 107 | allows a key to be provided that is passed into a closure capable of maintaining 108 | state in the context of that key. `memoizeAs` internally uses `memoizeHandlerAs` 109 | to wrap the provided selectors in a way that they accept the memoized key. 110 | 111 | You can use this if you feel as though you need finer control of what happens 112 | in the keyed context rather than just executing selectors. 113 | --------------------------------------------------------------------------------