├── .DS_Store ├── .babelrc ├── .travis.yml ├── .gitignore ├── .npmignore ├── src ├── index.ts ├── utils.ts ├── global-state.ts ├── provider.tsx ├── connect.ts └── tests │ └── index.test.tsx ├── tsconfig.json ├── demo ├── single.tsx └── refetch.tsx ├── LICENSE ├── package.json └── readme.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dobjs/dob-react/HEAD/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ] 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | install: 5 | - npm install 6 | node_js: 7 | - 8 8 | script: 9 | - npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | 7 | node_modules 8 | built 9 | 10 | coverage 11 | .nyc_output 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | out 5 | gen 6 | 7 | node_modules 8 | 9 | coverage 10 | .nyc_output 11 | .babelrc -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Provider } from './provider' 2 | export { default as Connect } from './connect' 3 | export { startDebug, stopDebug } from './utils' 4 | export { globalState } from './global-state' -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { globalState } from './global-state' 2 | import { startDebug as dobStartDebug, stopDebug as dobStopDebug, useStrict } from 'dob' 3 | 4 | /** 5 | * 开启调试模式 6 | */ 7 | export function startDebug() { 8 | globalState.useDebug = true 9 | dobStartDebug() 10 | } 11 | 12 | /** 13 | * 终止调试模式 14 | */ 15 | export function stopDebug() { 16 | globalState.useDebug = false 17 | dobStopDebug() 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "strict": true, 5 | "target": "es6", 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "jsx": "react", 9 | "sourceMap": true, 10 | "outDir": "built", 11 | "skipLibCheck": true, 12 | "moduleResolution": "node", 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true 15 | }, 16 | "exclude": ["node_modules", "built"] 17 | } 18 | -------------------------------------------------------------------------------- /demo/single.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { combineStores, observable, inject, observe } from "dob"; 4 | import { Connect } from "../src"; 5 | 6 | @observable 7 | class Store { 8 | name = 123; 9 | } 10 | 11 | class Action { 12 | @inject(Store) store!: Store; 13 | 14 | changeName = () => { 15 | this.store.name = 456; 16 | }; 17 | } 18 | 19 | const stores = combineStores({ Store, Action }); 20 | 21 | @Connect(stores) 22 | class App extends React.Component { 23 | render() { 24 | return ( 25 |
26 | {this.props.Store!.name} 27 |
28 | ); 29 | } 30 | } 31 | 32 | ReactDOM.render(, document.getElementById("react-dom")); 33 | -------------------------------------------------------------------------------- /src/global-state.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const tag = "ascoders-dob-react" 4 | 5 | class GlobalState { 6 | /** 7 | * 是否开启 debug 8 | */ 9 | public useDebug = false 10 | /** 11 | * provider 计数器,如果页面拥有多个 provider,聚合在一起显示 12 | */ 13 | public providerCounter = 0 14 | /** 15 | * debug 工具栏 16 | */ 17 | public DebugToolBox: React.ComponentClass | null = null 18 | /** 19 | * debug 每个组件的 wrapper,通过实现这两个组件,完成与 dob-react 的调试模式对接 20 | */ 21 | public DebugWrapper: React.ComponentClass | null = null 22 | /** 23 | * 当每个组件因为 dob 触发 rerender 时,会触发每个 DebugWrapper 此方法,且传入 debugId 24 | */ 25 | public handleReRender = 'DYNAMIC_REACT_HANDLE_RE_RENDER' 26 | } 27 | 28 | let globalState = new GlobalState() 29 | 30 | const globalOrWindow: any = (typeof self === "object" && self.self === self && self) || 31 | (typeof global === "object" && global.global === global && global) 32 | 33 | if (globalOrWindow[tag]) { 34 | globalState = globalOrWindow[tag] 35 | } else { 36 | globalOrWindow[tag] = globalState 37 | } 38 | 39 | export { globalState } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present, ziyi huang(ascoders) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /demo/refetch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import { combineStores, observable, inject, observe } from 'dob' 4 | import { Provider, Connect } from '../src' 5 | 6 | interface IResponse { 7 | isLoading: boolean 8 | hasError: boolean 9 | data: T 10 | } 11 | 12 | function mockFetch(params: string, target: IResponse) { 13 | target.isLoading = true 14 | target.hasError = false 15 | target.data = null as any 16 | 17 | setTimeout(() => { 18 | target.isLoading = false 19 | target.hasError = false 20 | target.data = params 21 | }, 1000) 22 | } 23 | 24 | @observable 25 | class Store { 26 | param = 'abc' 27 | 28 | user: IResponse = { 29 | isLoading: false, 30 | hasError: false, 31 | data: null as any 32 | } 33 | 34 | async response() { 35 | await mockFetch(this.param, this.user) 36 | } 37 | } 38 | 39 | class Action { 40 | @inject(Store) store!: Store 41 | 42 | changeParam = () => { 43 | this.store.param = 'xyz' + Math.random().toString() 44 | } 45 | } 46 | 47 | const stores = combineStores({ Store, Action }) 48 | 49 | @Connect 50 | class App extends React.Component { 51 | componentWillMount() { 52 | observe(() => { 53 | this!.props!.Store!.response() 54 | }) 55 | } 56 | 57 | render() { 58 | if (this!.props!.Store!.user!.isLoading) { 59 | return ( 60 | loading.. 61 | ) 62 | } 63 | 64 | return ( 65 | 数据是:{this!.props!.Store!.user!.data} 点击我重新发请求 68 | ) 69 | } 70 | } 71 | 72 | ReactDOM.render( 73 | 74 | 75 | 76 | , document.getElementById('react-dom')) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dob-react", 3 | "version": "2.4.17", 4 | "description": "", 5 | "main": "built/src/index.js", 6 | "scripts": { 7 | "test": "tsc && nyc --reporter=lcov --reporter=text --reporter=json ava", 8 | "posttest": "codecov -f coverage/*.json -t e25fe2bb-a132-42cf-b126-6e62b7c7517e", 9 | "prepublish": "rm -rf built && tsc && babel built --out-dir built && npm run build", 10 | "start": "run-react develop", 11 | "build": "run-react production" 12 | }, 13 | "types": "src/index.ts", 14 | "ava": { 15 | "files": [ 16 | "built/**/*.test.js" 17 | ] 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/ascoders/dob-react.git" 22 | }, 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/ascoders/dob-react/issues" 27 | }, 28 | "homepage": "https://github.com/ascoders/dob-react#readme", 29 | "peerDependencies": { 30 | "react": "*", 31 | "@types/create-react-class": "*", 32 | "@types/prop-types": "*" 33 | }, 34 | "devDependencies": { 35 | "@types/react-test-renderer": "*", 36 | "babel-cli": "^6.26.0", 37 | "babel-plugin-transform-class-properties": "^6.24.1", 38 | "babel-plugin-transform-es2015-classes": "^6.24.1", 39 | "babel-preset-es2015": "^6.24.0", 40 | "babel-preset-stage-0": "^6.22.0", 41 | "codecov": "^2.3.0", 42 | "dependency-inject": "^1.1.4", 43 | "lz-string": "^1.4.4", 44 | "nyc": "^11.2.1", 45 | "react": "^16.0.0", 46 | "react-color": "^2.13.8", 47 | "react-dom": "^16.0.0", 48 | "react-router": "^4.2.0", 49 | "react-router-dom": "^4.2.2", 50 | "react-test-renderer": "^16.0.0", 51 | "run-react": "^2.2.2", 52 | "sortablejs": "^1.6.1", 53 | "typescript": "^2.5.2", 54 | "webpack": "^2.6.1" 55 | }, 56 | "dependencies": { 57 | "create-react-class": "^15.6.2", 58 | "dob": "^2.5.9", 59 | "prop-types": "^15.6.0", 60 | "shallow-eq": "^1.0.5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { globalState } from './global-state' 3 | import { IDebugInfo, Event } from 'dob' 4 | 5 | const PropTypes = require('prop-types') 6 | 7 | const specialReactKeys = new Set(['children', 'key', 'ref']) 8 | 9 | interface Props { 10 | [store: string]: object | undefined 11 | } 12 | 13 | export default class Provider extends React.Component { 14 | 15 | static contextTypes = { 16 | dyStores: PropTypes.object, 17 | dyDebug: PropTypes.object, 18 | } 19 | 20 | static childContextTypes = { 21 | dyStores: PropTypes.object.isRequired, 22 | dyDebug: PropTypes.object 23 | } 24 | 25 | getChildContext() { 26 | // 继承 store 27 | const dyStores = Object.assign({}, this.context.dyStores) 28 | 29 | // 添加用户传入的 store 30 | for (let key in this.props) { 31 | if (!specialReactKeys.has(key)) { 32 | dyStores[key] = this.props[key] 33 | } 34 | } 35 | 36 | if (globalState.useDebug) { 37 | return { 38 | dyStores: dyStores, 39 | dyDebug: { 40 | /** 41 | * 存储当前 dob 所有 action 触发的 debug 信息 42 | */ 43 | debugInfoMap: new Map(), 44 | /** 45 | * 事件系统,用于 debug ui 之间通信 46 | */ 47 | event: new Event() 48 | } 49 | } 50 | } 51 | 52 | return { 53 | dyStores: dyStores 54 | } 55 | } 56 | 57 | render() { 58 | globalState.providerCounter++ 59 | 60 | if (globalState.useDebug && globalState.providerCounter === 1) { 61 | const ToolBox = globalState.DebugToolBox as any 62 | // 即使在 debug 模式下,ToolBox 也只会实例化一个 63 | return ( 64 | 65 | {this.props.children} 66 | 67 | ) 68 | } 69 | 70 | return this.props.children as React.ReactElement 71 | } 72 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # dob-react · [![CircleCI Status](https://img.shields.io/travis/dobjs/dob-react/master.svg?style=flat)](https://travis-ci.org/dobjs/dob-react) [![npm version](https://img.shields.io/npm/v/dob-react.svg?style=flat)](https://www.npmjs.com/package/dob-react) [![code coverage](https://img.shields.io/codecov/c/github/dobjs/dob-react/master.svg)](https://codecov.io/github/dobjs/dob-react) 2 | 3 | React bindings for dob. 4 | 5 | Design idea from [Mobx Implementation](https://github.com/ascoders/blog/issues/16) 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm i dob-react 11 | ``` 12 | 13 | ## Online demo 14 | 15 | Here is a basic [demo](https://jsfiddle.net/yp90Lep9/21/), and here is a [demo](https://jsfiddle.net/g19ehhgu/11/) with fractal. 16 | 17 | ## Simple Usage 18 | 19 | ```typescript 20 | import { Provider, Connect } from 'dob-react' 21 | 22 | @Connect 23 | class App extends React.Component { 24 | render() { 25 | return ( 26 | {this.props.store.name} 27 | ) 28 | } 29 | } 30 | 31 | ReactDOM.render( 32 | 33 | 34 | 35 | , document.getElementById('react-dom')) 36 | ``` 37 | 38 | `Connect`: All parameters from outer Provider are injected into the wrapped components, and the component rerender when the variables used in the render function are modified(sync usage). 39 | 40 | ## `Connect` all functions 41 | 42 | ### Connect all 43 | 44 | Connect all from Provider's parameters, also is this example above. 45 | 46 | ### Connect extra data 47 | 48 | > Will also inject all parameters from outer Provider. 49 | 50 | ```typescript 51 | @Connect({ 52 | customStore: { 53 | name: 'lucy' 54 | } 55 | }) 56 | class App extends React.Component {} 57 | ``` 58 | 59 | ### Map state to props 60 | 61 | > Will also inject all parameters from outer Provider. 62 | 63 | ```typescript 64 | @Connect(state => { 65 | return { 66 | customName: 'custom' + state.store.name 67 | } 68 | }) 69 | class App extends React.Component {} 70 | 71 | ReactDOM.render( 72 | 73 | , document.getElementById('react-dom')) 74 | ``` 75 | 76 | ### Support stateless component 77 | 78 | ```typescript 79 | class App extends React.Component { 80 | render() { 81 | return ( 82 | {this.props.store.name} 83 | ) 84 | } 85 | } 86 | 87 | const ConnectApp = Connect()(App) 88 | // const ConnectApp = Connect({ ... })(App) 89 | // const ConnectApp = Connect( state => { ... })(App) 90 | ``` 91 | -------------------------------------------------------------------------------- /src/connect.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { Reaction } from "dob"; 4 | import { globalState } from "./global-state"; 5 | import shallowEqual from "shallow-eq"; 6 | 7 | const PropTypes = require("prop-types"); 8 | const createClass = require("create-react-class"); 9 | 10 | /** 11 | * 组件是否已销毁 12 | */ 13 | const isUmount = Symbol() as any; 14 | 15 | /** 16 | * observer 对象存放的 key 17 | */ 18 | const reactionKey = Symbol() as any; 19 | 20 | /** 21 | * render 次数 22 | */ 23 | const renderCountKey = Symbol() as any; 24 | 25 | interface ReactiveMixin { 26 | [lifecycleName: string]: any; 27 | } 28 | 29 | /** 30 | * baseRender 初始化未渲染状态 31 | * 之所以不用 null 判断是否有渲染,因为 render 函数本身就可以返回 null,所以只好用 symbol 准确判断是否执行了 render 32 | */ 33 | const emptyBaseRender = Symbol(); 34 | 35 | /** 36 | * 报告该组件触发了 dob 渲染 37 | */ 38 | function reportTrack(reactElement: React.ReactElement, debugId: number) { 39 | if ( 40 | !globalState.useDebug || 41 | !reactElement.props[globalState.handleReRender] 42 | ) { 43 | return; 44 | } 45 | 46 | Promise.resolve().then(() => { 47 | reactElement.props[globalState.handleReRender](debugId); 48 | }); 49 | } 50 | 51 | /** 52 | * 将生命周期聚合到 react 中 53 | */ 54 | function patch(target: any, funcName: string, runMixinFirst = false) { 55 | // 原始生命周期函数 56 | const base = target[funcName]; 57 | // 待聚合的生命周期函数 58 | const mixinFunc = reactiveMixin[funcName]; 59 | if (!base) { 60 | // 如果没有原始生命周期函数,直接覆盖即可 61 | target[funcName] = mixinFunc; 62 | } else { 63 | // 将两个函数串起来执行 64 | target[funcName] = 65 | runMixinFirst === true 66 | ? function(this: any, ...args: any[]) { 67 | mixinFunc.apply(this, args); 68 | base.apply(this, args); 69 | } 70 | : function(this: any, ...args: any[]) { 71 | base.apply(this, args); 72 | mixinFunc.apply(this, args); 73 | }; 74 | } 75 | } 76 | 77 | /** 78 | * 双向绑定的 Mixin 79 | */ 80 | const reactiveMixin: ReactiveMixin = { 81 | componentWillMount: function() { 82 | // 当前组件名 83 | const initialName = 84 | this.displayName || 85 | this.name || 86 | (this.constructor && 87 | ((this.constructor as any).displayName || this.constructor.name)) || 88 | ""; 89 | 90 | // 当前节点 id 91 | const rootNodeID = 92 | this._reactInternalInstance && this._reactInternalInstance._rootNodeID; 93 | 94 | // 是否在渲染期间 95 | let isRenderingPending = false; 96 | 97 | // 原始 render 98 | const baseRender = this.render.bind(this); 99 | 100 | let renderResult: any = emptyBaseRender; 101 | 102 | // 核心 reaction 103 | let reaction: Reaction; 104 | 105 | // 初始化 render 106 | const initialRender = () => { 107 | reaction = new Reaction(`${initialName}.render`, () => { 108 | if (isRenderingPending) { 109 | return; 110 | } 111 | isRenderingPending = true; 112 | 113 | // 执行经典的 componentWillReact 114 | typeof this.componentWillReact === "function" && 115 | this[renderCountKey] && 116 | this.componentWillReact(); 117 | 118 | // 如果组件没有被销毁,尝试调用 forceUpdate 119 | // 而且第一次渲染不会调用 forceUpdate 120 | if (!this[isUmount] && this[renderCountKey]) { 121 | React.Component.prototype.forceUpdate.call(this); 122 | } 123 | 124 | isRenderingPending = false; 125 | }); 126 | 127 | this[reactionKey] = reaction; 128 | 129 | // 之后都用 reactiveRender 作 render 130 | this.render = reactiveRender; 131 | 132 | return reactiveRender(); 133 | }; 134 | 135 | const reactiveRender = () => { 136 | reaction.track(debugId => { 137 | renderResult = baseRender(); 138 | 139 | reportTrack(this as React.ReactElement, debugId); 140 | }); 141 | 142 | this[renderCountKey] 143 | ? this[renderCountKey]++ 144 | : (this[renderCountKey] = 1); 145 | 146 | // 如果 observe 跑过了 renderResult,就不要再执行一遍 baseRender,防止重复调用 render 147 | if (renderResult !== emptyBaseRender) { 148 | const tempResult = renderResult; 149 | 150 | // 防止 setState 这种没有通过 observe 触发的情况,不应该直接用结果, 151 | // 所以要每次清空,如果不是 observe,而是 setState 触发 render,就执行 baseRender 152 | renderResult = emptyBaseRender; 153 | 154 | return tempResult; 155 | } 156 | 157 | return baseRender(); 158 | }; 159 | 160 | // 默认用初始化 render 161 | this.render = initialRender; 162 | }, 163 | componentWillUnmount: function() { 164 | // 取消 observe 监听 165 | this[reactionKey] && this[reactionKey].dispose(); 166 | 167 | this[isUmount] = true; 168 | }, 169 | shouldComponentUpdate: function(nextProps: any, nextState: any) { 170 | // 任何 state 修改都会重新 render 171 | if (!shallowEqual(this.state, nextState)) { 172 | return true; 173 | } 174 | 175 | return !shallowEqual(this.props, nextProps); 176 | } 177 | }; 178 | 179 | /** 180 | * 聚合生命周期 181 | */ 182 | function mixinLifecycleEvents(target: any) { 183 | patch(target, "componentWillMount", true); 184 | patch(target, "componentWillUnmount"); 185 | 186 | if (!target.shouldComponentUpdate && !target.isPureReactComponent) { 187 | // 只有原对象没有 shouldComponentUpdate 的时候,才使用 mixins 188 | target.shouldComponentUpdate = reactiveMixin.shouldComponentUpdate; 189 | } 190 | } 191 | 192 | function getWrappedComponent(componentClass: any): any { 193 | if (componentClass && componentClass.WrappedComponent) { 194 | return getWrappedComponent(componentClass.WrappedComponent); 195 | } 196 | return componentClass; 197 | } 198 | 199 | function mixinAndInject( 200 | componentClass: any, 201 | extraInjection: Object | Function = {}, 202 | isGetWrappedComponent = true 203 | ): any { 204 | if (!componentClass) { 205 | return null; 206 | } 207 | 208 | const wrappedComponentClass = isGetWrappedComponent 209 | ? getWrappedComponent(componentClass) 210 | : componentClass; 211 | 212 | if (!isReactFunction(componentClass)) { 213 | // stateless react function 214 | return mixinAndInject( 215 | createClass({ 216 | displayName: componentClass.displayName || componentClass.name, 217 | propTypes: componentClass.propTypes, 218 | contextTypes: componentClass.contextTypes, 219 | statics: { 220 | WrappedComponent: wrappedComponentClass 221 | }, 222 | getDefaultProps: function() { 223 | return componentClass.defaultProps; 224 | }, 225 | render: function() { 226 | return componentClass.call(this, this.props, this.context); 227 | } 228 | }), 229 | extraInjection, 230 | false 231 | ); 232 | } 233 | 234 | mixinLifecycleEvents( 235 | wrappedComponentClass.prototype || wrappedComponentClass 236 | ); 237 | 238 | return class InjectWrapper extends React.Component { 239 | // 取 context 240 | static contextTypes = { 241 | dyStores: PropTypes.object 242 | }; 243 | 244 | render() { 245 | let wrappedComponent: React.ReactElement | null = null; 246 | 247 | if (typeof extraInjection === "object") { 248 | wrappedComponent = React.createElement(componentClass, { 249 | ref: this.props.wrappedComponentRef, 250 | ...this.context.dyStores, 251 | ...this.props, 252 | ...extraInjection 253 | }); 254 | } else if (typeof extraInjection === "function") { 255 | wrappedComponent = React.createElement(componentClass, { 256 | ref: this.props.wrappedComponentRef, 257 | ...this.context.dyStores, 258 | ...this.props, 259 | ...extraInjection(this.context.dyStores) 260 | }); 261 | } 262 | 263 | if (globalState.useDebug) { 264 | if (globalState.DebugWrapper !== null) { 265 | return React.createElement( 266 | globalState.DebugWrapper, 267 | { ref: this.props.wrappedComponentRef }, 268 | wrappedComponent 269 | ); 270 | } 271 | } 272 | 273 | return wrappedComponent; 274 | } 275 | }; 276 | } 277 | 278 | function isReactFunction(obj: any) { 279 | if (typeof obj === "function") { 280 | if ( 281 | (obj.prototype && obj.prototype.render) || 282 | obj.isReactClass || 283 | React.Component.isPrototypeOf(obj) 284 | ) { 285 | return true; 286 | } 287 | } 288 | 289 | return false; 290 | } 291 | 292 | /** 293 | * Observer function / decorator 294 | */ 295 | export default function Connect( 296 | target: any, 297 | propertyKey?: string, 298 | descriptor?: PropertyDescriptor 299 | ): any; 300 | export default function Connect(injectExtension: any): any; 301 | export default function Connect( 302 | mapStateToProps?: (state?: T) => any 303 | ): any; 304 | export default function Connect(target: any): any { 305 | // usage: @Connect 306 | if (isReactFunction(target)) { 307 | return mixinAndInject(target); 308 | } 309 | 310 | // usage: @Connect(object) 311 | if (typeof target === "object") { 312 | return (realComponentClass: any) => { 313 | if (!realComponentClass) { 314 | // Error 315 | return null; 316 | } 317 | 318 | return mixinAndInject(realComponentClass, target); 319 | }; 320 | } 321 | 322 | // usage: @Connect(function) 323 | if (typeof target === "function") { 324 | return (realComponentClass: any) => { 325 | return mixinAndInject(realComponentClass, target); 326 | }; 327 | } 328 | 329 | // usage: Connect()(App) 330 | if (!target) { 331 | return (realComponentClass: any) => { 332 | return mixinAndInject(realComponentClass); 333 | }; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /src/tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as React from "react"; 3 | import { create } from "react-test-renderer"; 4 | import { inject, Container, injectFactory } from "dependency-inject"; 5 | import { Provider, Connect } from "../index"; 6 | import { observable } from "dob"; 7 | 8 | function immediate(fn: Function, time?: number) { 9 | if (time) { 10 | return new Promise(resolve => 11 | setTimeout(() => { 12 | fn(); 13 | resolve(); 14 | }, time) 15 | ); 16 | } 17 | 18 | return Promise.resolve().then(() => { 19 | fn(); 20 | }); 21 | } 22 | 23 | function timeout(time?: number) { 24 | return new Promise(resolve => setTimeout(resolve, time)); 25 | } 26 | 27 | test("no args with no error and run once", t => { 28 | let runCount = 0; 29 | 30 | @Connect 31 | class App extends React.Component { 32 | render() { 33 | runCount++; 34 | return ; 35 | } 36 | } 37 | 38 | create( 39 | 40 | 41 | 42 | ); 43 | 44 | return timeout().then(() => t.true(runCount === 1)); 45 | }); 46 | 47 | test("test connect inject", t => { 48 | let runCount = 0; 49 | 50 | @Connect 51 | class App extends React.Component { 52 | render() { 53 | runCount++; 54 | t.true(this.props.store.name === "bob"); 55 | return ; 56 | } 57 | } 58 | 59 | create( 60 | 61 | 62 | 63 | ); 64 | 65 | return timeout().then(() => t.true(runCount === 1)); 66 | }); 67 | 68 | test("test action and store but not use!", t => { 69 | let runCount = 0; 70 | 71 | @observable 72 | class Store { 73 | name = "bob"; 74 | } 75 | 76 | class Action { 77 | @inject(Store) store!: Store; 78 | 79 | setName(name: string) { 80 | this.store.name = name; 81 | } 82 | } 83 | 84 | const container = new Container(); 85 | container.set(Store, new Store()); 86 | container.set(Action, new Action()); 87 | 88 | @Connect 89 | class App extends React.Component { 90 | componentWillMount() { 91 | this.props.action.setName("nick"); 92 | } 93 | 94 | render() { 95 | runCount++; 96 | 97 | return ; 98 | } 99 | } 100 | 101 | create( 102 | 103 | 104 | 105 | ); 106 | 107 | return timeout().then(() => t.true(runCount === 1)); 108 | }); 109 | 110 | test("test action and store but use out render!", t => { 111 | let runCount = 0; 112 | let shouldNotChange = "shouldNotChange"; 113 | 114 | @observable 115 | class Store { 116 | name = "bob"; 117 | } 118 | 119 | class Action { 120 | @inject(Store) store!: Store; 121 | 122 | setName(name: string) { 123 | this.store.name = name; 124 | } 125 | } 126 | 127 | const container = new Container(); 128 | container.set(Store, new Store()); 129 | container.set(Action, new Action()); 130 | 131 | @Connect 132 | class App extends React.Component { 133 | componentWillMount() { 134 | t.true(this.props.store.name === "bob"); // use it, but not observered 135 | } 136 | 137 | componentWillReact() { 138 | shouldNotChange = "changed"; 139 | t.true(this.props.store.name === "nick"); // can't run 140 | } 141 | 142 | render() { 143 | runCount++; 144 | 145 | return ; 146 | } 147 | } 148 | 149 | create( 150 | 151 | 152 | 153 | ); 154 | 155 | return timeout() 156 | .then(() => t.true(runCount === 1)) 157 | .then(() => t.true(shouldNotChange === "shouldNotChange")); 158 | }); 159 | 160 | test("test action and store but use in render!", t => { 161 | let runCount = 0; 162 | 163 | @observable 164 | class Store { 165 | name = "bob"; 166 | } 167 | 168 | class Action { 169 | @inject(Store) store!: Store; 170 | 171 | setName(name: string) { 172 | this.store.name = name; 173 | } 174 | } 175 | 176 | const container = new Container(); 177 | container.set(Store, new Store()); 178 | container.set(Action, new Action()); 179 | 180 | @Connect 181 | class App extends React.Component { 182 | async componentWillMount() { 183 | t.true(this.props.store.name === "bob"); 184 | await immediate(() => { 185 | this.props.action.setName("nick"); 186 | }); 187 | } 188 | 189 | componentWillReact() { 190 | t.true(this.props.store.name === "nick"); // so it can run 191 | } 192 | 193 | render() { 194 | // use it! 195 | this.props.store.name; 196 | 197 | runCount++; 198 | 199 | return ; 200 | } 201 | } 202 | 203 | create( 204 | 205 | 206 | 207 | ); 208 | 209 | return timeout().then(() => t.true(runCount === 2)); // 2 210 | }); 211 | 212 | test("innert store connect", t => { 213 | let runCount = 0; 214 | 215 | @observable 216 | class Store { 217 | name = "bob"; 218 | } 219 | 220 | class Action { 221 | @inject(Store) store!: Store; 222 | 223 | setName(name: string) { 224 | this.store.name = name; 225 | } 226 | } 227 | 228 | const stores = injectFactory({ Store, Action }); 229 | 230 | @Connect({ 231 | store: stores.Store, 232 | action: stores.Action 233 | }) 234 | class App extends React.Component { 235 | async componentWillMount() { 236 | t.true(this.props.store.name === "bob"); 237 | await immediate(() => { 238 | this.props.action.setName("nick"); 239 | }); 240 | } 241 | 242 | componentWillReact() { 243 | t.true(this.props.store.name === "nick"); // so it can run 244 | } 245 | 246 | render() { 247 | // use it! 248 | this.props.store.name; 249 | 250 | runCount++; 251 | 252 | return ; 253 | } 254 | } 255 | 256 | create(); 257 | 258 | return timeout().then(() => t.true(runCount === 2)); // 2 259 | }); 260 | 261 | test("innert store connect and global provider", t => { 262 | let runCount = 0; 263 | 264 | @observable 265 | class Store { 266 | name = "bob"; 267 | } 268 | 269 | class Action { 270 | @inject(Store) store!: Store; 271 | 272 | setName(name: string) { 273 | this.store.name = name; 274 | } 275 | } 276 | 277 | @observable 278 | class GlobalStore { 279 | age = 1; 280 | } 281 | 282 | class GlobalAction { 283 | @inject(GlobalStore) store!: GlobalStore; 284 | 285 | setAge(age: number) { 286 | this.store.age = age; 287 | } 288 | } 289 | 290 | const stores = injectFactory({ Store, Action, GlobalStore, GlobalAction }); 291 | 292 | @Connect({ 293 | store: stores.Store, 294 | action: stores.Action 295 | }) 296 | class App extends React.Component { 297 | async componentWillMount() { 298 | t.true(this.props.store.name === "bob"); 299 | t.true(this.props.globalStore.age === 1); 300 | await immediate(() => { 301 | this.props.action.setName("nick"); 302 | }); 303 | await immediate(() => { 304 | this.props.globalAction.setAge(2); 305 | }); 306 | } 307 | 308 | componentWillReact() { 309 | t.true(this.props.store.name === "nick"); 310 | } 311 | 312 | render() { 313 | // use it! 314 | this.props.store.name; 315 | this.props.globalStore.age; 316 | 317 | runCount++; 318 | 319 | return ; 320 | } 321 | } 322 | 323 | create( 324 | 328 | 329 | 330 | ); 331 | 332 | return timeout().then(() => { 333 | t.true(runCount === 3); 334 | }); 335 | }); 336 | 337 | test("functional react component connect", t => { 338 | let runCount = 0; 339 | 340 | @observable 341 | class Store { 342 | name = "bob"; 343 | } 344 | 345 | class Action { 346 | @inject(Store) store!: Store; 347 | 348 | setName(name: string) { 349 | this.store.name = name; 350 | } 351 | } 352 | 353 | const stores = injectFactory({ Store, Action }); 354 | 355 | function App(this: any) { 356 | t.true(this.props.Store.name === "bob"); 357 | 358 | runCount++; 359 | 360 | return ; 361 | } 362 | 363 | const ConnectApp = Connect()(App); 364 | 365 | create( 366 | 367 | 368 | 369 | ); 370 | 371 | return timeout().then(() => t.true(runCount === 1)); 372 | }); 373 | 374 | test("functional call classable react component connect", t => { 375 | let runCount = 0; 376 | 377 | @observable 378 | class Store { 379 | name = "bob"; 380 | } 381 | 382 | class Action { 383 | @inject(Store) store!: Store; 384 | 385 | setName(name: string) { 386 | this.store.name = name; 387 | } 388 | } 389 | 390 | const stores = injectFactory({ Store, Action }); 391 | 392 | class App extends React.Component { 393 | render() { 394 | t.true(this.props.Store.name === "bob"); 395 | 396 | runCount++; 397 | 398 | return ; 399 | } 400 | } 401 | 402 | const ConnectApp = Connect()(App); 403 | 404 | create( 405 | 406 | 407 | 408 | ); 409 | 410 | return timeout().then(() => t.true(runCount === 1)); 411 | }); 412 | 413 | test("functional react component inner connect", t => { 414 | let runCount = 0; 415 | 416 | @observable 417 | class Store { 418 | name = "bob"; 419 | } 420 | 421 | class Action { 422 | @inject(Store) store!: Store; 423 | 424 | setName(name: string) { 425 | this.store.name = name; 426 | } 427 | } 428 | 429 | const stores = injectFactory({ Store, Action }); 430 | 431 | function App(this: any) { 432 | t.true(this.props.Store.name === "bob"); 433 | 434 | runCount++; 435 | 436 | return ; 437 | } 438 | 439 | const ConnectApp = Connect(stores)(App); 440 | 441 | create(); 442 | 443 | return timeout().then(() => t.true(runCount === 1)); 444 | }); 445 | 446 | test("functional store connect", t => { 447 | let runCount = 0; 448 | 449 | @observable 450 | class Store { 451 | user = { 452 | name: "lucy" 453 | }; 454 | } 455 | 456 | class Action { 457 | @inject(Store) store!: Store; 458 | } 459 | 460 | const stores = injectFactory({ Store, Action }); 461 | 462 | @Connect(state => { 463 | return { 464 | user: state!.Store!.user 465 | }; 466 | }) 467 | class App extends React.Component { 468 | render() { 469 | t.true(this.props.user.name === "lucy"); 470 | runCount++; 471 | 472 | return ; 473 | } 474 | } 475 | 476 | create( 477 | 478 | 479 | 480 | ); 481 | 482 | return timeout().then(() => t.true(runCount === 1)); // 2 483 | }); 484 | 485 | test("unmount will disConnect", t => { 486 | let runCount = 0; 487 | 488 | @observable 489 | class Store { 490 | name = "lucy"; 491 | } 492 | 493 | class Action { 494 | @inject(Store) store!: Store; 495 | 496 | setName(name: string) { 497 | this.store.name = name; 498 | } 499 | } 500 | 501 | const stores = injectFactory({ Store, Action }); 502 | 503 | @Connect(stores) 504 | class App extends React.Component { 505 | render() { 506 | runCount++; 507 | 508 | return {this.props.Store.name}; 509 | } 510 | } 511 | 512 | @Connect(stores) 513 | class Container extends React.Component { 514 | async componentWillMount() { 515 | await immediate(() => { 516 | this.props.Action.setName("nick"); 517 | }); 518 | await immediate(() => { 519 | this.setAppHidden(); 520 | }); 521 | await immediate(() => { 522 | this.props.Action.setName("lucy"); 523 | }); 524 | } 525 | 526 | state = { 527 | showApp: true 528 | }; 529 | 530 | setAppHidden() { 531 | this.setState({ 532 | showApp: false 533 | }); 534 | } 535 | 536 | render() { 537 | if (!this.state.showApp) { 538 | return null; 539 | } 540 | 541 | return ; 542 | } 543 | } 544 | 545 | create(); 546 | 547 | return timeout().then(() => t.true(runCount === 2)); 548 | }); 549 | --------------------------------------------------------------------------------