├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src ├── createContext.ts ├── index.ts └── test.tsx └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | lib/ 61 | package-lock.json 62 | 63 | .vscode/ 64 | yarn.lock 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | package-lock.json 61 | 62 | .vscode/ 63 | yarn.lock 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | # create-react-context 2 | A dead simple way to share states across react components, using javascript scope rather than react context api. 3 | 4 | ## Install 5 | `npm i @xialvjun/create-react-context` or `yarn add @xialvjun/create-react-context` 6 | 7 | ## Example 8 | codesandbox.io: https://codesandbox.io/s/5zz2m570l 9 | 10 | ```jsx 11 | import { createContext } from '@xialvjun/create-react-context'; 12 | 13 | const Auth = createContext({ 14 | state: { logined: false }, 15 | login() { 16 | setTimeout(() => { 17 | // here, autocomplete is not very good. 18 | this.setState({ logined: true }); 19 | }, 1000); 20 | }, 21 | logout: () => { 22 | // here, autocomplete is totally ok. 23 | Auth.getContext().setState({ logined: false }); 24 | }, 25 | }); 26 | 27 | 28 | {auth =>
29 | {auth.state.logined ? 30 | : 31 | } 32 |
} 33 |
34 | ``` 35 | 36 | 37 | ## Documents 38 | 39 | ```jsx 40 | import { createContext } from '@xialvjun/create-react-context'; 41 | import { render } from 'react-dom'; 42 | // instead of 'react-adopt', I recommend my '@xialvjun/react-compose'. 'react-adopt' has some bugs. 43 | import { Compose } from '@xialvjun/react-compose'; 44 | 45 | 46 | // setState is async 47 | const Counter = createContext({ 48 | state: { count: 0 }, 49 | increment_version_1() { 50 | // assume now `this.state.count === 0` 51 | console.log('1. now the count is ', this.state.count); // 1. 0 52 | this.setState({ count: this.state.count + 1 }); 53 | console.log('2. now the count is ', this.state.count); // 2. 0 54 | this.setState(state => { 55 | console.log('3. now the count is ', state.count); // 3. 1 56 | return { count: state.count + 1 }; 57 | }, () => { 58 | console.log('7. now the count is ', this.state.count); // 7. 3 59 | }); 60 | console.log('4. now the count is ', this.state.count); // 4. 0 61 | this.setState(state => { 62 | console.log('5. now the count is ', state.count); // 5. 2 63 | return { count: state.count + 1 }; 64 | }, () => { 65 | console.log('8. now the count is ', this.state.count); // 8. 3 66 | }); 67 | console.log('6. now the count is ', this.state.count); // 6. 0 68 | }, 69 | increment_version_2() { 70 | // assume now `this.state.count === 0` 71 | console.log('1. now the count is ', this.state.count); // 1. 0 72 | this.setState({ count: this.state.count + 1 }); 73 | console.log('2. now the count is ', this.state.count); // 2. 0 74 | this.setState(state => { 75 | console.log('3. now the count is ', state.count); // 3. 1 76 | return { count: state.count + 1 }; 77 | }, () => { 78 | console.log('8. now the count is ', this.state.count); // 8. 1 79 | }); 80 | console.log('4. now the count is ', this.state.count); // 4. 0 81 | this.setState(state => { 82 | console.log('5. now the count is ', state.count); // 5. 2 83 | return { count: state.count + 1 }; 84 | }, () => { 85 | console.log('9. now the count is ', this.state.count); // 9. 1 86 | }); 87 | console.log('6. now the count is ', this.state.count); // 6. 0 88 | this.setState({ count: this.state.count + 1 }); 89 | console.log('7. now the count is ', this.state.count); // 7. 0 90 | }, 91 | increment_version_3() { 92 | // there is also a `setStateSync` 93 | // assume now `this.state.count === 0` 94 | console.log('1. now the count is ', this.state.count); // 1. 0 95 | this.setStateSync({ count: this.state.count + 1 }); 96 | console.log('2. now the count is ', this.state.count); // 2. 1 97 | const old_state = this.state; 98 | this.setStateSync({ count: this.state.count + 1 }); 99 | // state is replaced rather than modified 100 | console.log('3. now the old_state.count is ', old_state.count); // 3. 1 101 | console.log('4. now the count is ', this.state.count); // 4. 2 102 | this.setStateSync({ count: this.state.count + 1 }); 103 | console.log('5. now the count is ', this.state.count); // 5. 3 104 | }, 105 | set_to_0() { 106 | // ! use normal function and use this in it, you don't have right type signature. 107 | // ! It means editor can not autocomplete this.set_to_0 108 | // * but you can use Counter.getContext() to get the real context obj, then the autcomplete will be fine 109 | // * so because you didn't use this in the function, you can use arrow function. 110 | this.setState({ count: 0 }); 111 | }, 112 | increment_async() { 113 | setTimeout(() => { 114 | this.setState({ count: this.state.count + 1 }); 115 | }, 1000); 116 | }, 117 | }); 118 | 119 | const setState_is_async = 120 | {counter =>
121 |
{counter.state.count}
122 | 123 | 124 | 125 | 126 | 127 |
} 128 |
129 | 130 | const Auth = createContext({ 131 | state: { logined: false }, 132 | login() { 133 | setTimeout(() => { 134 | this.setState({ logined: true }); 135 | }, 1000); 136 | }, 137 | logout() { 138 | this.setState({ logined: false }); 139 | }, 140 | }); 141 | 142 | const we_can_compose_the_render_props = 143 | {({ counter, auth }) =>
144 | {counter.state.count} 145 |
} 146 |
147 | 148 | // you can operate the context outside of React 149 | const counter = Counter.getContext(); 150 | counter.increment_async(); 151 | 152 | // you can not only use Render Props Component, but also HOC 153 | const CustomComponent = ({ counter }) => { 154 | return
{counter.state.count}
155 | } 156 | const WrappedCustomComponent = Counter.hoc('counter')(CustomComponent); 157 | 158 | 159 | function App() { 160 | return
161 | 162 | {counter =>
163 |
{counter.state.count}
164 | 165 | 166 | 167 | 168 | 169 |
} 170 |
171 | Something Others 172 | 173 | {counter =>
174 |
{counter.state.count}
175 | 176 |
} 177 |
178 | , counter2: ({ children }) => {children} }}> 179 | {({ counter, auth, counter2 }) => null} 180 | 181 |
182 | } 183 | render(, document.querySelector('#root')); 184 | ``` 185 | 186 | # Migrate from v0 187 | 1. Remove `Contexts`: 188 | > There is a better one: [@xialvjun/react-compose](https://github.com/xialvjun/react-compose) 189 | 190 | 2. Change `setState` from sync to async: 191 | > Its signature is the same as React.Component.setState. 192 | 193 | 3. Add `setStateSync(partialState: Object)`: 194 | > The same as v0's `setState`. 195 | 196 | 4. Add `hoc(name: string)`: 197 | > Render Props Component and Higher Order Component are both good things for sharing states, we shouldn't ignore any one. 198 | 199 | # FAQ 200 | 1. Why state change is ~~sync~~async? 201 | > ~~React has done the optimization for us: **forceUpdate twice just render once**. And sync state is easy to use.~~ 202 | > State should keep sync with the view. But view rendering is async, `setState` should be async too. 203 | 204 | 2. Why add `setStateSync`? 205 | > Some times we just need **Eventual Consistency** rather than **Strong Consistency** between state and view, `setStateSync` is for this. 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xialvjun/create-react-context", 3 | "version": "1.1.2", 4 | "description": "A dead simple way to share states across react components, using javascript scope rather than react context api.", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/xialvjun/create-react-context.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "context" 18 | ], 19 | "author": "xialvjun@live.com", 20 | "license": "MIT", 21 | "peerDependencies": { 22 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "^16.3.8", 26 | "typescript": "^2.8.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/createContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Component, ReactNode } from 'react'; 3 | 4 | export function createContext(model: M) { 5 | type S = M['state']; 6 | const listeners = []; 7 | 8 | let timeout = null; 9 | let new_state = null; 10 | let callbacks= []; 11 | 12 | function updater() { 13 | timeout = null; 14 | ctx.state = new_state; 15 | new_state = null; 16 | const cbs = callbacks.slice(); 17 | callbacks = []; 18 | cbs.forEach(it => it()); 19 | listeners.forEach(it => it()); 20 | } 21 | 22 | const ctx = Object.assign({}, model, { 23 | setState( 24 | partialState: ((prevState: Readonly) => (Partial | null)) | (Partial | null), 25 | callback?: () => void 26 | ) { 27 | callback && callbacks.push(callback); 28 | const current_state = new_state || ctx.state; 29 | if (typeof partialState === 'function') { 30 | partialState = partialState(current_state); 31 | } 32 | new_state = Object.assign({}, current_state, partialState); 33 | clearTimeout(timeout); 34 | timeout = setTimeout(updater, 0); 35 | }, 36 | setStateSync(partialState: Partial | null) { 37 | ctx.state = Object.assign({}, ctx.state, partialState); 38 | listeners.forEach(it => it()); 39 | }, 40 | }); 41 | Object.keys(ctx).forEach(key => { 42 | if (typeof ctx[key] === 'function') { 43 | ctx[key] = ctx[key].bind(ctx); 44 | } 45 | }); 46 | 47 | return class Context extends Component<{ children: (context: typeof ctx) => ReactNode }> { 48 | static getContext = () => ctx 49 | static hoc = (name: string) => BaseComponent => class HOC extends Component { 50 | __update__ = () => this.forceUpdate() 51 | componentDidMount() { 52 | listeners.push(this.__update__); 53 | } 54 | componentWillUnmount() { 55 | listeners.splice(listeners.indexOf(this.__update__), 1); 56 | } 57 | render() { 58 | return React.createElement(BaseComponent, { ...this.props, [name]: ctx }); 59 | } 60 | } 61 | __update__ = () => this.forceUpdate() 62 | componentDidMount() { 63 | listeners.push(this.__update__); 64 | } 65 | componentWillUnmount() { 66 | listeners.splice(listeners.indexOf(this.__update__), 1); 67 | } 68 | render() { 69 | return this.props.children(ctx); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from './createContext'; 2 | 3 | 4 | export { 5 | createContext, 6 | } 7 | -------------------------------------------------------------------------------- /src/test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | // instead of 'react-adopt', I recommend my '@xialvjun/react-compose'. 'react-adopt' has some bugs. 4 | import { Compose } from '@xialvjun/react-compose'; 5 | 6 | import { createContext } from './index'; 7 | 8 | 9 | // setState is async 10 | const Counter = createContext({ 11 | state: { count: 0 }, 12 | increment_version_1() { 13 | // assume now `this.state.count === 0` 14 | console.log('1. now the count is ', this.state.count); // 1. 0 15 | this.setState({ count: this.state.count + 1 }); 16 | console.log('2. now the count is ', this.state.count); // 2. 0 17 | this.setState(state => { 18 | console.log('3. now the count is ', state.count); // 3. 1 19 | return { count: state.count + 1 }; 20 | }, () => { 21 | console.log('7. now the count is ', this.state.count); // 7. 3 22 | }); 23 | console.log('4. now the count is ', this.state.count); // 4. 0 24 | this.setState(state => { 25 | console.log('5. now the count is ', state.count); // 5. 2 26 | return { count: state.count + 1 }; 27 | }, () => { 28 | console.log('8. now the count is ', this.state.count); // 8. 3 29 | }); 30 | console.log('6. now the count is ', this.state.count); // 6. 0 31 | }, 32 | increment_version_2() { 33 | // assume now `this.state.count === 0` 34 | console.log('1. now the count is ', this.state.count); // 1. 0 35 | this.setState({ count: this.state.count + 1 }); 36 | console.log('2. now the count is ', this.state.count); // 2. 0 37 | this.setState(state => { 38 | console.log('3. now the count is ', state.count); // 3. 1 39 | return { count: state.count + 1 }; 40 | }, () => { 41 | console.log('8. now the count is ', this.state.count); // 8. 1 42 | }); 43 | console.log('4. now the count is ', this.state.count); // 4. 0 44 | this.setState(state => { 45 | console.log('5. now the count is ', state.count); // 5. 2 46 | return { count: state.count + 1 }; 47 | }, () => { 48 | console.log('9. now the count is ', this.state.count); // 9. 1 49 | }); 50 | console.log('6. now the count is ', this.state.count); // 6. 0 51 | this.setState({ count: this.state.count + 1 }); 52 | console.log('7. now the count is ', this.state.count); // 7. 0 53 | }, 54 | increment_version_3() { 55 | // there is also a `setStateSync` 56 | // assume now `this.state.count === 0` 57 | console.log('1. now the count is ', this.state.count); // 1. 0 58 | this.setStateSync({ count: this.state.count + 1 }); 59 | console.log('2. now the count is ', this.state.count); // 2. 1 60 | const old_state = this.state; 61 | this.setStateSync({ count: this.state.count + 1 }); 62 | // state is replaced rather than modified 63 | console.log('3. now the old_state.count is ', old_state.count); // 3. 1 64 | console.log('4. now the count is ', this.state.count); // 4. 2 65 | this.setStateSync({ count: this.state.count + 1 }); 66 | console.log('5. now the count is ', this.state.count); // 5. 3 67 | }, 68 | set_to_0: () => { 69 | // ! use normal function and use this in it, you don't have right type signature. 70 | // ! It means editor can not autocomplete this.set_to_0 71 | // * but you can use Counter.getContext() to get the real context obj, then the autcomplete will be fine 72 | // * so because you didn't use this in the function, you can use arrow function. 73 | Counter.getContext().setState({ count: 0 }); 74 | }, 75 | increment_async() { 76 | setTimeout(() => { 77 | this.setState({ count: this.state.count + 1 }); 78 | }, 1000); 79 | }, 80 | }); 81 | 82 | const setState_is_async = 83 | {counter =>
84 |
{counter.state.count}
85 | 86 | 87 | 88 | 89 | 90 |
} 91 |
92 | 93 | const Auth = createContext({ 94 | state: { logined: false }, 95 | login() { 96 | setTimeout(() => { 97 | this.setState({ logined: true }); 98 | }, 1000); 99 | }, 100 | logout() { 101 | this.setState({ logined: false }); 102 | }, 103 | }); 104 | 105 | const we_can_compose_the_render_props = 106 | {({ counter, auth }) =>
107 | {counter.state.count} 108 |
} 109 |
110 | 111 | // you can operate the context outside of React 112 | const counter = Counter.getContext(); 113 | counter.increment_async(); 114 | 115 | // you can not only use Render Props Component, but also HOC 116 | const CustomComponent = ({ counter }) => { 117 | return
{counter.state.count}
118 | } 119 | const WrappedCustomComponent = Counter.hoc('counter')(CustomComponent); 120 | 121 | 122 | function App() { 123 | return
124 | 125 | {counter =>
126 |
{counter.state.count}
127 | 128 | 129 | 130 | 131 | 132 |
} 133 |
134 | Something Others 135 | 136 | {counter =>
137 |
{counter.state.count}
138 | 139 |
} 140 |
141 | {_=>_}, counter2: ({ children }) => {children} }}> 142 | {({ counter, auth, counter2 }) => null} 143 | 144 |
145 | } 146 | render(, document.querySelector('#root')); 147 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "lib", 6 | "declaration": true, 7 | "jsx": "react", 8 | "lib": [ 9 | "es2015", 10 | "dom" 11 | ], 12 | } 13 | } 14 | --------------------------------------------------------------------------------