├── .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 |
--------------------------------------------------------------------------------