├── .npmrc ├── .gitignore ├── .travis.yml ├── .huskyrc ├── .lintstagedrc ├── .babelrc.js ├── rollup.config.js ├── __tests__ ├── __snapshots__ │ └── index.js.snap └── index.js ├── src └── index.js ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,md}": [ 3 | "prettier --single-quote --no-semi --trailing-comma=all --write", 4 | "git add" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/env', 5 | { 6 | loose: true, 7 | modules: false, 8 | }, 9 | ], 10 | '@babel/react', 11 | ], 12 | plugins: [ 13 | process.env.NODE_ENV === 'test' && '@babel/transform-modules-commonjs', 14 | ].filter(Boolean), 15 | } 16 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import pkg from './package.json' 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: [ 7 | { file: pkg.main, format: 'cjs' }, 8 | { file: pkg.module, format: 'esm' }, 9 | ], 10 | external: [ 11 | ...Object.keys(pkg.dependencies || {}), 12 | ...Object.keys(pkg.peerDependencies || {}), 13 | ], 14 | plugins: [babel()], 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createSelector 1`] = ` 4 | 5 | 5 6 | 7 | `; 8 | 9 | exports[`createSelector 2`] = ` 10 | 11 | 5 12 | 13 | `; 14 | 15 | exports[`createSelector 3`] = ` 16 | 17 | 7 18 | 19 | `; 20 | 21 | exports[`createStateSelector 1`] = ` 22 | 23 | 5 24 | 25 | `; 26 | 27 | exports[`createStateSelector 2`] = ` 28 | 29 | 5 30 | 31 | `; 32 | 33 | exports[`createStateSelector 3`] = ` 34 | 35 | 13 36 | 37 | `; 38 | 39 | exports[`createStateSelector 4`] = ` 40 | 41 | 13 42 | 43 | `; 44 | 45 | exports[`createStateSelector 5`] = ` 46 | 47 | 16 48 | 49 | `; 50 | 51 | exports[`createStructuredSelector 1`] = ` 52 | 53 | 5 54 | 55 | `; 56 | 57 | exports[`createStructuredSelector 2`] = ` 58 | 59 | 5 60 | 61 | `; 62 | 63 | exports[`createStructuredSelector 3`] = ` 64 | 65 | 13 66 | 67 | `; 68 | 69 | exports[`createStructuredSelector 4`] = ` 70 | 71 | 13 72 | 73 | `; 74 | 75 | exports[`createStructuredSelector 5`] = ` 76 | 77 | 16 78 | 79 | `; 80 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | 3 | const createMemoSelector = resolver => dependencies => 4 | useMemo(() => resolver(...dependencies), dependencies) 5 | 6 | const resolveResolvers = (resolvers, args) => 7 | resolvers.map(resolver => resolver(...args)) 8 | 9 | export const createSelector = resolver => { 10 | const selector = createMemoSelector(resolver) 11 | return (...dependencies) => selector(dependencies) 12 | } 13 | 14 | export const createStateSelector = (dependencyResolvers, resolver) => { 15 | const selector = createMemoSelector(resolver) 16 | return (...args) => selector(resolveResolvers(dependencyResolvers, args)) 17 | } 18 | 19 | export const createStructuredSelector = (dependencyResolversMap, resolver) => { 20 | const keys = Object.keys(dependencyResolversMap) 21 | const dependencyResolvers = keys.map(key => dependencyResolversMap[key]) 22 | return (...args) => { 23 | const dependencies = resolveResolvers(dependencyResolvers, args) 24 | return useMemo( 25 | () => 26 | resolver( 27 | keys.reduce((value, key, index) => { 28 | value[key] = dependencies[index] 29 | return value 30 | }, {}), 31 | ), 32 | dependencies, 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-selector-hooks", 3 | "version": "0.1.0", 4 | "description": "Collection of hook-based selector factories for declarations outside of render.", 5 | "main": "./dist/react-selector-hooks.js", 6 | "module": "./dist/react-selector-hooks.esm.js", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "test": "jest --env=node", 12 | "prebuild": "rimraf dist", 13 | "build": "rollup -c", 14 | "preversion": "npm test", 15 | "prepare": "npm run build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/Andarist/react-selector-hooks.git" 20 | }, 21 | "author": "Mateusz Burzyński (https://github.com/Andarist)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/Andarist/react-selector-hooks/issues" 25 | }, 26 | "homepage": "https://github.com/Andarist/react-selector-hooks#readme", 27 | "peerDependencies": { 28 | "react": "^16.7.0-alpha.0" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.1.2", 32 | "@babel/plugin-transform-modules-commonjs": "^7.1.0", 33 | "@babel/preset-env": "^7.1.0", 34 | "@babel/preset-react": "^7.0.0", 35 | "babel-core": "^7.0.0-bridge.0", 36 | "husky": "^1.1.0", 37 | "jest": "^23.6.0", 38 | "lint-staged": "^7.3.0", 39 | "prettier": "^1.14.3", 40 | "react": "16.7.0-alpha.0", 41 | "react-test-renderer": "^16.7.0-alpha.0", 42 | "rimraf": "^2.6.2", 43 | "rollup": "^0.66.6", 44 | "rollup-plugin-babel": "^4.0.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-selector-hooks 2 | 3 | Collection of hook-based memoized selector factories for declarations outside of render. 4 | 5 | ## Motivation 6 | 7 | Reusing existing functions. It also might often be desirable to declare selector functions outside of render to keep render functions less bloated. 8 | 9 | Instead of this: 10 | 11 | ```jsx 12 | function Component({ a, b }) { 13 | const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]) 14 | return {memoizedValue} 15 | } 16 | ``` 17 | 18 | You can write this: 19 | 20 | ```jsx 21 | const useSelector = createSelector(computeExpensiveValue) 22 | 23 | function Component({ a, b }) { 24 | const memoizedValue = useSelector(a, b) 25 | return {memoizedValue} 26 | } 27 | ``` 28 | 29 | ## API 30 | 31 | ### createSelector(resultFunc) 32 | 33 | ```jsx 34 | import * as React from 'react' 35 | import { createSelector } from 'react-selector-hooks' 36 | 37 | const useSelector = createSelector(computeExpensiveValue) 38 | 39 | export default function Component({ a, b }) { 40 | const memoizedValue = useSelector(a, b) 41 | return {memoizedValue} 42 | } 43 | ``` 44 | 45 | ### createStateSelector([inputSelectors], resultFunc) 46 | 47 | This is really similar to [`reselect's createSelector`](https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc). 48 | 49 | ```jsx 50 | import * as React from 'react' 51 | import { createStateSelector } from 'react-selector-hooks' 52 | 53 | const useSelector = createStateSelector( 54 | [ 55 | state => state.values.value1, 56 | (state, a) => state.values.value2 + a, 57 | (state, a) => state.values.value3 * a, 58 | ], 59 | (value1, value2, value3) => value1 + value2, 60 | ) 61 | 62 | export default function Component({ a }) { 63 | const state = React.useContext(StoreContext) 64 | const memoizedValue = useSelector(state, a) 65 | return {memoizedValue} 66 | } 67 | ``` 68 | 69 | ### createStructuredSelector({...inputSelectors}, resultFunc) 70 | 71 | This is really similar to [`reselect's createStructuredSelector`](https://github.com/reduxjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector). 72 | 73 | ```jsx 74 | import * as React from 'react' 75 | import { createStructuredSelector } from 'react-selector-hooks' 76 | 77 | const useSelector = createStructuredSelector( 78 | { 79 | value1: state => state.values.value1, 80 | value2: (state, a) => state.values.value2 + a, 81 | }, 82 | ({ value1, value2 }) => value1 + value2, 83 | ) 84 | 85 | export default function Component({ a }) { 86 | const state = React.useContext(StoreContext) 87 | const memoizedValue = useSelector(state, a) 88 | return {memoizedValue} 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TestRenderer from 'react-test-renderer' 3 | import { 4 | createSelector, 5 | createStateSelector, 6 | createStructuredSelector, 7 | } from '../src' 8 | 9 | const render = element => { 10 | const renderer = TestRenderer.create(element) 11 | return { 12 | rerender(element) { 13 | renderer.update(element) 14 | }, 15 | snapshot() { 16 | expect(renderer.toJSON()).toMatchSnapshot() 17 | }, 18 | } 19 | } 20 | 21 | test('createSelector', () => { 22 | const selector = jest.fn((a, b) => a + b) 23 | const useSelector = createSelector(selector) 24 | 25 | function Test({ a, b }) { 26 | const result = useSelector(a, b) 27 | return {result} 28 | } 29 | 30 | const { rerender, snapshot } = render() 31 | expect(selector).toBeCalledTimes(1) 32 | expect(selector).toHaveBeenCalledWith(2, 3) 33 | expect(selector).toHaveReturnedWith(5) 34 | snapshot() 35 | 36 | rerender() 37 | expect(selector).toBeCalledTimes(1) 38 | snapshot() 39 | 40 | rerender() 41 | expect(selector).toBeCalledTimes(2) 42 | expect(selector).toHaveReturnedWith(7) 43 | snapshot() 44 | }) 45 | 46 | test('createStateSelector', () => { 47 | const selector = jest.fn((a, b) => a + b) 48 | const useSelector = createStateSelector( 49 | [(state, path) => state[path], state => state.b], 50 | selector, 51 | ) 52 | 53 | function Test({ state, path }) { 54 | const result = useSelector(state, path) 55 | return {result} 56 | } 57 | 58 | const obj = { a: 2, b: 3, c: 10 } 59 | const { rerender, snapshot } = render() 60 | expect(selector).toBeCalledTimes(1) 61 | expect(selector).toHaveBeenCalledWith(2, 3) 62 | expect(selector).toHaveReturnedWith(5) 63 | snapshot() 64 | 65 | rerender() 66 | expect(selector).toBeCalledTimes(1) 67 | snapshot() 68 | 69 | rerender() 70 | expect(selector).toBeCalledTimes(2) 71 | expect(selector).toHaveBeenCalledWith(10, 3) 72 | expect(selector).toHaveReturnedWith(13) 73 | snapshot() 74 | 75 | const obj2 = { ...obj } 76 | rerender() 77 | expect(selector).toBeCalledTimes(2) 78 | snapshot() 79 | 80 | const obj3 = { ...obj2, b: 6 } 81 | rerender() 82 | expect(selector).toBeCalledTimes(3) 83 | expect(selector).toHaveBeenCalledWith(10, 6) 84 | expect(selector).toHaveReturnedWith(16) 85 | snapshot() 86 | }) 87 | 88 | test('createStructuredSelector', () => { 89 | const selector = jest.fn(({ x, y }) => x + y) 90 | const useSelector = createStructuredSelector( 91 | { 92 | x: (state, path) => state[path], 93 | y: state => state.b, 94 | }, 95 | selector, 96 | ) 97 | 98 | function Test({ state, path }) { 99 | const result = useSelector(state, path) 100 | return {result} 101 | } 102 | 103 | const obj = { a: 2, b: 3, c: 10 } 104 | const { rerender, snapshot } = render() 105 | expect(selector).toBeCalledTimes(1) 106 | expect(selector.mock.calls[0][0]).toEqual({ x: 2, y: 3 }) 107 | expect(selector).toHaveReturnedWith(5) 108 | snapshot() 109 | 110 | rerender() 111 | expect(selector).toBeCalledTimes(1) 112 | snapshot() 113 | 114 | rerender() 115 | expect(selector).toBeCalledTimes(2) 116 | expect(selector.mock.calls[1][0]).toEqual({ x: 10, y: 3 }) 117 | expect(selector).toHaveReturnedWith(13) 118 | snapshot() 119 | 120 | const obj2 = { ...obj } 121 | rerender() 122 | expect(selector).toBeCalledTimes(2) 123 | snapshot() 124 | 125 | const obj3 = { ...obj2, b: 6 } 126 | rerender() 127 | expect(selector).toBeCalledTimes(3) 128 | expect(selector.mock.calls[2][0]).toEqual({ x: 10, y: 6 }) 129 | expect(selector).toHaveReturnedWith(16) 130 | snapshot() 131 | }) 132 | --------------------------------------------------------------------------------