├── .eslintignore ├── .npmignore ├── demo ├── src │ ├── react-app-env.d.ts │ ├── modules │ │ ├── common.ts │ │ ├── project.ts │ │ ├── index.ts │ │ └── user.ts │ ├── setupTests.ts │ ├── index.tsx │ ├── App.test.tsx │ ├── index.css │ ├── components │ │ └── Project.tsx │ ├── App.css │ ├── App.tsx │ └── logo.svg ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── robots.txt │ ├── manifest.json │ └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── .gitignore ├── src ├── index.ts ├── helper.ts ├── mixin.ts ├── container.ts └── bootstrap.ts ├── .prettierrc.js ├── rollup.config.js ├── tsconfig.json ├── .eslintrc.js ├── LICENSE ├── package.json ├── index.d.ts ├── docs └── README-zh-cn.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | demo 3 | docs 4 | node_modules 5 | .eslintcache -------------------------------------------------------------------------------- /demo/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnfe/clean-state/HEAD/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnfe/clean-state/HEAD/demo/public/logo192.png -------------------------------------------------------------------------------- /demo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnfe/clean-state/HEAD/demo/public/logo512.png -------------------------------------------------------------------------------- /demo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npmrc 3 | 4 | lib 5 | .eslintcache 6 | 7 | package-lock.json 8 | yarn.lock -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import bootstrap from './bootstrap'; 2 | 3 | export { default as mixin } from './mixin'; 4 | export default bootstrap; 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jsxBracketSameLine: true, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | arrowParens: "always" 6 | }; 7 | -------------------------------------------------------------------------------- /demo/src/modules/common.ts: -------------------------------------------------------------------------------- 1 | const common = { 2 | reducers: { 3 | setValue({payload, state}: {payload: Record, state: State}): State { 4 | return {...state, ...payload} 5 | } 6 | } 7 | } 8 | 9 | export default common; -------------------------------------------------------------------------------- /demo/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | -------------------------------------------------------------------------------- /demo/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /demo/src/modules/project.ts: -------------------------------------------------------------------------------- 1 | const state = { 2 | count: 0 3 | } 4 | 5 | const user = { 6 | state, 7 | reducers: { 8 | increaseCount({payload, state}: any) { 9 | const {count} = state 10 | const {num} = payload 11 | return {...state, count: count + num} 12 | } 13 | }, 14 | } 15 | 16 | export default user; -------------------------------------------------------------------------------- /demo/src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import user from './user' 2 | import project from './project' 3 | import common from './common' 4 | import devtool from 'cs-redux-devtool' 5 | 6 | import bootstrap, {mixin} from 'clean-state' 7 | 8 | const modules = mixin(common, { user, project }) 9 | 10 | bootstrap.addPlugin(devtool) 11 | export const {useModule, dispatch} = bootstrap(modules); -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/modules/user.ts: -------------------------------------------------------------------------------- 1 | const state = { 2 | name: 'test' 3 | } 4 | 5 | const user = { 6 | state, 7 | reducers: { 8 | setName({payload, state}: any) { 9 | return {...state, ...payload} 10 | } 11 | }, 12 | effects: { 13 | async fetchNameAndSet({dispatch}: any) { 14 | const name = await Promise.resolve('fetch_name') 15 | dispatch.user.setName({name}) 16 | } 17 | } 18 | } 19 | 20 | export default user; -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache -------------------------------------------------------------------------------- /demo/src/components/Project.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, memo} from 'react' 2 | import {useModule, dispatch} from '../modules' 3 | 4 | const Project = memo(()=> { 5 | 6 | const {project} = useModule('project') 7 | const increase = useCallback(()=> { 8 | dispatch.project.increaseCount({num: 1}) 9 | }, []) 10 | 11 | console.log('project update') 12 | return
13 | project count: {project.count} 14 | 15 |
16 | }) 17 | 18 | export default Project -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | 4 | module.exports = { 5 | external: ['react', 'eventemitter3'], 6 | input: 'src/index.ts', 7 | plugins: [ 8 | typescript({ 9 | exclude: 'node_modules/**', 10 | typescript: require('typescript'), 11 | }), 12 | terser(), 13 | ], 14 | output: [ 15 | { 16 | format: 'umd', 17 | name: 'clean-state', 18 | file: 'lib/index.min.js', 19 | }, 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src", 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Split Each module state and method 3 | * @param {object} modules 4 | */ 5 | export function splitPropertyAndMethod(modules: Record) { 6 | const rootState = {}; 7 | const rootReducers = {}; 8 | const rootEffects = {}; 9 | 10 | Object.keys(modules).forEach((key) => { 11 | const module = modules[key]; 12 | 13 | rootState[key] = {}; 14 | rootReducers[key] = {}; 15 | rootEffects[key] = {}; 16 | 17 | Object.assign(rootState[key], module.state); 18 | Object.assign(rootReducers[key], module.reducers); 19 | Object.assign(rootEffects[key], module.effects); 20 | }); 21 | 22 | return { rootState, rootReducers, rootEffects }; 23 | } 24 | -------------------------------------------------------------------------------- /demo/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es3", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "pretty": true, 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "rootDir": "src", 12 | "sourceMap": false, 13 | "strict": true, 14 | "removeComments": true, 15 | "esModuleInterop": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noImplicitAny": false, 20 | "noFallthroughCasesInSwitch": true, 21 | "outDir": "lib", 22 | "lib": [ 23 | "es2018", 24 | "dom", 25 | "esnext" 26 | ], 27 | }, 28 | "include": ["src/**/*"], 29 | "exclude": [ 30 | "node_modules", 31 | "dom" 32 | ] 33 | } -------------------------------------------------------------------------------- /demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react' 2 | import { useModule, dispatch } from './modules' 3 | import Project from './components/Project' 4 | import './App.css'; 5 | 6 | function App() { 7 | const { user } = useModule('user') 8 | const onChange = useCallback((e)=> { 9 | const {target} = e 10 | dispatch.user.setValue({name: target.value}) 11 | }, []) 12 | 13 | const onClick = useCallback(()=> { 14 | dispatch.user.fetchNameAndSet() 15 | }, []) 16 | 17 | console.log('app update') 18 | return ( 19 |
20 |
21 |
22 | name: {user.name} 23 |
24 |
25 | 修改用户名: 26 |
27 | 28 |
29 |
30 | 31 |
32 | ); 33 | } 34 | 35 | export default App; -------------------------------------------------------------------------------- /src/mixin.ts: -------------------------------------------------------------------------------- 1 | import { Module, MixinModule } from '../index.d'; 2 | 3 | /** 4 | * Mix in common variables and methods for all modules 5 | * @param {object} common 6 | * @param {object} modules 7 | */ 8 | const mixin = >( 9 | common: C, 10 | modules: M, 11 | ): MixinModule => { 12 | const keys = Object.keys(modules); 13 | keys.forEach((key) => { 14 | const module = modules[key]; 15 | module.state = module.state || {}; 16 | module.reducers = module.reducers || {}; 17 | module.effects = module.effects || {}; 18 | // state mixin 19 | if (common.state) Object.assign(module.state, common.state); 20 | // reducer mixin 21 | if (common.reducers) Object.assign(module.reducers, common.reducers); 22 | // effects mixin 23 | if (common.effects) Object.assign(module.effects, common.effects); 24 | }); 25 | 26 | return modules as MixinModule; 27 | }; 28 | 29 | export default mixin; 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | parser: '@typescript-eslint/parser', 8 | extends: [ 9 | 'prettier/@typescript-eslint', 10 | 'plugin:prettier/recommended', 11 | 'plugin:react/recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | ], 14 | plugins: ['react', 'react-hooks', '@typescript-eslint'], 15 | settings: { 16 | react: { 17 | version: 'latest', 18 | }, 19 | }, 20 | rules: { 21 | 'react-hooks/rules-of-hooks': 2, // 检查 Hook 的规则 22 | 'react-hooks/exhaustive-deps': 1, // 检查 effect 的依赖 23 | 'react/prop-types': 1, 24 | 'no-unused-vars': 0, 25 | 'arrow-parens': 0, 26 | 'implicit-arrow-linebreak': 0, 27 | 'function-paren-newline': 0, 28 | '@typescript-eslint/no-unused-vars': 2, 29 | '@typescript-eslint/no-explicit-any': 0, 30 | '@typescript-eslint/no-var-requires': 0, 31 | '@typescript-eslint/explicit-module-boundary-types': 0, 32 | }, 33 | parserOptions: { 34 | ecmaFeatures: { 35 | jsx: true, 36 | }, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) clean-state authors. 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. -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@types/jest": "^26.0.15", 10 | "@types/node": "^12.0.0", 11 | "@types/react": "^16.9.53", 12 | "@types/react-dom": "^16.9.8", 13 | "clean-state": "^2.2.2", 14 | "react": "^17.0.1", 15 | "react-dom": "^17.0.1", 16 | "react-scripts": "4.0.1", 17 | "typescript": "^4.0.3", 18 | "web-vitals": "^0.2.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "cs-redux-devtool": "^1.1.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clean-state", 3 | "version": "2.2.4", 4 | "description": "", 5 | "main": "lib/index.min.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "build": "rollup -c", 9 | "dev": "tsc --watch", 10 | "lint": "eslint ./src --fix --ext js,ts,tsx" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/tnfe/clean-state" 15 | }, 16 | "author": "freezeYe", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@babel/core": "^7.12.9", 20 | "eventemitter3": "^4.0.7", 21 | "react": "^17.0.1" 22 | }, 23 | "keywords": [ 24 | "react", 25 | "hooks", 26 | "redux", 27 | "reducer", 28 | "flux", 29 | "mobx" 30 | ], 31 | "devDependencies": { 32 | "@typescript-eslint/eslint-plugin": "^4.7.0", 33 | "@typescript-eslint/parser": "^4.7.0", 34 | "eslint": "^7.12.1", 35 | "eslint-config-prettier": "^6.15.0", 36 | "eslint-loader": "^4.0.2", 37 | "eslint-plugin-prettier": "^3.1.4", 38 | "eslint-plugin-react": "^7.21.5", 39 | "eslint-plugin-react-hooks": "^4.2.0", 40 | "prettier": "^2.2.1", 41 | "rollup": "^2.38.0", 42 | "rollup-plugin-sourcemaps": "^0.6.3", 43 | "rollup-plugin-terser": "^7.0.2", 44 | "rollup-plugin-typescript": "^1.0.1", 45 | "typescript": "^4.1.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Module { 2 | state?: Record; 3 | reducers?: Record; 4 | effects?: Record; 5 | } 6 | 7 | export interface Bootstrap { 8 | (modules: Modules): { 9 | useModule: UseModule; 10 | dispatch: InnerDispatch; 11 | }; 12 | DISPATCH_TYPE: string; 13 | addPlugin: (plugin: any) => void; 14 | } 15 | 16 | export type NameSpaceDeclare = keyof Modules | (keyof Modules)[]; 17 | 18 | export type UseModule> = < 19 | NameSpace extends NameSpaceDeclare 20 | >( 21 | namespace: NameSpace, 22 | ) => NameSpace extends keyof Modules 23 | ? { [key in NameSpace]: Modules[key]['state'] } 24 | : { [key in NameSpace[number]]: Modules[key]['state'] }; 25 | 26 | export type RootEffects = { 27 | [key in keyof Modules]: Modules[key]['effects'] extends undefined 28 | ? Record 29 | : { 30 | [fnKey in keyof Modules[key]['effects']]: ( 31 | payload?: Parameters[0]['payload'], 32 | ) => any; 33 | }; 34 | }; 35 | 36 | export type RootReducers = { 37 | [key in keyof Modules]: Modules[key]['reducers'] extends undefined 38 | ? Record 39 | : { 40 | [fnKey in keyof Modules[key]['reducers']]: ( 41 | payload?: Parameters[0]['payload'], 42 | ) => any; 43 | }; 44 | }; 45 | 46 | export type Dispatch = ( 47 | namespace: string, 48 | payload?: Record, 49 | ) => any; 50 | 51 | export type InnerDispatch = Dispatch & 52 | RootEffects & 53 | RootReducers; 54 | 55 | export type MixinModule = { 56 | [key in keyof M]: { 57 | state: M[key]['state'] & C['state']; 58 | reducers: M[key]['reducers'] & C['reducers']; 59 | effects: M[key]['effects'] & C['effects']; 60 | }; 61 | }; 62 | 63 | export type EffectProps> = { 64 | payload: T; 65 | state: S; 66 | rootState: R; 67 | dispatch?: any; 68 | }; 69 | 70 | export type ReducerProps> = { 71 | payload: T; 72 | state: S; 73 | rootState: R; 74 | }; 75 | 76 | export type Plugin = (modules: any, on: any) => void; 77 | 78 | export const mixin: >( 79 | common: C, 80 | modules: M, 81 | ) => MixinModule; 82 | 83 | const bootstrap: Bootstrap; 84 | export default bootstrap; 85 | -------------------------------------------------------------------------------- /demo/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/container.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import { splitPropertyAndMethod } from './helper'; 3 | 4 | type Listener = () => void; 5 | 6 | /** 7 | * The container is one of the most important objects in FOX. 8 | * It manages the organization, distribution, 9 | * and synchronization of all module states to the UI layer 10 | */ 11 | class Container { 12 | private rootState: Record; 13 | private rootReducers: Record; 14 | private rootEffects: Record; 15 | private emitter: EventEmitter; 16 | 17 | constructor(modules) { 18 | const initialData = splitPropertyAndMethod(modules); 19 | 20 | this.rootState = initialData.rootState; 21 | this.rootReducers = initialData.rootReducers; 22 | this.rootEffects = initialData.rootEffects; 23 | 24 | this.emitter = new EventEmitter(); 25 | } 26 | 27 | public getRootEffects() { 28 | return this.rootEffects; 29 | } 30 | 31 | public getRootReducers() { 32 | return this.rootReducers; 33 | } 34 | 35 | public getRootState() { 36 | return this.rootState; 37 | } 38 | 39 | public getModule(namespace: string | string[]) { 40 | const combineModule = {}; 41 | 42 | const assign = (k: string) => { 43 | const module = { 44 | state: this.rootState[k], 45 | reducers: this.rootReducers[k], 46 | effects: this.rootEffects[k], 47 | }; 48 | 49 | Object.assign(combineModule, { [k]: module }); 50 | }; 51 | 52 | if (Array.isArray(namespace)) { 53 | namespace.forEach((k) => assign(k)); 54 | } else { 55 | assign(namespace); 56 | } 57 | return combineModule; 58 | } 59 | 60 | public getState(namespace: string | string[]) { 61 | const combineModule = this.getModule(namespace); 62 | 63 | return Object.keys(combineModule).reduce((result, key) => { 64 | result[key] = combineModule[key].state; 65 | return result; 66 | }, {}); 67 | } 68 | 69 | public setState(namespace: string, newState: Record): void { 70 | if (this.rootState[namespace]) { 71 | this.rootState[namespace] = newState; 72 | } 73 | 74 | this.trigger(namespace); 75 | } 76 | 77 | public addListener(namespace: string | string[], listener: Listener): void { 78 | if (Array.isArray(namespace)) { 79 | namespace.forEach((k) => this.emitter.on(k, listener)); 80 | } else { 81 | this.emitter.on(namespace, listener); 82 | } 83 | } 84 | 85 | public removeListener( 86 | namespace: string | string[], 87 | listener: Listener, 88 | ): void { 89 | if (Array.isArray(namespace)) { 90 | namespace.forEach((k) => this.emitter.removeListener(k, listener)); 91 | } else { 92 | this.emitter.removeListener(namespace, listener); 93 | } 94 | } 95 | 96 | public trigger(namespace: string): void { 97 | this.emitter.emit(namespace); 98 | } 99 | } 100 | 101 | export default Container; 102 | -------------------------------------------------------------------------------- /src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback, useState } from 'react'; 2 | import EventEmitter from 'eventemitter3'; 3 | import Container from './container'; 4 | import { Bootstrap, Plugin } from '../index.d'; 5 | 6 | const DISPATCH_TYPE = 'CS_DISPATCH_TYPE'; 7 | const plugins: Plugin[] = []; 8 | 9 | /** 10 | * This is CS's entry method, connecting each individual module into a whole. 11 | * It exposes the user to which useModule hooks are used to register the module's state, 12 | * and to which dispatches are used to invoke the module's methods and side effects 13 | * @param {object} modules 14 | */ 15 | const bootstrap: Bootstrap = (modules: Modules) => { 16 | const container = new Container(modules); 17 | const pluginEmitter = new EventEmitter(); 18 | 19 | // The only module method call that is exposed to the outside world 20 | const dispatch: any = ( 21 | nameAndMethod: string, 22 | payload: Record, 23 | ) => { 24 | const [namespace, methodName] = nameAndMethod.split('/'); 25 | const combineModule = container.getModule(namespace); 26 | 27 | const { state, reducers, effects } = combineModule[namespace]; 28 | const rootState = container.getRootState(); 29 | 30 | // The side effects take precedence over the reducer execution 31 | if (effects[methodName]) { 32 | pluginEmitter.emit(DISPATCH_TYPE, { 33 | type: nameAndMethod, 34 | payload, 35 | }); 36 | return effects[methodName]({ state, payload, rootState, dispatch }); 37 | } else if (reducers[methodName]) { 38 | const newState = reducers[methodName]({ 39 | state, 40 | rootState, 41 | payload, 42 | }); 43 | container.setState(namespace, newState); 44 | 45 | // Sync state to plugin 46 | pluginEmitter.emit(DISPATCH_TYPE, { 47 | type: nameAndMethod, 48 | payload, 49 | newState, 50 | }); 51 | } 52 | }; 53 | 54 | const injectFns = (reducersOrEffects) => { 55 | Object.keys(reducersOrEffects).forEach((key) => { 56 | if (!dispatch[key]) dispatch[key] = {}; 57 | const originFns = reducersOrEffects[key]; 58 | const fns = {}; 59 | Object.keys(originFns).forEach((fnKey) => { 60 | fns[fnKey] = (payload: Record) => 61 | dispatch(`${key}/${fnKey}`, payload); 62 | }); 63 | Object.assign(dispatch[key], fns); 64 | }); 65 | }; 66 | 67 | // This hook function exports the module state required by the user 68 | // and does the dependent binding of data to the view 69 | function useModule(namespace: string | string[]): any { 70 | const [, setState] = useState({}); 71 | 72 | const setStateProxy = useCallback(() => setState({}), [setState]); 73 | 74 | useEffect(() => { 75 | container.addListener(namespace, setStateProxy); 76 | return () => container.removeListener(namespace, setStateProxy); 77 | }, [namespace, setStateProxy]); 78 | 79 | return container.getState(namespace); 80 | } 81 | 82 | // Inject each module's reducer and effect method into the Dispatch 83 | const rootReducers = container.getRootReducers(); 84 | const rootEffects = container.getRootEffects(); 85 | 86 | injectFns(rootReducers); 87 | injectFns(rootEffects); 88 | 89 | plugins.forEach((plugin) => plugin(modules, pluginEmitter)); 90 | return { useModule: useModule as any, dispatch }; 91 | }; 92 | 93 | bootstrap.addPlugin = (plugin) => { 94 | plugins.push(plugin); 95 | }; 96 | bootstrap.DISPATCH_TYPE = DISPATCH_TYPE; 97 | 98 | export default bootstrap; 99 | -------------------------------------------------------------------------------- /docs/README-zh-cn.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | English | 4 | 中文 5 | 6 |

7 | 8 |

9 | 10 |

11 | 12 |
13 | Npm Version 14 | Package License 15 | Downloads 16 |
17 | 18 | ## 概览 19 | 🐻 clean-state是一款纯净小巧的状态管理神器。它放下了React所有的历史包袱,使用原生hooks来实现,并摆脱了Redux在状态更新时的无效渲染问题。在架构层面它会通过一个极其简单的api来自动组织。🍋如果你不是要制造一艘航空母舰又厌烦了复杂且难用的大型状态管理库,那么不妨来试试Clean-State。它小巧玲珑、性能极致完全可以满足你的需求。 20 | 21 | ## 特性 22 | 1. 使用原生hooks实现,对外部零依赖。 23 | 2. 架构简单,module 层粒度精细可测,划分清晰。 24 | 3. 性能优异,可做到模块级别的精确更新。 25 | 4. 原生支持副作用。 26 | 5. 极其小巧,仅仅200行代码。 27 | 6. 仅仅是react语法,零学习接入成本。 28 | 7. 对Typescript支持友好,可以自动推导模块类型。 29 | 8. 支持redux-tool调试工具。 30 | 9. 完美支持RN开发。 31 | 32 | ## 安装 33 | ```javascript 34 | npm i clean-state --save 35 | ``` 36 | 37 | ## 使用 38 | #### 1.定义一个模块 39 | ```javascript 40 | // modules/user.ts 41 | const state = { 42 | name: 'test' 43 | } 44 | 45 | const user = { 46 | state, 47 | reducers: { 48 | setName({payload, state}) { 49 | return {...state, ...payload} 50 | } 51 | }, 52 | effects: { 53 | async fetchNameAndSet({dispatch}) { 54 | const name = await Promise.resolve('fetch_name') 55 | dispatch.user.setName({name}) 56 | } 57 | } 58 | } 59 | 60 | export default user; 61 | ``` 62 | #### 2.注册模块 63 | ```javascript 64 | // modules/index.ts 65 | import user from './user' 66 | import bootstrap from 'clean-state' 67 | 68 | const modules = { user } 69 | export const {useModule, dispatch} = bootstrap(modules); 70 | ``` 71 | 72 | #### 3.使用模块 73 | ```javascript 74 | // page.ts 75 | import {useCallback} from 'react' 76 | import { useModule, dispatch } from './modules' 77 | 78 | function App() { 79 | /** 80 | * 这里你也能够传入数组同时返回多个模块状态 81 | * const {user, project} = useModule(['user', 'project']) 82 | */ 83 | const { user } = useModule('user') 84 | const onChange = useCallback((e)=> { 85 | const { target } = e 86 | dispatch.user.setName({name: target.value}) 87 | }, []) 88 | 89 | const onClick = useCallback(()=> { 90 | dispatch.user.fetchNameAndSet() 91 | }, []) 92 | 93 | return ( 94 |
95 |
96 |
97 | name: {user.name} 98 |
99 |
100 | 修改用户名: 101 |
102 | 103 |
104 |
105 | ); 106 | } 107 | 108 | export default App; 109 | ``` 110 | 111 | ## 混入 112 | 113 | 在很多情况下,多个模块之间会存在公共的state、reducers或者effects,这里我们为了防止用户在每个模块里做重复声明,对外暴露了混入的方法。 114 | 115 | ```javascript 116 | // common.ts 117 | const common = { 118 | reducers: { 119 | setValue({payload, state}: {payload: Record, state: State}): State { 120 | return {...state, ...payload} 121 | } 122 | } 123 | } 124 | export default common; 125 | 126 | // modules/index.ts 127 | import commont from './common' 128 | import user from './user' 129 | import { mixin } from 'clean-state'; 130 | 131 | // Mix Common's setValue method into the User module 132 | const modules = mixin(common, { user }) 133 | 134 | // You can now call the dispatch.user.setValue method on other pages 135 | export const {useModule, dispatch} = bootstrap(modules); 136 | 137 | ``` 138 | 139 | ## 模块 140 | ### `state` 141 | 模块状态,是一个属性对象。 142 | ``` 143 | { 144 | name: 'zhangsan', 145 | order: 1 146 | } 147 | ``` 148 | ### `reducers` 149 | 更改模块状态的处理函数集合,会返回最新的state。 150 | ``` 151 | { 152 | setValue({payload, state, rootState}) { 153 | return {...state, ...payload} 154 | } 155 | } 156 | ``` 157 | 158 | ### `effects` 159 | 模块的副作用方法集合,主要处理异步调用。 160 | ``` 161 | { 162 | async fetchAndSetValue({payload, state, rootState, dispatch}) { 163 | const newOrder = await fetch('xxx') 164 | dispatch.user.setValue({order: newOrder}) 165 | } 166 | } 167 | ``` 168 | 169 | ## 接口 170 | 171 | ### `bootstrap(modules)` 172 | | 参数 | 说明 | 类型 | 173 | | :----: | :----: | :----: | 174 | | modules | 注册的模块集合 | {string, Module} | 175 | 176 | ### `useModule(moduleName)` 177 | | 参数 | 说明 | 类型 | 178 | | :----: | :----: | :----: | 179 | | moduleName | 使用的模块名,返回对应状态 | string / string[] | 180 | 181 | ### `mixin(common, modules)` 182 | | 参数 | 说明 | 类型 | 183 | | :----: | :----: | :----: | 184 | | common | 需要注入的公共模块 | Module | 185 | | modules | 注册的模块集合 | Module | 186 | 187 | ### `dispatch.{moduleName}.{fnName}(payload)` 188 | | 参数 | 说明 | 类型 | 189 | | :----: | :----: | :----: | 190 | | moduleName | 调用的具体模块名,需在bootstrap中注册 | string | 191 | | fnName | 调用模块的方法名,reducer/effect | string | 192 | | payload | 传递的负载值 | object | 193 | 194 | ## 调试 195 | 你可以使用 [cs-redux-devtool](https://github.com/freezeYe/cs-redux-devtool) 来调试你的项目,追踪历史数据变化。 196 |

197 | 198 |

199 | 200 | 201 | ## 注意 202 | 203 | Dispatch调用优先级为 effects -> reducers,所以当一个模块下存在同名的reducer和effect时,只会执行effect。 204 | 205 | ## 问题 206 | 207 | 如果您对本库有更好的建议,或者遇到了任何使用上的问题,可以在这里记录: 208 | [https://github.com/tnfe/clean-state/issues](https://github.com/tnfe/clean-state/issues) 209 | 210 | ## 许可 211 | [MIT](./LICENSE) 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | English | 4 | 中文 5 | 6 |

7 | 8 |

9 | 10 |

11 | 12 |
13 | Npm Version 14 | Package License 15 | Downloads 16 |
17 | 18 | ## Overview 19 | 🐻 Clean-State is a neat, compact state management tool. It drops all of React's historical baggage, uses native hooks to implement it, and gets rid of Redux's problem of invalid rendering during status updates. At the architectural level it is automatically organized through a very simple API. 🍋 If you're not building an aircraft carrier and you're tired of having a large, complex and unwield-of-use State management library, try clean-state. It is small and exquisite, the performance of the extreme can meet your needs. 20 | 21 | ## Features 22 | 1. Using native hooks implementation, zero external dependencies. 23 | 2. The structure is simple, the module layer granularity is fine and measurable, and the division is clear. 24 | 3. Excellent performance, can do module level accurate update. 25 | 4. Native support side effects. 26 | 5. It's extremely small, just 200 lines of code. 27 | 6. Just React syntax, zero learning access cost. 28 | 7. TypeScript friendly and automatically deduces module types. 29 | 8. Support for Redux - Tool debugging tool. 30 | 9. Perfect support for RN. 31 | 32 | ## Installation 33 | ```javascript 34 | npm i clean-state --save 35 | ``` 36 | 37 | ## Usage 38 | #### 1.Define a module 39 | ```javascript 40 | // modules/user.ts 41 | const state = { 42 | name: 'test' 43 | } 44 | 45 | const user = { 46 | state, 47 | reducers: { 48 | setName({payload, state}) { 49 | return {...state, ...payload} 50 | } 51 | }, 52 | effects: { 53 | async fetchNameAndSet({dispatch}) { 54 | const name = await Promise.resolve('fetch_name') 55 | dispatch.user.setName({name}) 56 | } 57 | } 58 | } 59 | 60 | export default user; 61 | ``` 62 | 63 | #### 2.Registration module 64 | ```javascript 65 | // modules/index.ts 66 | import user from './user' 67 | import bootstrap from 'clean-state' 68 | 69 | const modules = { user } 70 | export const {useModule, dispatch} = bootstrap(modules); 71 | ``` 72 | 73 | #### 3.Use the module 74 | ```javascript 75 | // page.ts 76 | import {useCallback} from 'react' 77 | import { useModule, dispatch } from './modules' 78 | 79 | function App() { 80 | /** 81 | * Here you can also pass in an array and return multiple module states at the same time 82 | * const {user, project} = useModule(['user', 'project']) 83 | */ 84 | const { user } = useModule('user') 85 | const onChange = useCallback((e)=> { 86 | const { target } = e 87 | dispatch.user.setName({name: target.value}) 88 | }, []) 89 | 90 | const onClick = useCallback(()=> { 91 | dispatch.user.fetchNameAndSet() 92 | }, []) 93 | 94 | return ( 95 |
96 |
97 |
98 | name: {user.name} 99 |
100 |
101 | modify: 102 |
103 | 104 |
105 |
106 | ); 107 | } 108 | 109 | export default App; 110 | ``` 111 | 112 | ## Mixin 113 | 114 | In many cases, there are common states, reducers, or effects between multiple modules, and here we expose the methods to prevent users from making duplicate declarations in each module. 115 | 116 | ```javascript 117 | // common.ts 118 | const common = { 119 | reducers: { 120 | setValue({payload, state}: {payload: Record, state: State}): State { 121 | return {...state, ...payload} 122 | } 123 | } 124 | } 125 | export default common; 126 | 127 | // modules/index.ts 128 | import commont from './common' 129 | import user from './user' 130 | import { mixin } from 'clean-state'; 131 | 132 | // Mix Common's setValue method into the User module 133 | const modules = mixin(common, { user }) 134 | 135 | // You can now call the dispatch.user.setValue method on other pages 136 | export const {useModule, dispatch} = bootstrap(modules); 137 | 138 | ``` 139 | 140 | ## Module 141 | ### `state` 142 | Module state, which is a property object. 143 | ``` 144 | { 145 | name: 'zhangsan', 146 | order: 1 147 | } 148 | ``` 149 | ### `reducers` 150 | A collection of handlers that change the state of a module, returning the latest state. 151 | ``` 152 | { 153 | setValue({payload, state, rootState}) { 154 | return {...state, ...payload} 155 | } 156 | } 157 | ``` 158 | 159 | ### `effects` 160 | Module's collection of side effects methods that handle asynchronous calls. 161 | ``` 162 | { 163 | async fetchAndSetValue({payload, state, rootState, dispatch}) { 164 | const newOrder = await fetch('xxx') 165 | dispatch.user.setValue({order: newOrder}) 166 | } 167 | } 168 | ``` 169 | 170 | ## API 171 | 172 | ### `bootstrap(modules)` 173 | | Property | Description | Type | 174 | | :----: | :----: | :----: | 175 | | modules | A collection of registered modules | {string, Module} | 176 | 177 | ### `useModule(moduleName)` 178 | | Property | Description | Type | 179 | | :----: | :----: | :----: | 180 | | moduleName | The name of the module used returns the corresponding status | string / string[] | 181 | 182 | ### `mixin(common, modules)` 183 | | Property | Description | Type | 184 | | :----: | :----: | :----: | 185 | | common | Public modules that need to be injected | Module | 186 | | modules | A collection of registered modules | Module | 187 | 188 | ### `dispatch.{moduleName}.{fnName}(payload)` 189 | | Property | Description | Type | 190 | | :----: | :----: | :----: | 191 | | moduleName | The specific module name of the call should be registered in Bootstrap | string | 192 | | fnName | The method name of the call module, reducer/effect | string | 193 | | payload | The load value passed | object | 194 | 195 | ## Debugging 196 | You can use [cs-redux-devtool](https://github.com/freezeYe/cs-redux-devtool) to debug your project and track historical data changes. 197 |

198 | 199 |

200 | 201 | ## Notice 202 | 203 | Dispatch calls take precedence at effects-> reducers, so when there are reducers and effects with the same name under a module, only effects are executed. 204 | 205 | ## Issues 206 | 207 | If you have better suggestions for this library, or have any problems using it, you can write them here: [https://github.com/tnfe/clean-state/issues](https://github.com/tnfe/clean-state/issues) 208 | 209 | ## License 210 | [MIT](./LICENSE) 211 | --------------------------------------------------------------------------------