├── .babelrc ├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── package.json ├── src ├── index.js └── shallowEqual.js ├── storybook ├── __conf__ │ ├── mockConfig.js │ ├── polyfill.js │ └── setup.js ├── __mocks__ │ ├── facade.js │ └── file.js ├── addons.js ├── config.js ├── examples │ ├── TodoList │ │ ├── __tests__ │ │ │ └── TodoList.stories.js │ │ ├── actions │ │ │ └── index.js │ │ ├── components │ │ │ ├── Footer.js │ │ │ └── Todo.js │ │ ├── containers │ │ │ ├── AddTodo.js │ │ │ ├── FilterLink.js │ │ │ └── VisibleTodoList.js │ │ ├── index.js │ │ └── reducers │ │ │ ├── index.js │ │ │ ├── todos.js │ │ │ └── visibilityFilter.js │ └── ToggleButton │ │ ├── __tests__ │ │ ├── ToggleButton.spec.js │ │ ├── ToggleButton.stories.js │ │ └── __snapshots__ │ │ │ ├── ToggleButton.spec.js.snap │ │ │ └── ToggleButton.stories.js.snap │ │ └── index.js └── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "loose": true, 9 | "useBuiltIns": "entry", 10 | "modules": "commonjs" 11 | } 12 | ], 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [ 16 | "@babel/plugin-syntax-dynamic-import", 17 | "@babel/plugin-syntax-import-meta", 18 | "@babel/plugin-proposal-class-properties", 19 | "@babel/plugin-proposal-json-strings" 20 | ] 21 | }, 22 | "es": { 23 | "presets": [ 24 | [ 25 | "@babel/preset-env", 26 | { 27 | "loose": true, 28 | "useBuiltIns": "entry", 29 | "modules": false 30 | } 31 | ], 32 | "@babel/preset-react" 33 | ], 34 | "plugins": [ 35 | "@babel/plugin-syntax-dynamic-import", 36 | "@babel/plugin-syntax-import-meta", 37 | "@babel/plugin-proposal-class-properties", 38 | "@babel/plugin-proposal-json-strings" 39 | ] 40 | }, 41 | "cjs": { 42 | "presets": [ 43 | [ 44 | "@babel/preset-env", 45 | { 46 | "loose": true, 47 | "useBuiltIns": "entry", 48 | "modules": "commonjs" 49 | } 50 | ], 51 | "@babel/preset-react" 52 | ], 53 | "plugins": [ 54 | "@babel/plugin-syntax-dynamic-import", 55 | "@babel/plugin-syntax-import-meta", 56 | "@babel/plugin-proposal-class-properties", 57 | "@babel/plugin-proposal-json-strings" 58 | ] 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | ie 11 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/* 3 | coverage/* 4 | _public/* 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true, 7 | "jest": true, 8 | "jasmine": true 9 | }, 10 | "parser": "babel-eslint", 11 | "plugins": [ 12 | "import", 13 | "react" 14 | ], 15 | "parserOptions": { 16 | "ecmaVersion": 6, 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true, 20 | "experimentalObjectRestSpread": true 21 | } 22 | }, 23 | "rules": { 24 | "arrow-parens": ["error", "as-needed"], 25 | "class-methods-use-this": 0, 26 | "no-multi-spaces": ["error", { "exceptions": { "ImportDeclaration": true } }], 27 | "indent": ["error", "tab", { "SwitchCase": 1 }], 28 | "no-tabs": 0, 29 | "no-console": 0, 30 | "no-underscore-dangle": 0, 31 | "no-mixed-operators": ["error", { "allowSamePrecedence": true }], 32 | "function-paren-newline": ["error", "consistent"], 33 | "object-curly-newline": ["error", { "consistent": true }], 34 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "_" }], 35 | "react/jsx-indent": [2, "tab"], 36 | "react/jsx-indent-props": [2, "tab"], 37 | "react/prop-types": 0, 38 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 39 | "react/sort-comp": [1, { 40 | "order": [ 41 | "static-methods", 42 | "lifecycle", 43 | "everything-else", 44 | "render" 45 | ] 46 | }], 47 | "import/prefer-default-export": 0, 48 | "import/no-extraneous-dependencies": 0, 49 | "import/no-unresolved": 0, 50 | "import/extensions": 0, 51 | "jsx-a11y/anchor-is-valid": ["error", { 52 | "components": [], 53 | "specialLink": [], 54 | "aspects": ["noHref", "invalidHref", "preferButton"] 55 | }] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | dist-es/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # node 2 | node_modules/ 3 | 4 | # Else 5 | .editorconfig 6 | .travis.yml 7 | .eslintrc 8 | .eslintignore 9 | .prettierignore 10 | .prettierrc 11 | .travis.yml 12 | *.log 13 | coverage/ 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _public 3 | coverage 4 | library 5 | .travis.yml 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "parser": "babylon" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '8' 5 | 6 | sudo: false 7 | 8 | cache: yarn 9 | 10 | git: 11 | depth: 1 12 | 13 | script: 14 | - "yarn test" 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | # [0.3.0](https://github.com/jessy1092/react-redux-hooks/compare/v0.2.0...v0.3.0) (2019-02-10) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * Add pure and shouldHooksUpdate options for performance and customize. issue [#2](https://github.com/jessy1092/react-redux-hooks/issues/2) ([ae9ea9f](https://github.com/jessy1092/react-redux-hooks/commit/ae9ea9f)) 12 | * Update the peerDependencies to support react 16.8.0 for hooks official release ([02d8978](https://github.com/jessy1092/react-redux-hooks/commit/02d8978)) 13 | 14 | 15 | 16 | 17 | # [0.2.0](https://github.com/jessy1092/react-redux-hooks/compare/v0.1.1...v0.2.0) (2019-01-09) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * Set react and react-dom 16.7.0 as the peer dependencies ([0d90a45](https://github.com/jessy1092/react-redux-hooks/commit/0d90a45)) 23 | 24 | 25 | ### Features 26 | 27 | * Implement selector and bin actions method. issue [#3](https://github.com/jessy1092/react-redux-hooks/issues/3) ([bcf7e58](https://github.com/jessy1092/react-redux-hooks/commit/bcf7e58)) 28 | 29 | 30 | 31 | 32 | ## [0.1.1](https://github.com/jessy1092/react-redux-hooks/compare/v0.1.0...v0.1.1) (2018-12-12) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * Get redux state before first render ([c9a8477](https://github.com/jessy1092/react-redux-hooks/commit/c9a8477)) 38 | 39 | 40 | 41 | 42 | # 0.1.0 (2018-10-26) 43 | 44 | 45 | ### Features 46 | 47 | * Implement the basic react-redux-hook ([0690e3a](https://github.com/jessy1092/react-redux-hooks/commit/0690e3a)) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-redux-hooks 2 | ===== 3 | 4 | [![Commitizen friendly][commitizen-image]][commitizen-image] [![Standard Version][standard-version-image]][standard-version-url] [![npm][npm-image]][npm-url] [![Dependency Status][david-dm-image]][david-dm-url] 5 | 6 | The easiest way to connect redux. Power by [react hooks](https://reactjs.org/docs/hooks-intro.html). 7 | 8 | ## Getting Started 9 | 10 | ### Install 11 | 12 | ``` 13 | npm install react-redux-hooks 14 | ``` 15 | 16 | or 17 | 18 | ``` 19 | yarn add react-redux-hooks 20 | ``` 21 | 22 | ### Usage 23 | 24 | [See full examples](https://github.com/jessy1092/react-redux-hooks/tree/master/storybook/examples) 25 | 26 | #### Connect to redux in component 27 | 28 | Just use `useRedux`. It would return `state` and `dispatch` 29 | 30 | ```javascript 31 | import { useRedux } from 'react-redux-hooks'; 32 | 33 | const ToggleButton = () => { 34 | const [state, dispatch] = useRedux(); 35 | 36 | return ( 37 | 40 | ); 41 | }; 42 | ``` 43 | 44 | #### Top level Provider 45 | 46 | Just pass redux store with `Provider` like `react-redux`. 47 | 48 | ```javascript 49 | import React from 'react'; 50 | import { createStore } from 'redux'; 51 | import { Provider, useRedux } from 'react-redux-hooks'; 52 | 53 | const store = createStore((state = { toggle: false }, action) => { 54 | if (action.type === 'TOGGLE') { 55 | return { toggle: !state.toggle }; 56 | } 57 | 58 | return state; 59 | }); 60 | 61 | ReactDOM.render( 62 | 63 | 64 | , 65 | document.getElementById('content'), 66 | ); 67 | 68 | ``` 69 | 70 | ### Advanced usage 71 | 72 | Just like [react-redux](https://react-redux.js.org). We combine Selector, and actions creator in react-redux-hooks 73 | 74 | 75 | ```javascript 76 | function useRedux(mapStateToHook?, mapDispatchToHook?, options?) 77 | ``` 78 | 79 | useRedux accepts three different parameters, all optional. By convention, they are called: 80 | 81 | - mapStateToHook?: Function 82 | - mapDispatchToHook?: Function | Object 83 | - options?: Object 84 | 85 | 86 | #### Define `mapStateToHook` 87 | 88 | Just like [mapStateToProps](https://react-redux.js.org/using-react-redux/connect-mapstate). 89 | 90 | `mapStateToHook` should be defined as a function: 91 | 92 | ```javascript 93 | function mapStateToHook(state) 94 | ``` 95 | 96 | Arguments 97 | - `state` is the `store.getState()` return value 98 | 99 | Return 100 | - Must return plain object 101 | 102 | Example: 103 | 104 | ```javascript 105 | const mapStateToHook = (state) => state.toggle; 106 | 107 | const [toggle, dispatch] = useRedux(mapStateToHook); 108 | ``` 109 | 110 | #### Define `mapDispatchToHook` 111 | 112 | Just like [mapDispatchToProps](https://react-redux.js.org/using-react-redux/connect-mapdispatch). 113 | 114 | `mapDispatchToHook` Could defined as the three types: 115 | - `undefined` would return `dispatch` on hook by default 116 | - `function` would pass `dispatch` as the function parameter for user customize 117 | - `object` would combine [redux's bindActionCreators](https://redux.js.org/api/bindactioncreators) by default 118 | 119 | ##### Define `mapDispatchToHook` as the `undefined` 120 | 121 | Return `dispatch` on hook by default. 122 | 123 | Example: 124 | ```javascript 125 | const [, dispatch] = useRedux(); 126 | ``` 127 | 128 | ##### Define `mapDispatchToHook` as the `function` 129 | 130 | Pass `dispatch` as the function parameter for user customize. 131 | 132 | Example: 133 | ```javascript 134 | const mapDispatchToHook = (dispatch) => dispatch({ type: 'TOGGLE' }); 135 | 136 | const [, onToggle] = useRedux(undefined, mapDispatchToHook); 137 | ``` 138 | 139 | ##### Define `mapDispatchToHook` as the `object` 140 | 141 | Combine [redux's bindActionCreators](https://redux.js.org/api/bindactioncreators) by default 142 | 143 | Example: 144 | ```javascript 145 | const onToggleAction = () => ({ type: 'TOGGLE' }); 146 | 147 | const mapDispatchToHook = { onToggle: onToggleAction }; 148 | 149 | const [, onToggle] = useRedux(undefined, mapDispatchToHook); 150 | ``` 151 | 152 | [More example](https://github.com/jessy1092/react-redux-hooks/blob/master/storybook/examples/TodoList/containers/AddTodo.js) 153 | 154 | 155 | #### Define `options` as the `object` 156 | 157 | There are two options can be set: 158 | 159 | ``` 160 | { 161 | pure?: boolean, 162 | shouldHooksUpdate?: function 163 | } 164 | ``` 165 | 166 | - `pure`: When `pure` is `true`, `useRedux` performs several equality checks that are used to avoid unnecessary calls to change state, and ultimately to render. It uses [shallowEqual](https://github.com/jessy1092/react-redux-hooks/blob/master/src/shallowEqual.js) to compare state/prevState. When `pure` is `false`, update state everytime or update state on `shouldHooksUpdate` return `true`. 167 | - `shouldHooksUpdate: (nextState, prevState) => boolean`: You could customize update function. It only works on `pure` is `false`. 168 | 169 | ## Roadmap 170 | 171 | - [x] Shallow compare 172 | - [x] Customize Selector 173 | 174 | Discussion welcome to [open issue](https://github.com/jessy1092/react-redux-hooks/issues) 175 | 176 | 177 | ## Release Notes 178 | 179 | see [CHANGELOG.md](https://github.com/jessy1092/react-redux-hooks/blob/master/CHANGELOG.md) 180 | 181 | 182 | ## Contribute 183 | [![devDependency Status][david-dm-dev-image]][david-dm-dev-url] 184 | 185 | 1. Fork it. 186 | 2. Create your feature-branch `git checkout -b your-new-feature-branch` 187 | 3. Commit your change `git commit -am 'Add new feature'` 188 | 4. Push to the branch `git push origin your-new-feature-branch` 189 | 5. Create new Pull Request with `master` branch 190 | 191 | ### Commit Message Style 192 | 193 | Please following [angular format](https://github.com/ajoslin/conventional-changelog/blob/master/conventions/angular.md). 194 | 195 | ## License 196 | 197 | The MIT License (MIT) 198 | 199 | Copyright (c) 2018 Lee < jessy1092@gmail.com > 200 | 201 | Permission is hereby granted, free of charge, to any person obtaining a copy of 202 | this software and associated documentation files (the "Software"), to deal in 203 | the Software without restriction, including without limitation the rights to 204 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 205 | the Software, and to permit persons to whom the Software is furnished to do so, 206 | subject to the following conditions: 207 | 208 | The above copyright notice and this permission notice shall be included in all 209 | copies or substantial portions of the Software. 210 | 211 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 212 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 213 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 214 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 215 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 216 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 217 | 218 | [commitizen-image]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?style=flat-square 219 | [commitizen-url]: http://commitizen.github.io/cz-cli/ 220 | 221 | [standard-version-image]: https://img.shields.io/badge/release-standard%20version-brightgreen.svg?style=flat-square 222 | [standard-version-url]: https://github.com/conventional-changelog/standard-version 223 | 224 | [npm-image]: https://img.shields.io/npm/v/react-redux-hooks.svg?style=flat-square 225 | [npm-url]: https://www.npmjs.com/package/react-redux-hooks 226 | 227 | [david-dm-image]: https://david-dm.org/jessy1092/react-redux-hooks.svg?style=flat-square 228 | [david-dm-url]: https://david-dm.org/jessy1092/react-redux-hooks 229 | 230 | [david-dm-dev-image]: https://david-dm.org/jessy1092/react-redux-hooks/dev-status.svg?style=flat-square 231 | [david-dm-dev-url]: https://david-dm.org/jessy1092/react-redux-hooks#info=devDependencies 232 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-hooks", 3 | "description": "The easiest way to connect redux. Power by react hooks", 4 | "version": "0.3.0", 5 | "keywords": [ 6 | "react", 7 | "redux", 8 | "react hooks", 9 | "hook" 10 | ], 11 | "contributor": [ 12 | { 13 | "name": "Lee", 14 | "email": "jessy1092@gmail.com" 15 | } 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:jessy1092/react-redux-hooks.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/jessy1092/react-redux-hooks/issues" 23 | }, 24 | "license": "MIT", 25 | "main": "dist/index.js", 26 | "module": "dist-es/index.js", 27 | "jsnext:main": "dist-es/index.js", 28 | "scripts": { 29 | "start": "npm run storybook", 30 | "build": "cross-env BABEL_ENV=cjs babel src -d dist", 31 | "build:es": "cross-env BABEL_ENV=es babel src -d dist-es", 32 | "lint": "eslint . && stylelint .", 33 | "storybook": "start-storybook -p 8000 -c storybook", 34 | "build:storybook": "build-storybook -c storybook -o _public/storybook", 35 | "test": "jest --coverage", 36 | "commit": "git-cz", 37 | "release": "standard-version", 38 | "prepublish": "yarn build && yarn build:es", 39 | "format": "prettier --config ./.prettierrc --write \"./{,**/}/*.{js,css,json}\" && eslint --fix ." 40 | }, 41 | "engines": { 42 | "node": ">=6" 43 | }, 44 | "peerDependencies": { 45 | "react": "^16.8.0", 46 | "react-dom": "^16.8.0", 47 | "redux": "^4.0.1" 48 | }, 49 | "devDependencies": { 50 | "@babel/cli": "^7.0.0", 51 | "@babel/core": "^7.0.0", 52 | "@babel/node": "^7.0.0", 53 | "@babel/plugin-proposal-class-properties": "^7.0.0", 54 | "@babel/plugin-proposal-json-strings": "^7.0.0", 55 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 56 | "@babel/plugin-syntax-import-meta": "^7.0.0", 57 | "@babel/plugin-transform-react-constant-elements": "^7.0.0", 58 | "@babel/preset-env": "^7.0.0", 59 | "@babel/preset-react": "^7.0.0", 60 | "@babel/register": "^7.0.0", 61 | "@storybook/addon-actions": "^4.0.0-alpha.20", 62 | "@storybook/addon-knobs": "^4.0.0-alpha.20", 63 | "@storybook/addon-links": "^4.0.0-alpha.20", 64 | "@storybook/react": "^4.0.0-alpha.20", 65 | "babel-core": "^7.0.0-bridge.0", 66 | "babel-eslint": "^9.0.0", 67 | "babel-jest": "^22.0.4", 68 | "babel-loader": "^8.0.0", 69 | "commitizen": "^2.9.6", 70 | "connect-history-api-fallback": "^1.5.0", 71 | "cross-env": "^5.1.3", 72 | "cz-conventional-changelog": "^2.0.0", 73 | "enzyme": "^3.8.0", 74 | "enzyme-adapter-react-16": "^1.9.1", 75 | "enzyme-to-json": "^3.3.5", 76 | "eslint": "^4.14.0", 77 | "eslint-config-airbnb": "^16.1.0", 78 | "eslint-plugin-import": "^2.8.0", 79 | "eslint-plugin-jsx-a11y": "^6.0.3", 80 | "eslint-plugin-react": "^7.5.1", 81 | "express": "^4.15.3", 82 | "jest": "^22.0.4", 83 | "optimize-css-assets-webpack-plugin": "^5.0.1", 84 | "prettier": "^1.9.2", 85 | "prop-types": "^15.6.2", 86 | "react": "^16.8.1", 87 | "react-dom": "^16.8.1", 88 | "react-hot-loader": "^3.1.3", 89 | "react-test-renderer": "^16.8.1", 90 | "redux": "^4.0.1", 91 | "redux-logger": "^3.0.6", 92 | "redux-mock-store": "^1.4.0", 93 | "standard-version": "^4.3.0", 94 | "webpack": "^4.17.1", 95 | "webpack-cli": "^3.1.0", 96 | "webpack-dev-middleware": "^3.2.0", 97 | "webpack-hot-middleware": "^2.23.0" 98 | }, 99 | "jest": { 100 | "testURL": "http://localhost", 101 | "roots": [ 102 | "/src/", 103 | "/storybook/" 104 | ], 105 | "setupFiles": [ 106 | "/storybook/__conf__/polyfill.js", 107 | "/storybook/__conf__/setup.js" 108 | ], 109 | "testMatch": [ 110 | "**/src/**/*.stories.js", 111 | "**/src/**/*.spec.js", 112 | "**/storybook/**/*.spec.js", 113 | "**/storybook/**/*.spec.js" 114 | ], 115 | "automock": false, 116 | "globals": { 117 | "__TESTS__": true 118 | }, 119 | "unmockedModulePathPatterns": [ 120 | "/node_modules/react/", 121 | "/node_modules/react-dom/", 122 | "/node_modules/enzyme/", 123 | "/node_modules/react-addons-test-utils/" 124 | ], 125 | "moduleNameMapper": { 126 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/storybook/__mocks__/file.js", 127 | "@storybook/react": "/storybook/__mocks__/facade.js", 128 | "\\.jest-test-results.json$": "/storybook/__mocks__/file.js" 129 | }, 130 | "coveragePathIgnorePatterns": [ 131 | "/storybook/", 132 | "/node_modules/" 133 | ], 134 | "snapshotSerializers": [ 135 | "enzyme-to-json/serializer" 136 | ] 137 | }, 138 | "config": { 139 | "commitizen": { 140 | "path": "./node_modules/cz-conventional-changelog" 141 | } 142 | }, 143 | "moduleRoots": [ 144 | "src" 145 | ] 146 | } 147 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import React, { createContext, useContext, useEffect, useState, useRef } from 'react'; 3 | import shallowEqual from './shallowEqual'; 4 | 5 | export const ReduxContext = createContext({}); 6 | 7 | export const Provider = ({ store, children }) => ( 8 | {children} 9 | ); 10 | 11 | export const useReduxCore = (selector, { shouldHooksUpdate }) => { 12 | const store = useContext(ReduxContext); 13 | const runGetState = () => selector(store.getState()); 14 | 15 | const [state, setState] = useState(runGetState); 16 | 17 | const lastStore = useRef(store); 18 | const lastSelector = useRef(selector); 19 | const lastUpdateState = useRef(state); 20 | 21 | function handleChange() { 22 | const updateState = runGetState(); 23 | 24 | // Can custom setup shallowEqual method on shouldHooksUpdate 25 | const shouldUpdate = 26 | typeof shouldHooksUpdate === 'function' 27 | ? shouldHooksUpdate(updateState, lastUpdateState.current) 28 | : !shallowEqual(updateState, lastUpdateState.current); 29 | 30 | if (shouldUpdate) { 31 | setState(updateState); 32 | lastUpdateState.current = updateState; 33 | } 34 | } 35 | 36 | if (lastStore.current !== store || lastSelector.current !== selector) { 37 | lastStore.current = store; 38 | lastSelector.current = selector; 39 | 40 | handleChange(); 41 | } 42 | 43 | useEffect(() => { 44 | let didUnsubscribe = false; 45 | 46 | const checkForUpdates = () => { 47 | if (didUnsubscribe) { 48 | return; 49 | } 50 | 51 | handleChange(); 52 | }; 53 | 54 | checkForUpdates(); 55 | 56 | const unsubscribe = store.subscribe(checkForUpdates); 57 | return () => { 58 | didUnsubscribe = true; 59 | unsubscribe(); 60 | }; 61 | }, [store, selector]); 62 | 63 | return [state, store.dispatch]; 64 | }; 65 | 66 | const defaultSelector = state => state; 67 | 68 | export const useRedux = (originSelector, actions, options = {}) => { 69 | const selector = typeof originSelector !== 'function' ? defaultSelector : originSelector; 70 | 71 | const [state, dispatch] = useReduxCore(selector, options); 72 | 73 | if (typeof actions === 'undefined' || actions === null) { 74 | return [state, dispatch]; 75 | } 76 | 77 | const boundActions = 78 | typeof actions === 'function' ? actions(dispatch) : bindActionCreators(actions, dispatch); 79 | 80 | return [state, boundActions]; 81 | }; 82 | -------------------------------------------------------------------------------- /src/shallowEqual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is Copy from https://github.com/facebook/react/blob/master/packages/shared/shallowEqual.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | /** 11 | * inlined Object.is polyfill to avoid requiring consumers ship their own 12 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 13 | */ 14 | function is(x, y) { 15 | if (x === y) { 16 | return x !== 0 || 1 / x === 1 / y; 17 | } 18 | 19 | return x !== x && y !== y; // eslint-disable-line no-self-compare 20 | } 21 | 22 | const { hasOwnProperty } = Object.prototype; 23 | 24 | /** 25 | * Performs equality by iterating through keys on an object and returning false 26 | * when any key has values which are not strictly equal between the arguments. 27 | * Returns true when the values of all keys are strictly equal. 28 | */ 29 | function shallowEqual(objA, objB) { 30 | if (is(objA, objB)) { 31 | return true; 32 | } 33 | 34 | if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { 35 | return false; 36 | } 37 | 38 | const keysA = Object.keys(objA); 39 | const keysB = Object.keys(objB); 40 | 41 | if (keysA.length !== keysB.length) { 42 | return false; 43 | } 44 | 45 | // Test for A's keys different from B. 46 | for (let i = 0; i < keysA.length; i += 1) { 47 | if (!hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { 48 | return false; 49 | } 50 | } 51 | 52 | return true; 53 | } 54 | 55 | export default shallowEqual; 56 | -------------------------------------------------------------------------------- /storybook/__conf__/mockConfig.js: -------------------------------------------------------------------------------- 1 | jest.mock('../facade'); 2 | -------------------------------------------------------------------------------- /storybook/__conf__/polyfill.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = callback => { 2 | setTimeout(callback, 0); 3 | }; 4 | -------------------------------------------------------------------------------- /storybook/__conf__/setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /storybook/__mocks__/facade.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import expect from 'expect'; 3 | 4 | const createSnapshot = (name, story) => { 5 | it(name, () => { 6 | expect(mount(story)).toMatchSnapshot(); 7 | }); 8 | }; 9 | 10 | export const storiesOf = function storiesOf() { 11 | const api = {}; 12 | let story; 13 | 14 | api.add = (name, func, { ignoreTest } = { ignoreTest: false }) => { 15 | if (!ignoreTest) { 16 | story = func(); 17 | createSnapshot(name, story); 18 | } else { 19 | it(name, () => {}); 20 | } 21 | return api; 22 | }; 23 | 24 | api.addWithInfo = (name, func) => { 25 | story = func(); 26 | createSnapshot(name, story); 27 | return api; 28 | }; 29 | 30 | api.addDecorator = () => {}; 31 | 32 | return api; 33 | }; 34 | -------------------------------------------------------------------------------- /storybook/__mocks__/file.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-knobs/register'; 3 | -------------------------------------------------------------------------------- /storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | const req = require.context('./examples/', true, /stories\.js$/); 4 | 5 | function loadStories() { 6 | req.keys().forEach(req); 7 | } 8 | configure(loadStories, module); 9 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/__tests__/TodoList.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withKnobs } from '@storybook/addon-knobs/react'; 3 | import { storiesOf } from '@storybook/react'; 4 | 5 | import TodoList from '../index.js'; 6 | 7 | const stories = storiesOf('TodoList', module); 8 | 9 | stories.addDecorator(withKnobs); 10 | 11 | stories.add('__interactive', () => ); 12 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/actions/index.js: -------------------------------------------------------------------------------- 1 | let nextTodoId = 0; 2 | 3 | export const addTodo = text => { 4 | nextTodoId += 1; 5 | 6 | return { 7 | type: 'ADD_TODO', 8 | id: nextTodoId, 9 | text, 10 | }; 11 | }; 12 | 13 | export const setVisibilityFilter = filter => ({ 14 | type: 'SET_VISIBILITY_FILTER', 15 | filter, 16 | }); 17 | 18 | export const toggleTodo = id => ({ 19 | type: 'TOGGLE_TODO', 20 | id, 21 | }); 22 | 23 | export const VisibilityFilters = { 24 | SHOW_ALL: 'SHOW_ALL', 25 | SHOW_COMPLETED: 'SHOW_COMPLETED', 26 | SHOW_ACTIVE: 'SHOW_ACTIVE', 27 | }; 28 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FilterLink from '../containers/FilterLink'; 3 | import { VisibilityFilters } from '../actions'; 4 | 5 | const Footer = () => ( 6 |
7 | Show: 8 | All 9 | Active 10 | Completed 11 |
12 | ); 13 | 14 | export default Footer; 15 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Todo = ({ onClick, completed, text }) => ( 5 |
  • 6 | 14 |
  • 15 | ); 16 | 17 | Todo.propTypes = { 18 | onClick: PropTypes.func.isRequired, 19 | completed: PropTypes.bool.isRequired, 20 | text: PropTypes.string.isRequired, 21 | }; 22 | 23 | export default Todo; 24 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/containers/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as actionCreators from '../actions'; 3 | 4 | import { useRedux } from '../../../../src'; 5 | 6 | const useAddTodoAction = () => useRedux(undefined, actionCreators); 7 | 8 | const AddTodo = () => { 9 | const [, { addTodo }] = useAddTodoAction(); 10 | 11 | let input; 12 | 13 | return ( 14 |
    15 |
    { 17 | e.preventDefault(); 18 | if (!input.value.trim()) { 19 | return; 20 | } 21 | addTodo(input.value); 22 | input.value = ''; 23 | }} 24 | > 25 | { 27 | input = node; 28 | }} 29 | /> 30 | 31 |
    32 |
    33 | ); 34 | }; 35 | 36 | export default AddTodo; 37 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import * as actionCreators from '../actions'; 5 | import { useRedux } from '../../../../src'; 6 | 7 | const useFilterLink = filter => 8 | useRedux(({ visibilityFilter }) => visibilityFilter === filter, actionCreators); 9 | 10 | const Link = ({ filter, children }) => { 11 | const [active, { setVisibilityFilter }] = useFilterLink(filter); 12 | 13 | return ( 14 | 23 | ); 24 | }; 25 | 26 | Link.propTypes = { 27 | filter: PropTypes.string.isRequired, 28 | children: PropTypes.node.isRequired, 29 | }; 30 | 31 | export default Link; 32 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useRedux } from '../../../../src'; 4 | 5 | import { toggleTodo, VisibilityFilters } from '../actions'; 6 | 7 | import Todo from '../components/Todo'; 8 | 9 | const getVisibleTodos = ({ todos, visibilityFilter }) => { 10 | switch (visibilityFilter) { 11 | case VisibilityFilters.SHOW_ALL: 12 | return todos; 13 | case VisibilityFilters.SHOW_COMPLETED: 14 | return todos.filter(t => t.completed); 15 | case VisibilityFilters.SHOW_ACTIVE: 16 | return todos.filter(t => !t.completed); 17 | default: 18 | throw new Error(`Unknown filter: ${visibilityFilter}`); 19 | } 20 | }; 21 | 22 | const useTodoList = () => useRedux(getVisibleTodos, { toggleTodo }, { pure: true }); 23 | 24 | const TodoList = () => { 25 | const [todos, actions] = useTodoList(); 26 | 27 | return ( 28 |
      29 | {todos.map(todo => ( 30 | actions.toggleTodo(todo.id)} /> 31 | ))} 32 |
    33 | ); 34 | }; 35 | 36 | export default TodoList; 37 | -------------------------------------------------------------------------------- /storybook/examples/TodoList/index.js: -------------------------------------------------------------------------------- 1 | // This example is from redux official website 2 | // https://redux.js.org/basics/example 3 | // I override it to use react-redux-hooks 4 | 5 | import React from 'react'; 6 | import { createStore } from 'redux'; 7 | 8 | import { Provider } from '../../../src'; 9 | 10 | import Footer from './components/Footer'; 11 | import AddTodo from './containers/AddTodo'; 12 | import VisibleTodoList from './containers/VisibleTodoList'; 13 | 14 | import rootReducer from './reducers'; 15 | 16 | const store = createStore(rootReducer); 17 | 18 | const App = () => ( 19 | 20 | 21 | 22 |