├── .babelrc
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── .travis.yml
├── README.md
├── codecov.yml
├── docs
└── about.md
├── package.json
├── src
├── createStore.js
├── index.js
├── pub.js
├── store.js
├── stores.js
├── useReactHooks.js
├── useStore.js
└── util.js
└── test
├── __snapshots__
└── createStore.test.js.snap
├── createStore.test.js
├── store.test.js
├── todo.js
├── todo.test.js
└── util.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env"],
4 | [
5 | "@babel/preset-react",
6 | {
7 | "development": true
8 | }
9 | ]
10 | ],
11 | "plugins": []
12 | }
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: 'babel-eslint',
3 | extends: ['airbnb', 'prettier', 'plugin:prettier/recommended'],
4 | env: {
5 | browser: true,
6 | es6: true,
7 | jest: true,
8 | },
9 | rules: {
10 | 'no-param-reassign': 0,
11 | 'no-plusplus': 0,
12 | 'react/jsx-filename-extension': [1, { extensions: ['.js'] }],
13 | 'jsx-a11y/click-events-have-key-events': 0,
14 | 'jsx-a11y/no-noninteractive-element-interactions': 0,
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules
3 | coverage
4 | .idea/
5 | .DS_Store
6 | .vscode
7 | *.swp
8 | *.lock
9 | yarn-error.log
10 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "printWidth": 100,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | language: node_js
3 | node_js:
4 | - '10'
5 | script:
6 | - npm run test
7 | after_success:
8 | - npm run coverage
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### iostore
2 |
3 | [](https://npmjs.org/package/iostore)
4 | [](https://travis-ci.org/yisbug/iostore)
5 | [](https://codecov.io/gh/yisbug/iostore)
6 | [](https://snyk.io/test/npm/iostore)
7 | [](https://david-dm.org/yisbug/iostore)
8 |
9 | [背景介绍](docs/about.md)
10 |
11 | 由原 `react-hooks-model` 更名为 `iostore`。
12 |
13 | 极简的全局数据管理方案,忘掉 `redux`、`state`、`reducer`、`action`、`observer` 这些复杂的概念吧。
14 |
15 | ### 特性
16 |
17 | - 总计只有 100 多行代码。
18 | - 只需要学会两个 `API` 即可使用,非常简单:`createStore()`、`useStore()`。
19 | - 像普通的 `js` 对象一样定义 `store`。
20 | - 像普通的 `js` 对象一样使用数据和方法。
21 | - `store` 定义的方法内部可任意修改数据,可直接返回数据,支持同步、异步方法。
22 | - 当数据发生变化时,自动触发组件渲染。基于`React Hooks API`,实现了完整的单向数据流。
23 | - 集成异步方法的执行状态管理,目前最优雅的`loading`状态解决方案之一。
24 | - `store` 内部方法可以使用`this.stores.TodoStore`访问其他的 `store` 示例,实现更复杂的数据交互。
25 |
26 | 和之前的方案相比:
27 |
28 | - 不再区分 `state`, `reducer`, `helper`,去掉了这些概念,更简单。
29 | - 定义 `store` 就像定义一个普通的 `js object` 一样,只需要传入一个 `namespace` 用于区分不同的 `store`。
30 | - 基于 `Proxy` 重新设计,数据变化,则自动通知组件,重新渲染。
31 |
32 | ### TODO
33 |
34 | - [ ] TypeScript 支持
35 | - [ ] 支持 Vuejs
36 | - [ ] 更多的测试用例
37 |
38 | ### 如何使用
39 |
40 | 安装:
41 |
42 | ```shell
43 | npm install iostore
44 | // or
45 | yarn add iostore
46 | ```
47 |
48 | ### API
49 |
50 | 引入
51 |
52 | ```js
53 | import { createStore, useStore } from 'iostore';
54 | ```
55 |
56 | #### createStore(params)
57 |
58 | 定义一个 store。参数:
59 |
60 | 普通的 js 对象,必须指定一个`namespace`。
61 |
62 | ```js
63 | // TodoStore.js
64 | import { createStore } from 'iostore';
65 | createStore({
66 | namespace: 'TodoStore',
67 | todos: [],
68 | getTodoCount() {
69 | return this.todos.length;
70 | },
71 | getNs() {
72 | return this.namespace;
73 | },
74 | ...rest, // 其余自定义的数据和方法
75 | });
76 |
77 | // UserStore.js
78 | import { createStore } from 'iostore';
79 | createStore({
80 | namespace: 'UserStore',
81 | // 访问其他 store 的方法。
82 | getTodoCount() {
83 | return this.stores.TodoStore.getTodoCount();
84 | },
85 | ...rest, // 其余自定义的数据和方法
86 | });
87 | ```
88 |
89 | #### useStore()
90 |
91 | 在 `React` 函数式组件中引入所需 `store`。 无参数。
92 | 得益于 ES6 中的解构赋值语法,我们从该方法的返回值中,单独声明所需的 store。
93 |
94 | > 框架会在 `store` 中注入 `stores` 对象,用来访问其他 `store` 的数据。
95 | > 一般来说,只推荐访问其他 `store` 的计算数据,不要访问其他 `store` 中可能导致修改数据的方法。
96 | > 如果需要修改其他 `store` 的数据,请在逻辑层/组件内处理。
97 |
98 | 如下:
99 |
100 | ```js
101 | const Todo = () => {
102 | const { TodoStore } = useStore();
103 | // 之后便可以自由的使用 TodoStore 中定义的方法了。
104 | const ns = TodoStore.getNs();
105 | return
{ns}
;
106 | };
107 | ```
108 |
109 | #### 关于 loading
110 |
111 | 在对交互要求较高的场景下,获取异步方法的执行状态是非常必要的。
112 |
113 | 例如显示一个 `loading` 页面告诉用户正在加载数据,按钮上显示一个`loading`样式提示用户该按钮已经被点击。
114 |
115 | 当你使用`iostore`时,这一切变得非常简单。
116 |
117 | 我们可以非常容易的获取到每一个异步方法的`loading`状态,甚至可以获取到一个`store`下有没有异步方法正在执行。
118 |
119 | - 获取`store`中有没有异步方法正在执行:`Store.loading`,返回 `true/false`
120 | - 获取`store`中某个异步方法的 loading 状态:`Store.asyncFunction.loading`,返回 `true/false`
121 |
122 | 示例如下:
123 |
124 | ```js
125 | // 定义 store
126 | createStore({
127 | namespace: 'TodoStore',
128 | id: 0,
129 | async inc() {
130 | await sleep(1000 * 5);
131 | this.id++;
132 | },
133 | });
134 |
135 | // 获取 loading 状态
136 | const Todo = () => {
137 | const { TodoStore } = useStore();
138 | const handleClick = () => TodoStore.inc();
139 | // TodoStore.loading store 级别的 loading 状态
140 | // TodoStore.inc.loading 某个异步方法的 loading 状态
141 | return (
142 |
145 | );
146 | };
147 | ```
148 |
149 | ### 完整的 Todo 示例
150 |
151 | ```js
152 | // TodoStore.js
153 | import store, { createStore, useStore } from 'iostore';
154 | export default createStore({
155 | namespace: 'TodoStore', // store 命名空间
156 | id: 0,
157 | todos: [
158 | {
159 | id: 0,
160 | content: 'first',
161 | status: 'DOING',
162 | },
163 | ],
164 | addTodo(content) {
165 | this.id++;
166 | const todo = {
167 | id: this.id,
168 | content,
169 | status: 'DOING',
170 | };
171 | this.todos.push(todo);
172 | },
173 | getTodoById(id) {
174 | return this.todos.filter(item => item.id === id)[0];
175 | },
176 | updateTodo(id, status) {
177 | const todo = this.getTodoById(id);
178 | if (!todo) return;
179 | todo.status = status;
180 | },
181 | // test async function
182 | incId: 0,
183 | async delayIncId() {
184 | await sleep(1000 * 3);
185 | this.incId++;
186 | },
187 | });
188 |
189 | // Todos.js
190 | import React, { useRef } from 'react';
191 | import store, { createStore, useStore } from '../src/index';
192 | import todoStore from './TodoStore';
193 |
194 | export default () => {
195 | /**
196 | * 获取 TodoStore 的几种方式:
197 | * const { TodoStore } = useStore(); // 更符合 React Hooks 的理念
198 | * const { TodoStore } = store;
199 | * const TodoStore = todoStore.useStore();
200 | */
201 | const { TodoStore } = useStore();
202 | const inputEl = useRef(null);
203 | const handleClick = item => {
204 | if (item.status === 'DOING') {
205 | TodoStore.updateTodo(item.id, 'COMPLETED');
206 | } else if (item.status === 'COMPLETED') {
207 | TodoStore.updateTodo(item.id, 'DOING');
208 | }
209 | };
210 | const handleAddTodo = () => {
211 | console.warn('set data within component, should be got console.error : ');
212 | TodoStore.todos[0].id = 1000;
213 | const text = inputEl.current.value;
214 | if (text) {
215 | TodoStore.addTodo(text);
216 | }
217 | };
218 | console.log('render', 'totos.length:' + TodoStore.todos.length);
219 | return (
220 |
221 |
{TodoStore.incId}
222 | {!TodoStore.delayIncId.loading ?
: ''}
223 |
224 |
{TodoStore.delayIncId.loading ? 'loading' : 'completed'}
225 |
{TodoStore.todos.length}
226 |
227 | {TodoStore.todos.map(item => {
228 | return (
229 | - handleClick(item)} key={item.id}>
230 | {item.content}
231 | {item.status}
232 |
233 | );
234 | })}
235 |
236 |
237 |
240 |
241 |
244 |
245 | );
246 | };
247 | ```
248 |
249 | ### License
250 |
251 | MIT
252 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: off
2 | coverage:
3 | status:
4 | project:
5 | default:
6 | threshold: 90%
7 | patch: off
8 |
--------------------------------------------------------------------------------
/docs/about.md:
--------------------------------------------------------------------------------
1 | # 极简的数据管理方案 iostore
2 |
3 | ### 背景
4 |
5 | 之前写过一篇文章,简单的介绍了一个思路,主要是利用 `React Hooks API` 的特性做一个全局的数据管理方案,并且写了一个简单的 `demo`,随后丢到了 `Github` 上。
6 |
7 | 这段时间基于这个 `demo` 结合 `antd-design-pro` 做了几个实际的项目,又接入了一些旧的项目。具体的业务场景包括:登陆、登出、基于 `nodejs` 后端 `RESTFul` 的接口的增删改查、表单、搜索、多个 `store` 之间交互(产品、产品分类)、数组转 `treeNode`、权限管理控制 等等,可以说大部分场景都有所应用。
8 |
9 | 总体来说,开发体验确实蛮爽的。定义好 `store` 的数据接口和方法,组件里引入 `store`,剩下的代码就是一把梭,畅快无比。
10 |
11 | 主要是没有思想负担了,想改数据?看一眼 `store` 文件,直接调用对应接口。想获取数据?同样,看一眼就知道了。看完之后怎么用?`store` 里怎么定义的就怎么用啊,不需要 `dispatch,commit`,直接像原生方法一样调用就可以了。
12 |
13 | **而且,等后续支持了 TypeScript 就会更爽。**
14 |
15 | 其实本来是不会有这篇文章的,之前那个 `demo` 也可能会像以前搞的若干烂尾项目一样被人遗忘。因代码很少,自己的 `React` 项目需要用的话就复制一份,方便,改起来也容易。其次,如果思路清晰的话,开发这样的一个小工具库也挺简单的,如果只是简单的造轮子那就快速撸一个好了。但如果开源到 `Github` ,还是需要花不少精力来维护的,所以这也是一个阻力。
16 |
17 | 这段时间,之前发布的文章到现在也有一些赞和探讨,也有一些方案借鉴了这个思路,例如阿里的 https://github.com/ice-lab/icestore ,其 README.md 中提到的 `react-hooks-model` 便是本人提供的思路。尽管如此,我在 `Github` 上搜索了一下,还是很少有类似思路的方案实现的数据管理库,有些要么实现过于复杂,有些思路就不太一样,比较可惜,都无法达到我想要的极简的开发体验:包体积够小、开箱即用、没有复杂概念、定义数据就可以直接使用数据。
18 |
19 | 此外,近期接手了一个 `vuejs` 的项目,使用了 `vuex` 的方案。这个代码,写起来真叫一个繁琐,各种 `dispatch,commit`,再加上使用 `TypeScript` 的不当姿势,最终的代码看起来简直不忍直视,拖沓无比。尤其在 `React` 中使用过我自制的方案后,更觉得无法忍受 `vuex` 这个模式。这个项目想重构成 `React` 是不太可能了,目前项目已经非常大,够复杂了。于是呼,仔细想了一下是否可以把这个数据方案移植到 `vuejs` 的项目中,感觉是完全可行的,尤大本人也写了一个实验性质的库:https://github.com/yyx990803/vue-hooks 。
20 |
21 | 既然如此,那就说干就干吧,反正头发也没了。
22 |
23 | 第一步,现梳理现有的代码,修复之前的 `bug`,进一步简化 `API`,并做了一些简单的约定,例如不允许在 `store` 外部修改数据。
24 |
25 | 基于以上,于是就有了 `iostore` 的第一个版本,简介:
26 |
27 | **极简的全局数据管理方案,忘掉 `redux`、`state`、`reducer`、`action`、`observer` 这些复杂的概念吧。**
28 |
29 | ### 特性
30 |
31 | - 总计只有 100 多行代码。
32 | - 只需要学会两个 API 即可使用,非常简单:`createStore()`、`useStore()`。
33 | - 像普通的 `js` 对象一样定义 `store`。
34 | - 像普通的 `js` 对象一样使用数据和方法。
35 | - `store` 定义的方法内部可任意修改数据,可直接返回数据,支持同步、异步方法。
36 | - 当数据发生变化时,自动触发组件渲染。基于 `React Hooks API`,实现了单向数据流。
37 | - 集成异步方法的执行状态管理,目前最优雅的 `loading` 状态解决方案之一。
38 | - `store` 内部方法可以使用 `this.stores.TodoStore` 访问其他的 `store` 示例,实现更复杂的数据交互。
39 |
40 | ### 安装
41 |
42 | ```shell
43 | npm install iostore --save
44 | // or
45 | yarn add iostore
46 | ```
47 |
48 | ### 最简 `demo`
49 |
50 | ```js
51 | // demo.js
52 | import React, { useEffect } from 'react';
53 | import { createStore, useStore } from 'iostore';
54 |
55 | export default () => {
56 | const MyStore = createStore({
57 | namespace: 'MyStore', // 已经注册到全局 store
58 | firstName: 'li',
59 | lastName: 'lei',
60 | fullName: '',
61 | userInfo: {},
62 | getFullName() {
63 | this.fullName = `${this.firstName} ${this.lastName}`;
64 | },
65 | async getMyLoginInfo() {
66 | const res = await fetch('/api/userinfo');
67 | this.userInfo = res;
68 | },
69 | }).useStore();
70 |
71 | const { loading } = MyStore;
72 | const fetchUserInfo = () => {
73 | MyStore.getMyLoginInfo();
74 | };
75 |
76 | useEffect(() => {
77 | MyStore.getFullName();
78 | }, []);
79 |
80 | return (
81 |
82 |
{MyStore.fullName}
83 |
{MyStore.firstName}
84 |
{MyStore.lastName}
85 |
86 | 用户信息:
87 | {MyStore.userInfo.nickname || ''}
88 |
89 |
92 |
93 | );
94 | };
95 | // 另一个希望复用 MyStore 的组件
96 | import { useStore } from 'iostore';
97 | export default () => {
98 | const { MyStore } = useStore(); // 直接声明使用全局的 store
99 | return {MyStore.fullName}
;
100 | };
101 | ```
102 |
103 | ### 项目后续计划
104 |
105 | - [ ] 支持 `TypeScript`
106 | - [ ] 支持 `Vuejs`
107 | - [ ] 完善更多的测试用例
108 |
109 | 此外,本人目前还在为腾讯云工作,后续将有可能先应用到腾讯云内部的一些业务,所以可以暂时放心,项目不会那么快就烂尾。
110 | 更多详情欢迎移步 `Github` 上围观,地址:https://github.com/yisbug/iostore
111 | 如果觉得还不错的话,欢迎试用哈,方便的话顺手点个 star,如有问题可以留言,也可以邮件 `yisbug@qq.com` 交流,多谢。
112 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iostore",
3 | "version": "0.0.10",
4 | "main": "src/index.js",
5 | "scripts": {
6 | "postpublish": "git push origin --all; git push origin --tags",
7 | "postversion": "npm publish",
8 | "preversion": "npm test",
9 | "test": "NODE_ENV=unittest jest",
10 | "test:w": "jest --watchAll",
11 | "coverage": "codecov"
12 | },
13 | "author": "yisbug@qq.com",
14 | "license": "MIT",
15 | "description": "",
16 | "devDependencies": {
17 | "@babel/polyfill": "^7.4.3",
18 | "@babel/preset-env": "^7.4.3",
19 | "@babel/preset-react": "^7.0.0",
20 | "babel-eslint": "^10.0.1",
21 | "babel-jest": "^24.7.1",
22 | "codecov": "^3.5.0",
23 | "enzyme": "^3.9.0",
24 | "enzyme-adapter-react-16": "^1.12.1",
25 | "eslint": "^5.16.0",
26 | "eslint-config-airbnb": "^17.1.0",
27 | "eslint-config-prettier": "^4.3.0",
28 | "eslint-plugin-import": "^2.17.3",
29 | "eslint-plugin-jsx-a11y": "^6.2.1",
30 | "eslint-plugin-prettier": "^3.1.0",
31 | "eslint-plugin-react": "^7.13.0",
32 | "jest": "^24.7.1",
33 | "prettier": "^1.17.1",
34 | "react": "^16.8.6",
35 | "react-dom": "^16.8.6",
36 | "react-testing-library": "^7.0.1"
37 | },
38 | "peerDependencies": {
39 | "react": ">=16.8.6",
40 | "react-dom": ">=16.8.6"
41 | },
42 | "jest": {
43 | "testRegex": "(/test/.*\\.test\\.js)$",
44 | "coverageDirectory": "./coverage/",
45 | "collectCoverage": true
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/createStore.js:
--------------------------------------------------------------------------------
1 | import { isPromise, isUndefined, addProxy, isFunction } from './util';
2 | import { broadcast } from './pub';
3 | import stores from './stores';
4 | import useStore from './useStore';
5 |
6 | const disableProps = ['loading', 'stores', 'useStore'];
7 |
8 | export default config => {
9 | const { namespace = '', ...rest } = config;
10 | let service;
11 | let isChanged = false;
12 |
13 | const reducers = {};
14 | const state = { namespace };
15 |
16 | if (!namespace) {
17 | throw new Error('Invalid params, namespace is required.');
18 | }
19 | if (stores[namespace]) {
20 | return stores[namespace];
21 | }
22 | disableProps.forEach(key => {
23 | if (!isUndefined(rest[key])) {
24 | throw new Error(`${key} is not allowd in params.`);
25 | }
26 | });
27 |
28 | Object.keys(rest).forEach(key => {
29 | if (isFunction(rest[key])) {
30 | reducers[key] = rest[key];
31 | } else {
32 | state[key] = rest[key];
33 | }
34 | });
35 |
36 | const checkReducersStatus = name => {
37 | const keys = Object.keys(reducers);
38 | for (let i = 0; i < keys.length; i++) {
39 | if (service[keys[i]][name]) return true;
40 | }
41 | return false;
42 | };
43 |
44 | const handler = {
45 | set(target, prop, newValue) {
46 | if (disableProps.includes(prop) || isFunction(newValue)) {
47 | target[prop] = newValue;
48 | return true;
49 | }
50 | if (!checkReducersStatus('unlock')) {
51 | console.error(
52 | 'Do not modify data within components, call a method of service to update the data.',
53 | `namespace:${namespace}, prop:${prop}, value:${newValue}`
54 | );
55 | }
56 | if (target[prop] !== newValue) {
57 | isChanged = true;
58 | }
59 | delete target[prop];
60 | target[prop] = addProxy(newValue, handler);
61 | return true;
62 | },
63 | get(target, prop) {
64 | if (prop === '__isProxy__') return true;
65 | return target[prop];
66 | },
67 | };
68 | service = addProxy(state, handler);
69 |
70 | let timer = null;
71 | const checkUpdateAndBroadcast = () => {
72 | if (isChanged) {
73 | isChanged = false;
74 | if (timer) {
75 | clearTimeout(timer);
76 | timer = null;
77 | }
78 | timer = setTimeout(() => {
79 | timer = null;
80 | broadcast(namespace, Math.random());
81 | }, 0);
82 | }
83 | };
84 |
85 | Object.keys(reducers).forEach(key => {
86 | service[key] = (...args) => {
87 | service[key].unlock = true;
88 | const promise = reducers[key].apply(service, args);
89 | if (!isPromise(promise)) {
90 | service[key].unlock = false;
91 | checkUpdateAndBroadcast();
92 | return promise;
93 | }
94 | isChanged = true;
95 | service[key].loading = true;
96 | service[key].unlock = true;
97 | checkUpdateAndBroadcast();
98 | return new Promise((resolve, reject) => {
99 | promise
100 | .then(resolve)
101 | .catch(reject)
102 | .finally(() => {
103 | isChanged = true;
104 | service[key].loading = false;
105 | service[key].unlock = false;
106 | checkUpdateAndBroadcast();
107 | });
108 | });
109 | };
110 | service[key].loading = false;
111 | service[key].unlock = false;
112 | });
113 |
114 | Object.defineProperty(service, 'loading', {
115 | get() {
116 | return checkReducersStatus('loading');
117 | },
118 | });
119 |
120 | Object.assign(service, {
121 | stores,
122 | useStore: () => useStore()[namespace],
123 | });
124 |
125 | stores[namespace] = service;
126 | return service;
127 | };
128 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import store from './store';
2 | import createStore from './createStore';
3 | import useStore from './useStore';
4 |
5 | export { useStore, createStore };
6 | export default store;
7 |
--------------------------------------------------------------------------------
/src/pub.js:
--------------------------------------------------------------------------------
1 | const queue = {};
2 | export const broadcast = (name, state) => {
3 | if (!queue[name]) return;
4 | queue[name].forEach(fn => fn(state));
5 | };
6 | export const subScribe = (name, cb) => {
7 | if (!queue[name]) queue[name] = [];
8 | queue[name].push(cb);
9 | };
10 | export const unSubScribe = (name, cb) => {
11 | if (!queue[name]) return;
12 | const index = queue[name].indexOf(cb);
13 | if (index !== -1) queue[name].splice(index, 1);
14 | };
15 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { addProxy } from './util';
2 | import stores from './stores';
3 | import useReactHooks from './useReactHooks';
4 |
5 | export default addProxy(
6 | {},
7 | {
8 | get(target, key) {
9 | if (!stores[key]) throw new Error(`Not found the store: ${key}.`);
10 | useReactHooks(key);
11 | return stores[key];
12 | },
13 | }
14 | );
15 |
--------------------------------------------------------------------------------
/src/stores.js:
--------------------------------------------------------------------------------
1 | export default {};
2 |
--------------------------------------------------------------------------------
/src/useReactHooks.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { subScribe, unSubScribe } from './pub';
3 |
4 | export default name => {
5 | const [, setState] = useState();
6 | useEffect(() => {
7 | subScribe(name, setState);
8 | return () => unSubScribe(name, setState);
9 | }, []);
10 | };
11 |
--------------------------------------------------------------------------------
/src/useStore.js:
--------------------------------------------------------------------------------
1 | import store from './store';
2 |
3 | export default () => store;
4 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | export const isFunction = fn => typeof fn === 'function';
2 | export const isUndefined = prop => typeof prop === 'undefined';
3 | export const isObject = o => Object.prototype.toString.call(o) === '[object Object]';
4 | export const isArray = o => Array.isArray(o);
5 | export const isPromise = fn => {
6 | if (fn instanceof Promise) return true;
7 | return isObject(fn) && isFunction(fn.then);
8 | };
9 | export const addProxy = (o, handler) => {
10 | if (isArray(o)) {
11 | o.forEach((item, index) => {
12 | if (isObject(item)) {
13 | o[index] = addProxy(item, handler);
14 | }
15 | });
16 | } else if (isObject(o)) {
17 | Object.keys(o).forEach(key => {
18 | if (isObject(o[key])) {
19 | o[key] = addProxy(o[key], handler);
20 | }
21 | });
22 | } else {
23 | return o;
24 | }
25 | // eslint-disable-next-line
26 | if (o && o.__isProxy__) return o;
27 | return new Proxy(o, handler);
28 | };
29 |
--------------------------------------------------------------------------------
/test/__snapshots__/createStore.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`#createStore store.useStore() 1`] = `
4 | "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
5 | 1. You might have mismatching versions of React and the renderer (such as React DOM)
6 | 2. You might be breaking the Rules of Hooks
7 | 3. You might have more than one copy of React in the same app
8 | See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem."
9 | `;
10 |
--------------------------------------------------------------------------------
/test/createStore.test.js:
--------------------------------------------------------------------------------
1 | import { createStore } from '../src/index';
2 |
3 | describe('#createStore', () => {
4 | test('none namespace', () => {
5 | const createStoreNoNs = () => {
6 | createStore({
7 | name: 'test',
8 | });
9 | };
10 | expect(createStoreNoNs).toThrow('Invalid params, namespace is required.');
11 | });
12 |
13 | test('now allow loading', () => {
14 | const createStoreNoNs = () => {
15 | createStore({
16 | namespace: 'testStore',
17 | loading: false,
18 | });
19 | };
20 | expect(createStoreNoNs).toThrow('loading is not allowd in params.');
21 | });
22 |
23 | test('now allow stores', () => {
24 | const createStoreNoNs = () => {
25 | createStore({
26 | namespace: 'testStore',
27 | stores: {},
28 | });
29 | };
30 | expect(createStoreNoNs).toThrow('stores is not allowd in params.');
31 | });
32 |
33 | test('now allow useStore', () => {
34 | const createStoreNoNs = () => {
35 | createStore({
36 | namespace: 'testStore',
37 | useStore: () => {},
38 | });
39 | };
40 | expect(createStoreNoNs).toThrow('useStore is not allowd in params.');
41 | });
42 |
43 | test('now allow modify update', () => {
44 | const createStoreNoNs = () => {
45 | return createStore({
46 | namespace: 'testStore',
47 | name: 'test',
48 | });
49 | };
50 | const store = createStoreNoNs();
51 |
52 | const setName = () => {
53 | store.name = 'hello';
54 | return 'hello';
55 | };
56 | expect(setName()).toEqual('hello');
57 | });
58 |
59 | test('recreating store', () => {
60 | const store = createStore({
61 | namespace: 'testStore111',
62 | name: 'test',
63 | });
64 | const store1 = createStore({
65 | namespace: 'testStore111',
66 | name: 'test1',
67 | });
68 | expect(store).toEqual(store1);
69 | });
70 |
71 | test('store.useStore()', () => {
72 | const store = createStore({
73 | namespace: 'teststore222',
74 | name: 'test',
75 | });
76 | const tryUseStore = () => {
77 | store.useStore();
78 | };
79 |
80 | expect(tryUseStore).toThrowErrorMatchingSnapshot();
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/store.test.js:
--------------------------------------------------------------------------------
1 | import store from '../src/store';
2 |
3 | describe('#store', () => {
4 | test('not exists store', () => {
5 | const createStoreNoNs = () => {
6 | return store.notExistsStore;
7 | };
8 | expect(createStoreNoNs).toThrow('Not found the store: notExistsStore.');
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/test/todo.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { createStore, useStore } from '../src/index';
3 |
4 | const sleep = async t => new Promise(resolve => setTimeout(resolve, t));
5 |
6 | // eslint-disable-next-line
7 | const todoStore = createStore({
8 | namespace: 'TodoStore',
9 | id: 0,
10 | testNull: null,
11 | todos: [
12 | {
13 | id: 0,
14 | content: 'first',
15 | status: 'DOING',
16 | },
17 | ],
18 | addTodo(content) {
19 | this.id++;
20 | const todo = {
21 | id: this.id,
22 | content,
23 | status: 'DOING',
24 | };
25 | this.todos.push(todo);
26 | },
27 | updateNull() {
28 | this.testNull = { name: 'testname' };
29 | },
30 | getTodoById(id) {
31 | return this.todos.filter(item => item.id === id)[0];
32 | },
33 | updateTodo(id, status) {
34 | const todo = this.getTodoById(id);
35 | if (!todo) return;
36 | todo.status = status;
37 | },
38 | // test async function
39 | incId: 0,
40 | async delayIncId() {
41 | await sleep(1000 * 3);
42 | this.incId++;
43 | },
44 | });
45 |
46 | export default () => {
47 | /**
48 | * 获取 TodoStore 的几种方式:
49 | * const { TodoStore } = useStore(); // 更符合 React Hooks 的理念
50 | * const { TodoStore } = store;
51 | * const TodoStore = todoStore.useStore();
52 | */
53 | const { TodoStore } = useStore();
54 | const inputEl = useRef(null);
55 | const { loading } = TodoStore;
56 | const handleClick = item => {
57 | if (item.status === 'DOING') {
58 | TodoStore.updateTodo(item.id, 'COMPLETED');
59 | } else if (item.status === 'COMPLETED') {
60 | TodoStore.updateTodo(item.id, 'DOING');
61 | }
62 | };
63 | const handleAddTodo = () => {
64 | console.warn('set data within component, should be got console.error : ');
65 | TodoStore.todos[0].id = 1000;
66 | const text = inputEl.current.value;
67 | if (text) {
68 | TodoStore.addTodo(text);
69 | }
70 | };
71 | return (
72 |
73 |
74 | store loading:
75 | {loading}
76 |
77 |
{TodoStore.incId}
78 |
{TodoStore.testNull ? TodoStore.testNull.name : ''}
79 | {!TodoStore.delayIncId.loading ?
: ''}
80 |
81 |
{TodoStore.delayIncId.loading ? 'loading' : 'completed'}
82 |
{TodoStore.todos.length}
83 |
84 | {TodoStore.todos.map(item => {
85 | return (
86 | - handleClick(item)}>
87 | {item.content}
88 | {item.status}
89 |
90 | );
91 | })}
92 |
93 |
94 |
97 |
98 |
101 |
102 |
105 |
106 | );
107 | };
108 |
--------------------------------------------------------------------------------
/test/todo.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, fireEvent, waitForElement, cleanup } from 'react-testing-library';
3 | import '@babel/polyfill';
4 | // import { act } from 'react-dom/test-utils';
5 | import Todo from './todo';
6 |
7 | // const sleep = async t => new Promise(resolve => setTimeout(resolve, t));
8 |
9 | describe('#iostore', () => {
10 | afterEach(cleanup);
11 | test('#todo, click, add todo', async () => {
12 | const { getByTestId } = render();
13 |
14 | // first
15 | const todocount = getByTestId('todocount');
16 | expect(Number(todocount.textContent)).toEqual(1);
17 | console.log('todos count', todocount.textContent);
18 |
19 | const todolist = await waitForElement(() => getByTestId('todolist'));
20 | expect(todolist.innerHTML).toEqual('firstDOING');
21 | console.log('todolist html', todolist.innerHTML);
22 |
23 | // click
24 | console.log('====== click first todo start.');
25 | fireEvent.click(todolist.children[0]);
26 | // expect(todolist.innerHTML).toEqual('firstCOMPLETED');
27 | console.log('todolist html', todolist.innerHTML);
28 | fireEvent.click(todolist.children[0]);
29 | expect(todolist.innerHTML).toEqual('firstDOING');
30 | console.log('====== click first todo end.');
31 |
32 | // add todo
33 |
34 | const todoinput = getByTestId('todoinput');
35 | todoinput.value = 'second';
36 | fireEvent.click(todoinput);
37 | const todobtn = getByTestId('todobtn');
38 | fireEvent.click(todobtn);
39 | console.log('click add toto button.');
40 | expect(todolist.innerHTML).toEqual(
41 | 'firstDOINGsecondDOING'
42 | );
43 | console.log('todos count', todocount.textContent);
44 | console.log('todolist html', todolist.innerHTML);
45 | });
46 |
47 | test('#copytodo, store changed.', async () => {
48 | const { getByTestId } = render();
49 |
50 | // first
51 | const todocount = getByTestId('todocount');
52 | expect(Number(todocount.textContent)).toEqual(2);
53 | console.log('todos count', todocount.textContent);
54 |
55 | const todolist = await waitForElement(() => getByTestId('todolist'));
56 | expect(todolist.innerHTML).toEqual(
57 | 'firstDOINGsecondDOING'
58 | );
59 | console.log('todolist html', todolist.innerHTML);
60 | });
61 |
62 | test('#async inc id, test loading', async () => {
63 | const { getByTestId } = render();
64 |
65 | const loading = getByTestId('incidloading');
66 | // async incid
67 | expect(Number(getByTestId('incid').textContent)).toEqual(0);
68 | expect(loading.innerHTML).toEqual('completed');
69 | fireEvent.click(getByTestId('incbtn'));
70 | expect(loading.innerHTML).toEqual('loading');
71 | await waitForElement(() => getByTestId('incidfinish'));
72 | expect(loading.innerHTML).toEqual('completed');
73 | expect(Number(getByTestId('incid').textContent)).toEqual(1);
74 | console.log('loading', loading.innerHTML);
75 | console.log('incid', getByTestId('incid').textContent);
76 | });
77 |
78 | test('#update null', async () => {
79 | const { getByTestId } = render();
80 | const nullbtn = getByTestId('nullbtn');
81 | fireEvent.click(nullbtn);
82 | expect(getByTestId('testNull').innerHTML).toEqual('testname');
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/test/util.test.js:
--------------------------------------------------------------------------------
1 | import { isPromise, addProxy } from '../src/util';
2 | import '@babel/polyfill';
3 |
4 | describe('#util', () => {
5 | test('isPromise', () => {
6 | const sleep = async t =>
7 | new Promise(resolve => {
8 | setTimeout(resolve, t);
9 | });
10 | expect(isPromise(sleep(1))).toEqual(true);
11 |
12 | const fn = {};
13 | fn.then = () => {};
14 | expect(isPromise(fn)).toEqual(true);
15 | });
16 |
17 | test('addProxy', () => {
18 | const state = {
19 | state: {
20 | todos: [],
21 | info: {
22 | name: 'test',
23 | },
24 | },
25 | };
26 | const proxy = addProxy(state, {
27 | set(target, prop, newValue) {
28 | if (prop === 'name') {
29 | target[prop] = 'test';
30 | } else {
31 | target[prop] = newValue;
32 | }
33 | return true;
34 | },
35 | });
36 |
37 | proxy.state.info.name = 'test1';
38 | expect(proxy.state.info.name).toEqual('test');
39 | });
40 | });
41 |
--------------------------------------------------------------------------------