├── .babelrc
├── .gitignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── src
├── __tests__
│ ├── useActionCreators.spec.js
│ ├── useRedux.spec.js
│ └── useSelectors.spec.js
├── index.js
├── useActionCreators.js
├── useRedux.js
└── useSelectors.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "test": {
4 | "presets": [
5 | [
6 | "@babel/preset-env",
7 | {
8 | "targets": {
9 | "node": "current"
10 | }
11 | }
12 | ]
13 | ]
14 | },
15 | "commonjs": {
16 | "presets": [
17 | [
18 | "@babel/preset-env",
19 | {
20 | "modules": "commonjs"
21 | }
22 | ]
23 | ]
24 | },
25 | "es": {
26 | "presets": [
27 | [
28 | "@babel/preset-env",
29 | {
30 | "modules": false
31 | }
32 | ]
33 | ]
34 | }
35 | },
36 | "presets": ["@babel/preset-env", "@babel/preset-react"],
37 | "plugins": ["@babel/plugin-proposal-object-rest-spread"]
38 | }
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib/
3 | es/
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "tabWidth": 2,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "10"
4 |
5 | before_install:
6 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0
7 | - export PATH=$HOME/.yarn/bin:$PATH
8 |
9 | cache:
10 | yarn: true
11 |
12 | script:
13 | - yarn build
14 | - yarn test
15 |
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Florent Lepretre
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 | # DEPRECATED
2 |
3 | Hook to access redux
4 |
5 | :warning: [react-redux](https://react-redux.js.org/) now provide hooks support.
6 | I strongly recommend you to use the official [react-redux](https://react-redux.js.org/) implementation.
7 | If you already use this package switching to [react-redux](https://react-redux.js.org/) should not be too difficult.
8 |
9 | ## Migration to [react-redux](https://react-redux.js.org/)
10 |
11 | ### Provider
12 | First thing to do is to replace the use-redux provider by the react-redux one. Just change the import, it the provider underneath.
13 |
14 | ### useRedux
15 | The `use-redux` function was remove from the final implementation of [react-redux](https://react-redux.js.org/) hooks' so I recommend to use `useSelector` and `useDispatch`.
16 |
17 | ### useSelectors
18 | Hooks are designed to be called multiple time in the same component. If you have more than one selector, just make one call with `useSelector` for each selector.
19 |
20 | ### useActionCreators
21 | For this one you have to use `useDispatch` that give you access to the `dispatch` function.
22 | To understand the switch from bindActionCreators in connect to the dispatch function with hooks I recommend you to read [this comment from Dan Abramov](https://github.com/reduxjs/react-redux/issues/1252#issuecomment-488160930).
23 |
24 | ## Getting started
25 |
26 | ### Install
27 |
28 | ```sh
29 | yarn add use-redux
30 | ```
31 |
32 | ### useRedux
33 | You can get the redux state and the dispatch function with `useRedux` custom hooks.
34 |
35 | ```jsx
36 | import { useEffect } from 'react';
37 | import { useRedux } from 'use-redux';
38 |
39 | export const Clock = props => {
40 | const [ state, dispatch ] = useRedux();
41 |
42 | useEffect(() => {
43 | const timeout = setTimeout(
44 | () => dispatch({ type: 'SET', count: state.count + 1 }),
45 | 1000,
46 | );
47 |
48 | return () => clearTimeout(timeout);
49 | }, []);
50 |
51 | return state.count;
52 | };
53 | ```
54 |
55 | This way you can read the redux state in the `state` variable, and dispatch redux action with the `dispatch` function.
56 |
57 | If you don't have a Provider from `react-redux` in your app see the [Provider section](#provider-react-redux) below.
58 |
59 | ### with selectors and action creators
60 |
61 | Because your component should not access to the whole state and and the dispatch function, you can pass selectors and action creators to useRedux like that:
62 |
63 | ```jsx
64 | import { useRedux } from 'use-redux';
65 |
66 | // Some selectors
67 | const v1Selector = state => state.value1;
68 | const v2Selector = state => state.value2;
69 |
70 | // Some action creators
71 | const a1Creator = () => ({ type: 'FOO' });
72 | const a2Creator = payload => ({ type: 'BAR', payload });
73 |
74 | export const MyComponent = props => {
75 | const [ v1, v2, a1, a2 ] = useRedux([v1Selector, v2Selector], [a1Creator, a2Creator]);
76 | // v1 and v2 contains values selected in the redux state
77 | // a1 et a2 are function that dispatch redux actions define in creators
78 |
79 | // render stuff
80 | };
81 | ```
82 |
83 | See documentation for redux [action creators](https://redux.js.org/glossary#action-creator) and [selectors](https://react-redux.js.org/using-react-redux/connect-mapstate#use-selector-functions-to-extract-and-transform-data).
84 |
85 | ### useSelectors
86 |
87 | If you don't need to fire actions, you can just use useSelectors hook:
88 |
89 | ```jsx
90 | import { useSelectors } from 'use-redux';
91 |
92 | // Some selectors
93 | const v1Selector = state => state.value1;
94 | const v2Selector = state => state.value2;
95 |
96 | export const MyComponent = props => {
97 | const [ v1, v2 ] = useSelectors(v1Selector, v2Selector);
98 | // v1 and v2 contains values selected in the redux state
99 |
100 | return
v1: {v1} and v2: {v2}
;
101 | };
102 | ```
103 |
104 | ### useActionCreators
105 |
106 | If you don't need to read the state, you can just use useActionCreators hook:
107 | ```jsx
108 | import { useEffect } from 'react';
109 | import { useActionCreators } from 'use-redux';
110 |
111 | // Some action creators
112 | const setCreator = (payload) => ({ type: 'SET', payload });
113 | const resetCreator = () => ({ type: 'RESET' });
114 |
115 | // Hook that set things on mount and clear them when unmount
116 | export const MyCustomHook = (payload) => {
117 | const [ set, reset ] = useActionCreators(setCreator, resetCreator);
118 |
119 | useEffect(() => {
120 | set(payload);
121 |
122 | return () => reset();
123 | }, []);
124 | };
125 | ```
126 |
127 | ## Dependencies
128 | ### react-redux
129 | If you're already use [redux](https://redux.js.org/) in your app, you probably use [react-redux](https://react-redux.js.org/) to bind redux to your react app.
130 |
131 | `use-redux` uses the context of `react-redux` to access the same redux store.
132 |
133 | If you don't have `react-redux` you need to install it.
134 |
135 | ```sh
136 | yarn add react-redux
137 | ```
138 |
139 | :warning: Due to the new react context API, use-redux is only compatible with `react-redux` v6.0.0 or higher.
140 |
141 | ### Provider (react-redux)
142 |
143 | `use-redux` exports the Provider from `react-redux`
144 |
145 | First, surround your top component with the `Provider` and provide a [redux](https://redux.js.org/) store through the `store` prop.
146 |
147 | ```jsx
148 | import { createStore } from 'redux';
149 | // The provider has a slightly different name so you can easily know where it came from
150 | import { ReduxProvider } from 'use-redux';
151 | // Or directly from react-redux
152 | // import { Provider } from 'react-redux';
153 |
154 | // redux store
155 | const store = createStore(reducers)
156 |
157 | ReactDOM.render(
158 |
159 |
160 | ,
161 | document.getElementById('root')
162 | );
163 | ```
164 |
165 | See https://react-redux.js.org/api/provider for more
166 |
167 | ### connect (react-redux)
168 |
169 | `use-redux` exports the connect function from `react-redux`
170 |
171 | See https://react-redux.js.org/api/connect for
172 |
173 |
174 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after `n` failures
9 | // bail: 0,
10 |
11 | // Respect "browser" field in package.json when resolving modules
12 | // browser: false,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/hg/gk8zz05524b30v53yhv3w5_m1xy93z/T/jest_12o7zz",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | // collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: null,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: "coverage",
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | coveragePathIgnorePatterns: [
31 | "/node_modules/"
32 | ],
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: null,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: null,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // Force coverage collection from ignored files usin a array of glob patterns
52 | // forceCoverageMatch: [],
53 |
54 | // A path to a module which exports an async function that is triggered once before all test suites
55 | // globalSetup: null,
56 |
57 | // A path to a module which exports an async function that is triggered once after all test suites
58 | // globalTeardown: null,
59 |
60 | // A set of global variables that need to be available in all test environments
61 | // globals: {},
62 |
63 | // An array of directory names to be searched recursively up from the requiring module's location
64 | // moduleDirectories: [
65 | // "node_modules"
66 | // ],
67 |
68 | // An array of file extensions your modules use
69 | // moduleFileExtensions: [
70 | // "js",
71 | // "json",
72 | // "jsx",
73 | // "ts",
74 | // "tsx",
75 | // "node"
76 | // ],
77 |
78 | // A map from regular expressions to module names that allow to stub out resources with a single module
79 | // moduleNameMapper: {},
80 |
81 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
82 | // modulePathIgnorePatterns: [],
83 |
84 | // Activates notifications for test results
85 | // notify: false,
86 |
87 | // An enum that specifies notification mode. Requires { notify: true }
88 | // notifyMode: "failure-change",
89 |
90 | // A preset that is used as a base for Jest's configuration
91 | // preset: null,
92 |
93 | // Run tests from one or more projects
94 | // projects: null,
95 |
96 | // Use this configuration option to add custom reporters to Jest
97 | // reporters: undefined,
98 |
99 | // Automatically reset mock state between every test
100 | // resetMocks: false,
101 |
102 | // Reset the module registry before running each individual test
103 | // resetModules: false,
104 |
105 | // A path to a custom resolver
106 | // resolver: null,
107 |
108 | // Automatically restore mock state between every test
109 | // restoreMocks: false,
110 |
111 | // The root directory that Jest should scan for tests and modules within
112 | // rootDir: null,
113 |
114 | // A list of paths to directories that Jest should use to search for files in
115 | // roots: [
116 | // ""
117 | // ],
118 |
119 | // Allows you to use a custom runner instead of Jest's default test runner
120 | // runner: "jest-runner",
121 |
122 | // The paths to modules that run some code to configure or set up the testing environment before each test
123 | // setupFiles: [],
124 |
125 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
126 | // setupFilesAfterEnv: [],
127 |
128 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
129 | // snapshotSerializers: [],
130 |
131 | // The test environment that will be used for testing
132 | testEnvironment: "node",
133 |
134 | // Options that will be passed to the testEnvironment
135 | // testEnvironmentOptions: {},
136 |
137 | // Adds a location field to test results
138 | // testLocationInResults: false,
139 |
140 | // The glob patterns Jest uses to detect test files
141 | // testMatch: [
142 | // "**/__tests__/**/*.[jt]s?(x)",
143 | // "**/?(*.)+(spec|test).[tj]s?(x)"
144 | // ],
145 |
146 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
147 | testPathIgnorePatterns: [
148 | "/node_modules/"
149 | ],
150 |
151 | // The regexp pattern or array of patterns that Jest uses to detect test files
152 | // testRegex: [],
153 |
154 | // This option allows the use of a custom results processor
155 | // testResultsProcessor: null,
156 |
157 | // This option allows use of a custom test runner
158 | // testRunner: "jasmine2",
159 |
160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
161 | // testURL: "http://localhost",
162 |
163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
164 | // timers: "real",
165 |
166 | // A map from regular expressions to paths to transformers
167 | // transform: null,
168 |
169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
170 | transformIgnorePatterns: [
171 | "/node_modules/"
172 | ],
173 |
174 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
175 | // unmockedModulePathPatterns: undefined,
176 |
177 | // Indicates whether each individual test should be reported during the run
178 | // verbose: null,
179 |
180 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
181 | // watchPathIgnorePatterns: [],
182 |
183 | // Whether to use watchman for file crawling
184 | // watchman: true,
185 | };
186 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-redux",
3 | "version": "2.2.1",
4 | "description": "React binding for Redux using react's hooks",
5 | "main": "./lib/index.js",
6 | "module": "es/index.js",
7 | "files": [
8 | "lib/",
9 | "es/"
10 | ],
11 | "author": "Florent Lepretre ",
12 | "license": "MIT",
13 | "repository": "https://github.com/flepretre/use-redux",
14 | "keywords": [
15 | "redux",
16 | "react",
17 | "hooks",
18 | "state"
19 | ],
20 | "devDependencies": {
21 | "@babel/cli": "7.2.3",
22 | "@babel/core": "7.3.3",
23 | "@babel/plugin-proposal-object-rest-spread": "7.3.2",
24 | "@babel/preset-env": "7.3.1",
25 | "@babel/preset-react": "7.0.0",
26 | "@m6web/eslint-plugin-react": "3.2.0",
27 | "babel-jest": "24.1.0",
28 | "jest": "24.1.0",
29 | "react": "16.8.3",
30 | "react-redux": "6.0.1",
31 | "redux": "4.0.1"
32 | },
33 | "peerDependencies": {
34 | "react": "^16.8.1",
35 | "react-redux": "^6.0.0"
36 | },
37 | "scripts": {
38 | "build": "yarn build:commonjs && yarn build:es",
39 | "build:commonjs": "BABEL_ENV=commonjs babel src --out-dir lib --ignore src/__tests__/**/*",
40 | "build:es": "BABEL_ENV=es babel src --out-dir es --ignore src/__tests__/**/*",
41 | "format": "prettier-eslint --write 'src/**/*.js'",
42 | "prepublishOnly": "rm -rf es lib && yarn build",
43 | "test": "BABEL_ENV=test jest"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/__tests__/useActionCreators.spec.js:
--------------------------------------------------------------------------------
1 | import { useActionCreators } from '../useActionCreators';
2 | import { useContext } from 'react';
3 | import { ReactReduxContext } from 'react-redux';
4 |
5 | jest.mock('react');
6 | jest.mock('react-redux');
7 |
8 | describe('useActionCreators', () => {
9 | const dispatch = jest.fn(x => x);
10 | const contextMock = {
11 | store: { dispatch }
12 | };
13 |
14 | beforeEach(() => {
15 | useContext.mockReturnValue(contextMock);
16 | });
17 |
18 | const a1Creator = () => ({ type: 'FOO' });
19 | const a2Creator = payload => ({ type: 'BAR', payload });
20 | const a3Creator = (name, id) => ({ type: 'NAMEID', name, id });
21 |
22 | it('should return actions', () => {
23 | const actions = useActionCreators(a1Creator, a2Creator);
24 |
25 | expect(actions.length).toBe(2);
26 | expect(useContext).toHaveBeenCalledWith(ReactReduxContext);
27 |
28 | const [a1, a2] = actions;
29 |
30 | a1();
31 | expect(dispatch).toHaveBeenCalledWith({ type: 'FOO' });
32 |
33 | a2('payload');
34 | expect(dispatch).toHaveBeenCalledWith({ type: 'BAR', payload: 'payload' });
35 | });
36 |
37 | it('shoud allow action creators with more than one parameter', () => {
38 | const actions = useActionCreators(a3Creator);
39 |
40 | const [a3] = actions;
41 |
42 | a3('skywalker', 1);
43 | expect(dispatch).toHaveBeenCalledWith({
44 | type: 'NAMEID',
45 | name: 'skywalker',
46 | id: 1
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/src/__tests__/useRedux.spec.js:
--------------------------------------------------------------------------------
1 | import { useRedux } from '../useRedux';
2 | import { useState, useEffect, useContext } from 'react';
3 | import { ReactReduxContext } from 'react-redux';
4 |
5 | jest.mock('react');
6 | jest.mock('react-redux');
7 |
8 | describe('useRedux', () => {
9 | // Hooks mocks
10 | let hookState;
11 |
12 | const setState = jest.fn(value => {
13 | hookState = value;
14 | });
15 |
16 | const useStateMock = defaultValue => {
17 | if (!hookState && hookState !== 0) {
18 | hookState = defaultValue;
19 | }
20 |
21 | return [hookState, setState];
22 | };
23 |
24 | const useEffectMock = callback => {
25 | const cleanUp = callback();
26 |
27 | if (typeof cleanUp === 'function') {
28 | cleanUp();
29 | }
30 | };
31 |
32 | // React-redux mocks
33 | let reduxState;
34 | let reduxStoreListeners;
35 | const getState = jest.fn();
36 | const dispatch = jest.fn(x => x);
37 | const unSubscribe = jest.fn();
38 | const runStoreListeners = () =>
39 | reduxStoreListeners.forEach(listener => listener());
40 | const subscribeMock = listener => {
41 | reduxStoreListeners.push(listener);
42 |
43 | return unSubscribe;
44 | };
45 | const subscribe = jest.fn(subscribeMock);
46 |
47 | const reactReduxContext = {
48 | store: { getState, dispatch, subscribe }
49 | };
50 |
51 | const reduxInitialState = {
52 | foo: 'bar',
53 | bar: 'foo bar'
54 | };
55 |
56 | beforeEach(() => {
57 | hookState = undefined;
58 | reduxState = reduxInitialState;
59 | reduxStoreListeners = [];
60 | useState.mockImplementation(useStateMock);
61 | useEffect.mockImplementation(useEffectMock);
62 | useContext.mockReturnValue(reactReduxContext);
63 | getState.mockImplementation(() => reduxState);
64 | });
65 |
66 | it('should return the current state and dispatch function', () => {
67 | const [state, dispatch] = useRedux();
68 |
69 | expect(useContext).toHaveBeenCalledWith(ReactReduxContext);
70 | expect(getState).toHaveBeenCalled();
71 | expect(useState).toHaveBeenCalledWith(reduxInitialState);
72 |
73 | expect(state).toEqual(reduxInitialState);
74 | expect(dispatch).toBe(dispatch);
75 | });
76 |
77 | it('should subscribe to the store', () => {
78 | useRedux();
79 |
80 | expect(useEffect).toHaveBeenCalled();
81 | expect(subscribe).toHaveBeenCalled();
82 | expect(setState).not.toHaveBeenCalled();
83 | });
84 |
85 | it('should unsubscribe to the store', () => {
86 | useRedux();
87 |
88 | expect(useEffect).toHaveBeenCalled();
89 | expect(unSubscribe).toHaveBeenCalled();
90 | expect(setState).not.toHaveBeenCalled();
91 | });
92 |
93 | it('should use selectors', () => {
94 | const getFoo = ({ foo }) => foo;
95 | const getBar = ({ bar }) => bar;
96 |
97 | const [foo, bar, dispatch] = useRedux([getFoo, getBar]);
98 |
99 | expect(useContext).toHaveBeenCalledWith(ReactReduxContext);
100 | expect(getState).toHaveBeenCalled();
101 | expect(useState).toHaveBeenCalledWith(['bar', 'foo bar']);
102 |
103 | expect(foo).toBe('bar');
104 | expect(bar).toBe('foo bar');
105 |
106 | expect(dispatch).toBe(dispatch);
107 | });
108 |
109 | it('should use action creators', () => {
110 | const fooActionCreator = jest.fn(x => x);
111 | const barActionCreator = jest.fn(x => x);
112 |
113 | const [state, fooAction, barAction] = useRedux(undefined, [
114 | fooActionCreator,
115 | barActionCreator
116 | ]);
117 |
118 | expect(useContext).toHaveBeenCalledWith(ReactReduxContext);
119 | expect(getState).toHaveBeenCalled();
120 | expect(useState).toHaveBeenCalledWith(reduxInitialState);
121 |
122 | expect(state).toEqual(reduxInitialState);
123 |
124 | expect(fooAction('yolo')).toBe('yolo');
125 | expect(fooActionCreator).toHaveBeenCalledWith('yolo');
126 | expect(dispatch).toHaveBeenCalledWith('yolo');
127 |
128 | expect(barAction(2)).toBe(2);
129 | expect(barActionCreator).toHaveBeenCalledWith(2);
130 | expect(dispatch).toHaveBeenCalledWith(2);
131 | });
132 |
133 | it('should use selectors and action creators', () => {
134 | const getFoo = ({ foo }) => foo;
135 | const getBar = ({ bar }) => bar;
136 |
137 | const fooActionCreator = jest.fn(x => x);
138 | const barActionCreator = jest.fn(x => x);
139 |
140 | const [foo, bar, fooAction, barAction] = useRedux(
141 | [getFoo, getBar],
142 | [fooActionCreator, barActionCreator]
143 | );
144 |
145 | expect(useContext).toHaveBeenCalledWith(ReactReduxContext);
146 | expect(getState).toHaveBeenCalled();
147 | expect(useState).toHaveBeenCalledWith(['bar', 'foo bar']);
148 | expect(foo).toBe('bar');
149 | expect(bar).toBe('foo bar');
150 |
151 | expect(fooAction('yolo')).toBe('yolo');
152 | expect(fooActionCreator).toHaveBeenCalledWith('yolo');
153 | expect(dispatch).toHaveBeenCalledWith('yolo');
154 |
155 | expect(barAction(2)).toBe(2);
156 | expect(barActionCreator).toHaveBeenCalledWith(2);
157 | expect(dispatch).toHaveBeenCalledWith(2);
158 | });
159 |
160 | it('should not call setState if selected values stay the same', () => {
161 | const getFoo = ({ foo }) => foo;
162 | const getBar = ({ bar }) => bar;
163 |
164 | const [foo, bar, dispatch] = useRedux([getFoo, getBar]);
165 |
166 | expect(setState).not.toHaveBeenCalled();
167 |
168 | reduxState = { ...reduxState, a: 'b' };
169 | runStoreListeners();
170 |
171 | expect(setState).not.toHaveBeenCalled();
172 | expect(foo).toBe('bar');
173 | expect(bar).toBe('foo bar');
174 |
175 | expect(dispatch).toBe(dispatch);
176 | });
177 |
178 | it('should call setState if selected values changed', () => {
179 | const getFoo = ({ foo }) => foo;
180 | const getBar = ({ bar }) => bar;
181 |
182 | const [foo, bar, dispatch] = useRedux([getFoo, getBar]);
183 |
184 | expect(setState).not.toHaveBeenCalled();
185 |
186 | reduxState = { ...reduxState, foo: 'foo' };
187 | runStoreListeners();
188 |
189 | expect(setState).toHaveBeenCalledWith(['foo', 'foo bar']);
190 | expect(foo).toBe('bar');
191 | expect(bar).toBe('foo bar');
192 |
193 | expect(dispatch).toBe(dispatch);
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/src/__tests__/useSelectors.spec.js:
--------------------------------------------------------------------------------
1 | import { useRedux } from '../useRedux';
2 | import { useSelectors } from '../useSelectors';
3 |
4 | jest.mock('../useRedux', () => ({
5 | useRedux: jest.fn(selectors => [
6 | ...selectors.map(select => select({ foo: 'bar', bar: 'foo bar' })),
7 | 'dispatch'
8 | ])
9 | }));
10 | jest.unmock('../useSelectors');
11 |
12 | describe('useSelectors', () => {
13 | it('should return selected values', () => {
14 | const s1 = ({ foo }) => foo;
15 | const s2 = ({ bar }) => bar;
16 |
17 | const selectedValues = useSelectors(s1, s2);
18 | expect(selectedValues).toEqual(['bar', 'foo bar']);
19 | expect(useRedux).toHaveBeenCalledWith([s1, s2]);
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { Provider as ReduxProvider, connect } from 'react-redux';
2 | export { useRedux } from './useRedux';
3 | export { useActionCreators } from './useActionCreators';
4 | export { useSelectors } from './useSelectors';
5 |
--------------------------------------------------------------------------------
/src/useActionCreators.js:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { ReactReduxContext } from 'react-redux';
3 |
4 | export const useActionCreators = (...actionCreators) => {
5 | const { store } = useContext(ReactReduxContext);
6 | const { dispatch } = store;
7 |
8 | return actionCreators.map(actionCreator => (...params) =>
9 | dispatch(actionCreator(...params))
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/src/useRedux.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from 'react';
2 | import { ReactReduxContext } from 'react-redux';
3 |
4 | export const useRedux = (selectors, actionCreators) => {
5 | const { store } = useContext(ReactReduxContext);
6 | const { getState, dispatch, subscribe } = store;
7 | const withSelectors = selectors && selectors.length;
8 | const reduxState = getState();
9 |
10 | let values;
11 | let actions;
12 |
13 | if (withSelectors) {
14 | values = selectors.map(selector => selector(reduxState));
15 | }
16 |
17 | if (actionCreators && actionCreators.length) {
18 | actions = actionCreators.map(actionCreator => params =>
19 | dispatch(actionCreator(params))
20 | );
21 | }
22 |
23 | const [state, setState] = useState(values || reduxState);
24 |
25 | const updateState = () => {
26 | const newReduxState = getState();
27 |
28 | if (withSelectors) {
29 | let hasChanged = false;
30 | const newValues = [];
31 |
32 | for (let i = 0; i < selectors.length; i++) {
33 | newValues.push(selectors[i](newReduxState));
34 | hasChanged |= newValues[i] !== values[i];
35 | }
36 |
37 | if (hasChanged) {
38 | // Only rerender if selected values have changed
39 | setState(newValues);
40 | }
41 | } else {
42 | setState(newReduxState);
43 | }
44 | };
45 |
46 | useEffect(() => subscribe(updateState), []);
47 |
48 | return [].concat(state, actions || dispatch);
49 | };
50 |
--------------------------------------------------------------------------------
/src/useSelectors.js:
--------------------------------------------------------------------------------
1 | import { useRedux } from './useRedux';
2 |
3 | export const useSelectors = (...selectors) =>
4 | useRedux(selectors).slice(0, selectors.length);
5 |
--------------------------------------------------------------------------------