├── .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 | [![NPM version](https://img.shields.io/npm/v/iostore.svg?style=flat)](https://npmjs.org/package/iostore) 4 | [![build status](https://img.shields.io/travis/yisbug/iostore.svg?style=flat-square)](https://travis-ci.org/yisbug/iostore) 5 | [![Test coverage](https://img.shields.io/codecov/c/github/yisbug/iostore.svg?style=flat-square)](https://codecov.io/gh/yisbug/iostore) 6 | [![Known Vulnerabilities](https://snyk.io/test/npm/iostore/badge.svg)](https://snyk.io/test/npm/iostore) 7 | [![David deps](https://img.shields.io/david/yisbug/iostore.svg?style=flat-square)](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 | 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 | '
  • firstDOING
  • secondDOING
  • ' 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 | '
  • firstDOING
  • secondDOING
  • ' 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 | --------------------------------------------------------------------------------