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