├── .babelrc ├── .gitignore ├── README.md ├── dist └── watch-props.js ├── help └── test-env.js ├── package.json ├── src └── index.js ├── test └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-app"], 3 | "plugins": [ 4 | "transform-decorators-legacy" 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | .history/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # watch-props 2 | A higher-order component that adds watch props features to the react component 3 | 4 | ### Install 5 | npm i --save watch-props 6 | 7 | ### Usage 8 | 9 | ```js 10 | import React from 'react' 11 | import watchProps from 'watch-props' 12 | 13 | @watchProps 14 | class App extends React.Component{ 15 | watch={ 16 | id: function(newValue, oldValue){ 17 | console.log(newValue, oldValue) //print "100, 1" 18 | } 19 | } 20 | } 21 | 22 | class Home extends React.Component{ 23 | state = { 24 | id: 1 25 | } 26 | 27 | componentDidMount(){ 28 | setTimeout(()=>{ 29 | this.setState({ 30 | id: 100 31 | }) 32 | },1000) 33 | } 34 | 35 | render(){ 36 | return 37 | } 38 | } 39 | 40 | 41 | 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /dist/watch-props.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("react")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["react"], factory); 6 | else if(typeof exports === 'object') 7 | exports["watchProps"] = factory(require("react")); 8 | else 9 | root["watchProps"] = factory(root["React"]); 10 | })(window, function(__WEBPACK_EXTERNAL_MODULE__0__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // define getter function for harmony exports 47 | /******/ __webpack_require__.d = function(exports, name, getter) { 48 | /******/ if(!__webpack_require__.o(exports, name)) { 49 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 50 | /******/ } 51 | /******/ }; 52 | /******/ 53 | /******/ // define __esModule on exports 54 | /******/ __webpack_require__.r = function(exports) { 55 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 56 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 57 | /******/ } 58 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 59 | /******/ }; 60 | /******/ 61 | /******/ // create a fake namespace object 62 | /******/ // mode & 1: value is a module id, require it 63 | /******/ // mode & 2: merge all properties of value into the ns 64 | /******/ // mode & 4: return value when already ns object 65 | /******/ // mode & 8|1: behave like require 66 | /******/ __webpack_require__.t = function(value, mode) { 67 | /******/ if(mode & 1) value = __webpack_require__(value); 68 | /******/ if(mode & 8) return value; 69 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 70 | /******/ var ns = Object.create(null); 71 | /******/ __webpack_require__.r(ns); 72 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 73 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 74 | /******/ return ns; 75 | /******/ }; 76 | /******/ 77 | /******/ // getDefaultExport function for compatibility with non-harmony modules 78 | /******/ __webpack_require__.n = function(module) { 79 | /******/ var getter = module && module.__esModule ? 80 | /******/ function getDefault() { return module['default']; } : 81 | /******/ function getModuleExports() { return module; }; 82 | /******/ __webpack_require__.d(getter, 'a', getter); 83 | /******/ return getter; 84 | /******/ }; 85 | /******/ 86 | /******/ // Object.prototype.hasOwnProperty.call 87 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 88 | /******/ 89 | /******/ // __webpack_public_path__ 90 | /******/ __webpack_require__.p = ""; 91 | /******/ 92 | /******/ 93 | /******/ // Load entry module and return exports 94 | /******/ return __webpack_require__(__webpack_require__.s = 1); 95 | /******/ }) 96 | /************************************************************************/ 97 | /******/ ([ 98 | /* 0 */ 99 | /***/ (function(module, exports) { 100 | 101 | module.exports = __WEBPACK_EXTERNAL_MODULE__0__; 102 | 103 | /***/ }), 104 | /* 1 */ 105 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 106 | 107 | "use strict"; 108 | __webpack_require__.r(__webpack_exports__); 109 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return WatchComponentCreator; }); 110 | /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0); 111 | /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); 112 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 113 | 114 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 115 | 116 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 117 | 118 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 119 | 120 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 121 | 122 | 123 | 124 | function getParams(state) { 125 | var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 126 | 127 | var watch = state.__watchObject; 128 | if (watch && (typeof watch === 'undefined' ? 'undefined' : _typeof(watch)) === 'object') { 129 | var params = {}; 130 | Object.keys(watch).forEach(function (key) { 131 | params[key] = props[key]; 132 | }); 133 | return params; 134 | } 135 | } 136 | 137 | function shallowEqual() { 138 | var data1 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 139 | var data2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 140 | 141 | var keys = Object.keys(data1); 142 | for (var i in keys) { 143 | var key = keys[i]; 144 | if (data1[key] !== data2[key]) { 145 | return false; 146 | } 147 | } 148 | return true; 149 | } 150 | 151 | function checkReactGt163() { 152 | var versions = react__WEBPACK_IMPORTED_MODULE_0___default.a.version.split('.'); 153 | if (versions[0] > 16) return true; 154 | if (versions[0] == 16 && versions[1] >= 3) { 155 | return true; 156 | } 157 | return false; 158 | } 159 | 160 | function handleReactGt163(WatchComponent) { 161 | WatchComponent.prototype.componentDidUpdate = function (prevProps) { 162 | var _this = this; 163 | 164 | var params = this.state.__watchState; 165 | var watchObject = this.state.__watchObject; 166 | if (!watchObject || !params) { 167 | return; 168 | } 169 | Object.keys(watchObject).forEach(function (key) { 170 | if (params[key] !== prevProps[key]) { 171 | if (watchObject[key] && typeof watchObject[key] === 'function') { 172 | watchObject[key].call(_this.child, params[key], prevProps[key]); 173 | } 174 | } 175 | }); 176 | }; 177 | 178 | WatchComponent.getDerivedStateFromProps = function (nextProps, prevState) { 179 | var params = getParams(prevState, nextProps); 180 | var __watchObject = prevState.__watchObject; 181 | if (!params || !__watchObject) { 182 | return null; 183 | } 184 | if (!prevState.__watchState) { 185 | return { 186 | __watchState: params 187 | }; 188 | } else if (!shallowEqual(params, prevState.__watchState)) { 189 | return { 190 | __watchState: params 191 | }; 192 | } 193 | return null; 194 | }; 195 | } 196 | 197 | function handleReactLt163(WatchComponent) { 198 | WatchComponent.prototype.componentWillReceiveProps = function (nextProps) { 199 | var _this2 = this; 200 | 201 | if (!this.state.__watchObject) return; 202 | var watchObject = this.state.__watchObject; 203 | Object.keys(watchObject).forEach(function (key) { 204 | if (_this2.props[key] !== nextProps[key]) { 205 | watchObject[key].call(_this2.child, nextProps[key], _this2.props[key]); 206 | } 207 | }); 208 | }; 209 | } 210 | 211 | function WatchComponentCreator(WrappedComponent) { 212 | var WatchComponent = function (_React$PureComponent) { 213 | _inherits(WatchComponent, _React$PureComponent); 214 | 215 | function WatchComponent(props) { 216 | _classCallCheck(this, WatchComponent); 217 | 218 | var _this3 = _possibleConstructorReturn(this, (WatchComponent.__proto__ || Object.getPrototypeOf(WatchComponent)).call(this, props)); 219 | 220 | _this3.state = {}; 221 | return _this3; 222 | } 223 | 224 | _createClass(WatchComponent, [{ 225 | key: 'componentDidMount', 226 | value: function componentDidMount() { 227 | var watch = this.child.watch; 228 | if (watch) { 229 | if (typeof watch === 'function') { 230 | this.setState({ 231 | __watchObject: watch.call(this.child) 232 | }); 233 | } else if ((typeof watch === 'undefined' ? 'undefined' : _typeof(watch)) === 'object') { 234 | this.setState({ 235 | __watchObject: watch 236 | }); 237 | } 238 | } 239 | } 240 | }, { 241 | key: 'render', 242 | value: function render() { 243 | var _this4 = this; 244 | 245 | return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(WrappedComponent, Object.assign({ ref: function ref(child) { 246 | return _this4.child = child; 247 | } }, this.props)); 248 | } 249 | }]); 250 | 251 | return WatchComponent; 252 | }(react__WEBPACK_IMPORTED_MODULE_0___default.a.PureComponent); 253 | 254 | if (checkReactGt163()) { 255 | handleReactGt163(WatchComponent); 256 | } else { 257 | handleReactLt163(WatchComponent); 258 | } 259 | 260 | return WatchComponent; 261 | } 262 | 263 | /***/ }) 264 | /******/ ]); 265 | }); -------------------------------------------------------------------------------- /help/test-env.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | 3 | require('core-js/es6/map') 4 | require('core-js/es6/set') 5 | require('raf/polyfill') 6 | 7 | 8 | 9 | global.document = require('jsdom').jsdom('') 10 | global.window = document.defaultView 11 | global.navigator = window.navigator -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watch-props", 3 | "version": "0.0.3", 4 | "description": "", 5 | "main": "dist/watch-props.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "build": "NODE_ENV=production webpack --config webpack.config.js", 11 | "test": "NODE_ENV=test ava --verbose", 12 | "prepublish": "npm run test && npm run build" 13 | }, 14 | "pre-commit": [ 15 | "test" 16 | ], 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "react": "^16.5.2", 22 | "react-dom": "^16.5.2" 23 | }, 24 | "devDependencies": { 25 | "ava": "^0.25.0", 26 | "babel-cli": "^6.26.0", 27 | "babel-loader": "^7.1.5", 28 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 29 | "babel-preset-react-app": "^3.1.0", 30 | "babel-register": "^6.26.0", 31 | "core-js": "^2.5.7", 32 | "enzyme": "^3.7.0", 33 | "enzyme-adapter-react-16": "^1.6.0", 34 | "jsdom": "^7.2.2", 35 | "pre-commit": "^1.2.2", 36 | "prepublish": "^2.2.0", 37 | "prop-types": "^15.6.2", 38 | "raf": "^3.4.0", 39 | "webpack": "^4.20.2", 40 | "webpack-cli": "^3.1.2" 41 | }, 42 | "ava": { 43 | "files": [ 44 | "test/**/*.js" 45 | ], 46 | "require": [ 47 | "./help/test-env.js" 48 | ], 49 | "babel": "inherit" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function getParams (state, props = {}) { 4 | let watch = state.__watchObject; 5 | if (watch && typeof watch === 'object') { 6 | let params = {}; 7 | Object.keys (watch).forEach (key => { 8 | params[key] = props[key]; 9 | }); 10 | return params; 11 | } 12 | } 13 | 14 | function shallowEqual (data1 = {}, data2 = {}) { 15 | let keys = Object.keys (data1); 16 | for (let i in keys) { 17 | let key = keys[i]; 18 | if (data1[key] !== data2[key]) { 19 | return false; 20 | } 21 | } 22 | return true; 23 | } 24 | 25 | function checkReactGt163 () { 26 | let versions = React.version.split ('.'); 27 | if (versions[0] > 16) return true; 28 | if (versions[0] == 16 && versions[1] >= 3) { 29 | return true; 30 | } 31 | return false; 32 | } 33 | 34 | function handleReactGt163 (WatchComponent) { 35 | WatchComponent.prototype.componentDidUpdate = function (prevProps) { 36 | let params = this.state.__watchState; 37 | let watchObject = this.state.__watchObject; 38 | if (!watchObject || !params) { 39 | return; 40 | } 41 | Object.keys (watchObject).forEach (key => { 42 | if (params[key] !== prevProps[key]) { 43 | if (watchObject[key] && typeof watchObject[key] === 'function') { 44 | watchObject[key].call (this.child, params[key], prevProps[key]); 45 | } 46 | } 47 | }); 48 | }; 49 | 50 | WatchComponent.getDerivedStateFromProps = function (nextProps, prevState) { 51 | let params = getParams (prevState, nextProps); 52 | let __watchObject = prevState.__watchObject; 53 | if (!params || !__watchObject) { 54 | return null; 55 | } 56 | if (!prevState.__watchState) { 57 | return { 58 | __watchState: params, 59 | }; 60 | } else if (!shallowEqual (params, prevState.__watchState)) { 61 | return { 62 | __watchState: params, 63 | }; 64 | } 65 | return null; 66 | }; 67 | } 68 | 69 | function handleReactLt163(WatchComponent){ 70 | WatchComponent.prototype.componentWillReceiveProps = function(nextProps){ 71 | if(!this.state.__watchObject) return; 72 | let watchObject = this.state.__watchObject; 73 | Object.keys(watchObject).forEach(key=>{ 74 | if(this.props[key] !== nextProps[key]){ 75 | watchObject[key].call(this.child, nextProps[key], this.props[key]) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | export default function WatchComponentCreator (WrappedComponent) { 82 | class WatchComponent extends React.PureComponent { 83 | constructor (props) { 84 | super (props); 85 | this.state = {}; 86 | } 87 | 88 | componentDidMount(){ 89 | let watch = this.child.watch; 90 | if (watch) { 91 | if (typeof watch === 'function') { 92 | this.setState ({ 93 | __watchObject: watch.call (this.child), 94 | }); 95 | } else if (typeof watch === 'object') { 96 | this.setState ({ 97 | __watchObject: watch, 98 | }); 99 | } 100 | } 101 | } 102 | 103 | render () { 104 | return ( 105 | (this.child = child)} {...this.props} /> 106 | ); 107 | } 108 | } 109 | 110 | if (checkReactGt163 ()) { 111 | handleReactGt163 (WatchComponent); 112 | } else { 113 | handleReactLt163 (WatchComponent); 114 | } 115 | 116 | return WatchComponent; 117 | } 118 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import watchProps from '../dist/watch-props' 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | import { mount, configure } from 'enzyme'; 7 | import Adapter from 'enzyme-adapter-react-16'; 8 | 9 | configure({ adapter: new Adapter() }); 10 | 11 | test.cb('Test React 16 watchProps', t=>{ 12 | let num = 0; 13 | class App extends React.Component{ 14 | static propTypes = { 15 | x: PropTypes.string 16 | } 17 | state={ 18 | a: this.props.x 19 | } 20 | 21 | watch ={ 22 | x: function(val, old){ 23 | num++; 24 | this.setState({ 25 | a: val, 26 | old 27 | }) 28 | } 29 | } 30 | 31 | render(){ 32 | return this.state.a; 33 | } 34 | } 35 | 36 | let C = watchProps(App); 37 | let wrapper = mount(); 38 | t.is(wrapper.text(), 'a'); 39 | setTimeout(()=>{ 40 | wrapper.setProps({ 41 | x: 'b' 42 | }) 43 | },50) 44 | setTimeout(()=>{ 45 | t.is(wrapper.text(), 'b'); 46 | let state = wrapper.children().state(); 47 | t.is(state.old, 'a') 48 | t.is(num, 1) 49 | },100) 50 | 51 | setTimeout(()=>{ 52 | wrapper.setProps({ 53 | x: 'c' 54 | }) 55 | },150) 56 | 57 | setTimeout(()=>{ 58 | wrapper.setProps({ 59 | x: 'd' 60 | }) 61 | },160) 62 | 63 | setTimeout(()=>{ 64 | t.is(wrapper.text(), 'd'); 65 | let state = wrapper.children().state(); 66 | t.is(state.old, 'c') 67 | t.is(num, 3) 68 | t.end(); 69 | },200) 70 | 71 | }) 72 | 73 | 74 | test.cb('React 15', t=>{ 75 | let num = 0; 76 | React.version = '15.1.0' 77 | class App extends React.Component{ 78 | static propTypes = { 79 | x: PropTypes.string 80 | } 81 | state={ 82 | a: this.props.x 83 | } 84 | 85 | watch ={ 86 | x: function(val, old){ 87 | num++; 88 | this.setState({ 89 | a: val, 90 | old 91 | }) 92 | } 93 | } 94 | 95 | render(){ 96 | return this.state.a; 97 | } 98 | } 99 | 100 | let C = watchProps(App); 101 | let wrapper = mount(); 102 | t.is(wrapper.text(), 'a'); 103 | setTimeout(()=>{ 104 | wrapper.setProps({ 105 | x: 'b' 106 | }) 107 | },50) 108 | setTimeout(()=>{ 109 | t.is(wrapper.text(), 'b'); 110 | let state = wrapper.children().state(); 111 | t.is(state.old, 'a') 112 | t.is(num, 1) 113 | },100) 114 | 115 | setTimeout(()=>{ 116 | wrapper.setProps({ 117 | x: 'c' 118 | }) 119 | },150) 120 | 121 | setTimeout(()=>{ 122 | wrapper.setProps({ 123 | x: 'd' 124 | }) 125 | },160) 126 | 127 | setTimeout(()=>{ 128 | t.is(wrapper.text(), 'd'); 129 | let state = wrapper.children().state(); 130 | t.is(state.old, 'c') 131 | t.is(num, 3) 132 | t.end(); 133 | },200) 134 | 135 | }) 136 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require ('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js$/, 9 | exclude: /node_modules/, 10 | use: { 11 | loader: 'babel-loader', 12 | }, 13 | } 14 | ], 15 | }, 16 | entry: './src/index.js', 17 | output: { 18 | filename: 'watch-props.js', 19 | path: path.resolve (__dirname, 'dist'), 20 | libraryTarget: "umd", 21 | library: ['watchProps'], 22 | }, 23 | externals: [ 24 | { react: { commonjs: "react", commonjs2: "react",amd: 'react', root: ['React'] } }, 25 | ], 26 | optimization: { 27 | minimize: false 28 | } 29 | }; 30 | --------------------------------------------------------------------------------