├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .ncurc.json ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src └── index.js ├── test ├── index.spec.js ├── mocha.opts └── utils │ └── document.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | **/node_modules 3 | **/webpack.config.js 4 | examples/**/server.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "rules": { 9 | "semi": ["error", "always", { "omitLastInOneLineBlock": true}] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | dist 5 | lib 6 | coverage 7 | -------------------------------------------------------------------------------- /.ncurc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reject": [ 3 | "reselect" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log\ 3 | .idea 4 | src 5 | test 6 | examples 7 | coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## v1.2.2 7 | 8 | * Upgrade dependencies 9 | * Record the duration of the selector and pass it to the change callback via a new extra properties argument 10 | * Change log format to include duration & use groupCollapsed 11 | 12 | ## v1.1.0 (2016-08-19) 13 | 14 | Allow memoizeOptions to be passed through 15 | 16 | ## v1.0.2 (2016-04-10) 17 | 18 | Fix the package.json file so it can actually be used 19 | 20 | ## v1.0.1 (2016-04-10) 21 | 22 | Change the log format 23 | 24 | ## v1.0.0 (2016-04-10) 25 | 26 | Initial release 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kieran Brownlees 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reselect Change Memoize 2 | 3 | [![Build Status](https://travis-ci.org/kbrownlees/reselect-change-memoize.svg?branch=master)](https://travis-ci.org/kbrownlees/reselect-change-memoize) 4 | [![npm version](https://badge.fury.io/js/reselect-change-memoize.svg)](https://badge.fury.io/js/reselect-change-memoize) 5 | 6 | A simple memoize function for reselect which performs a callback everytime the result changes. 7 | 8 | This package contains three exports: 9 | * a changeMemoize 10 | * createSelectorWithChangeCallback which allows you to easily substitute createSelector 11 | * a createSelector helper which enables change logging if not in production and allows you to name selectors 12 | 13 | ## changeMemoize 14 | 15 | ```js 16 | import { createSelectorCreator } from 'reselect'; 17 | import { changeMemoize } from 'reselect-change-memoize'; 18 | 19 | const myCallback = function(lastArgs, lastResult, newArgs, newResult) { 20 | // Your code 21 | }; 22 | 23 | const createSelector = createSelectorCreator(changeMemoize, myCallback); 24 | ``` 25 | 26 | ## createSelectorWithChangeCallback 27 | 28 | ```js 29 | import { createSelectorWithChangeCallback } from 'reselect-change-memoize'; 30 | 31 | const myCallback = function(lastArgs, lastResult, newArgs, newResult) { 32 | // Your code 33 | }; 34 | 35 | const selector = createSelectorWithChangeCallback( 36 | myCallback, 37 | (state) => state, 38 | (state) => { 39 | return { state }; 40 | } 41 | ); 42 | 43 | selector({ initial: 'state' }); 44 | ``` 45 | 46 | ## createSelector 47 | 48 | ```js 49 | import { createSelector } from 'reselect-change-memoize'; 50 | 51 | const selector1 = createSelector( 52 | 'An awesome selector', 53 | (state) => state, 54 | (state) => { 55 | return { selector1: state }; 56 | } 57 | ); 58 | const selector2 = createSelector( 59 | 'A second awesome selector which uses the first awesome selector', 60 | selector1, 61 | (state) => { 62 | return { selector2: state }; 63 | } 64 | ); 65 | // The name doesn't have to be provided 66 | const selector3 = createSelector( 67 | selector1, 68 | (state) => { 69 | return { selector2: state }; 70 | } 71 | ); 72 | selector2({ initial: 'state' }); 73 | selector1({ second: 'state' }); 74 | selector3({ second: 'state' }); 75 | ``` 76 | 77 | produces 78 | 79 | ``` 80 | - An awesome selector (0.06ms) 81 | lastArgs {} 82 | lastResult {} 83 | newArgs [ { initial: 'state' } ] 84 | newResult { selector1: { initial: 'state' } } 85 | - A second awesome selector which uses the first awesome selector (0.05ms) 86 | lastArgs {} 87 | lastResult {} 88 | newArgs [ { selector1: { initial: 'state' } } ] 89 | newResult { selector2: { selector1: { initial: 'state' } } } 90 | - An awesome selector (0.01ms) 91 | lastArgs [ { initial: 'state' } ] 92 | lastResult { selector1: { initial: 'state' } } 93 | newArgs [ { second: 'state' } ] 94 | newResult { selector1: { second: 'state' } } 95 | - unknown (0.03ms) 96 | lastArgs {} 97 | lastResult {} 98 | newArgs [ { second: 'state' } ] 99 | newResult { selector3: { second: 'state' } } 100 | ``` 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reselect-change-memoize", 3 | "version": "1.2.2", 4 | "description": "A memoize function for reselect which will make a callback when a result changes", 5 | "main": "dist/reselect-change-memoize.js", 6 | "jsnext:main": "src/index.js", 7 | "scripts": { 8 | "clean": "rimraf lib dist", 9 | "build": "webpack --mode development", 10 | "build:umd": "webpack --mode production", 11 | "lint": "eslint src test", 12 | "test": "NODE_ENV=test mocha", 13 | "test:watch": "NODE_ENV=test mocha --watch", 14 | "test:cov": "babel-node ./node_modules/.bin/isparta cover ./node_modules/.bin/_mocha", 15 | "prepublish": "npm run lint && npm run test && npm run clean && npm run build && npm run build:umd" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/kbrownlees/reselect-change-memoize.git" 20 | }, 21 | "keywords": [ 22 | "reselect", 23 | "memoize", 24 | "logging" 25 | ], 26 | "author": "Kieran Brownlees", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/kbrownlees/reselect-change-memoize/issues" 30 | }, 31 | "homepage": "https://github.com/kbrownlees/reselect-change-memoize", 32 | "peerDependencies": { 33 | "reselect": ">=1.0.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.8.4", 37 | "@babel/preset-env": "^7.8.4", 38 | "@babel/register": "^7.8.3", 39 | "babel-loader": "^8.0.6", 40 | "eslint": "^6.8.0", 41 | "eslint-config-airbnb": "^18.0.1", 42 | "eslint-plugin-import": "^2.20.1", 43 | "eslint-plugin-jsx-a11y": "^6.2.3", 44 | "eslint-plugin-react": "^7.18.3", 45 | "expect": "^25.1.0", 46 | "isparta": "^4.1.1", 47 | "mocha": "^10.1.0", 48 | "reselect": ">=1.0.0", 49 | "rimraf": "^3.0.1", 50 | "webpack": "^4.41.5", 51 | "webpack-cli": "^3.3.10", 52 | "webpack-dev-server": "^3.10.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { createSelectorCreator, defaultMemoize } from 'reselect'; 3 | 4 | const notset = {}; 5 | 6 | 7 | export function changeMemoize(func, changeCallback, memoize, ...memoizeOptions) { 8 | const memoizeFunction = memoize || defaultMemoize; 9 | 10 | const memoizeInstance = memoizeFunction(func, ...memoizeOptions); 11 | if (changeCallback == null) { 12 | return memoizeInstance; 13 | } 14 | 15 | const performanceAvailable = typeof performance === 'object'; 16 | 17 | let lastArgs = notset; 18 | let lastResult = notset; 19 | return (...args) => { 20 | const startTime = performanceAvailable ? performance.now() : null; 21 | const result = memoizeInstance(...args); 22 | const finishTime = performanceAvailable ? performance.now() : null; 23 | 24 | const duration = startTime != null ? finishTime - startTime : null; 25 | 26 | if (result !== lastResult || lastResult === notset) { 27 | changeCallback( 28 | lastArgs, 29 | lastResult, 30 | args, 31 | result, 32 | { changed: true, duration }, 33 | ); 34 | lastResult = result; 35 | lastArgs = args; 36 | } 37 | 38 | return result; 39 | }; 40 | } 41 | 42 | export function createSelectorWithChangeCallback(callback, ...args) { 43 | return createSelectorCreator(changeMemoize, callback)(...args); 44 | } 45 | 46 | function logNamedChange(name) { 47 | let logName = name || 'unknown'; 48 | logName = `- ${logName}`; 49 | 50 | return (lastArgs, lastResult, newArgs, newResult, { duration }) => { 51 | let fullName = logName; 52 | if (duration != null) { 53 | fullName += ` (${duration.toFixed(2)}ms)`; 54 | } 55 | 56 | /* eslint-disable no-console */ 57 | console.groupCollapsed(fullName); 58 | console.debug('lastArgs', lastArgs); 59 | console.debug('lastResult', lastResult); 60 | console.debug('newArgs', newArgs); 61 | console.debug('newResult', newResult); 62 | console.groupEnd(); 63 | /* eslint-enable no-console */ 64 | }; 65 | } 66 | 67 | export function createSelector(...args) { 68 | let name; 69 | if (typeof args[0] === 'string') { 70 | name = args.shift(); 71 | } 72 | 73 | let changeCallback; 74 | if (process.env.NODE_ENV !== 'production') { 75 | changeCallback = logNamedChange(name); 76 | } 77 | 78 | return createSelectorWithChangeCallback(changeCallback, ...args); 79 | } 80 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { performance } from 'perf_hooks'; 4 | 5 | import { createSelectorCreator } from 'reselect'; 6 | import { changeMemoize, createSelector, createSelectorWithChangeCallback } from '../src'; 7 | 8 | const setup = () => { 9 | if (global.performance == null) { 10 | global.performance = performance; 11 | } 12 | }; 13 | 14 | describe('createSelectorWithChangeCallback', () => { 15 | setup(); 16 | 17 | it('Code for README', () => { 18 | // eslint-disable-next-line no-unused-vars 19 | function myCallback(lastArgs, lastResult, newArgs, newResult) { 20 | // Your code 21 | } 22 | 23 | const selector = createSelectorWithChangeCallback( 24 | myCallback, 25 | (state) => state, 26 | (state) => { // eslint-disable-line arrow-body-style 27 | return { state }; 28 | }, 29 | ); 30 | 31 | selector({ initial: 'state' }); 32 | }); 33 | 34 | it('Should callback when a value changes', () => { 35 | let calls = 0; 36 | const selector = createSelectorWithChangeCallback( 37 | () => { calls += 1 }, 38 | (state) => state, 39 | (state) => { // eslint-disable-line arrow-body-style 40 | return { state }; 41 | }, 42 | ); 43 | 44 | let state = {}; 45 | selector(state); 46 | expect(calls).toBe(1); 47 | selector(state); 48 | expect(calls).toBe(1); 49 | state = { something: 'different' }; 50 | selector(state); 51 | expect(calls).toBe(2); 52 | }); 53 | }); 54 | 55 | describe('createSelector', () => { 56 | setup(); 57 | 58 | it('Output for README', () => { 59 | const selector1 = createSelector( 60 | 'An awesome selector', 61 | (state) => state, 62 | (state) => { // eslint-disable-line arrow-body-style 63 | return { selector1: state }; 64 | }, 65 | ); 66 | const selector2 = createSelector( 67 | 'A second awesome selector which uses the first awesome selector', 68 | selector1, 69 | (state) => { // eslint-disable-line arrow-body-style 70 | return { selector2: state }; 71 | }, 72 | ); 73 | // The name doesn't not have to be provided 74 | const selector3 = createSelector( 75 | (state) => state, 76 | (state) => { // eslint-disable-line arrow-body-style 77 | return { selector3: state }; 78 | }, 79 | ); 80 | 81 | selector2({ initial: 'state' }); 82 | selector1({ second: 'state' }); 83 | selector3({ second: 'state' }); 84 | }); 85 | }); 86 | 87 | describe('createSelectorCreator', () => { 88 | setup(); 89 | 90 | it('code for README', () => { 91 | // eslint-disable-next-line no-unused-vars 92 | function myCallback(lastArgs, lastResult, newArgs, newResult) { 93 | // Your code 94 | // eslint-disable-next-line no-console 95 | console.log(newResult); 96 | } 97 | 98 | // eslint-disable-next-line no-shadow 99 | const createSelector = createSelectorCreator(changeMemoize, myCallback); 100 | 101 | const selector = createSelector( 102 | (state) => state, 103 | (state) => { // eslint-disable-line arrow-body-style 104 | return { selector: state }; 105 | }, 106 | ); 107 | 108 | selector({ initial: 'state' }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require @babel/register 2 | --recursive 3 | -------------------------------------------------------------------------------- /test/utils/document.js: -------------------------------------------------------------------------------- 1 | if (typeof document === 'undefined') { 2 | global.document = {}; 3 | } 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | module: { 7 | rules: [{ 8 | test: /\.js$/, 9 | use: ['babel-loader'], 10 | exclude: /node_modules/ 11 | }] 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: 'reselect-change-memoize.js', 16 | library: 'reselectChangeMemoize', 17 | libraryTarget: 'umd', 18 | globalObject: 'this', 19 | }, 20 | externals: ['reselect'] 21 | }; 22 | --------------------------------------------------------------------------------