├── .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 | logout :
31 | login }
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 |
set_to_0
123 |
increment_version_1
124 |
increment_version_2
125 |
increment_version_3
126 |
increment_async
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 |
set_to_0
165 |
increment_version_1
166 |
increment_version_2
167 |
increment_version_3
168 |
increment_async
169 |
}
170 |
171 | Something Others
172 |
173 | {counter =>
174 |
{counter.state.count}
175 |
set_to_0
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 |
set_to_0
86 |
increment_version_1
87 |
increment_version_2
88 |
increment_version_3
89 |
increment_async
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 |
set_to_0
128 |
increment_version_1
129 |
increment_version_2
130 |
increment_version_3
131 |
increment_async
132 |
}
133 |
134 | Something Others
135 |
136 | {counter =>
137 |
{counter.state.count}
138 |
set_to_0
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 |
--------------------------------------------------------------------------------