├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .size-snapshot.json ├── LICENSE ├── babel.config.js ├── eslintrc.back ├── package.json ├── readme.md ├── rollup.config.js ├── src └── index.ts ├── tests └── computed.test.tsx ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | Thumbs.db 5 | ehthumbs.db 6 | Desktop.ini 7 | $RECYCLE.BIN/ 8 | .DS_Store 9 | .vscode 10 | .docz/ 11 | package-lock.json 12 | coverage/ 13 | .idea 14 | .rpt2_cache/ 15 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn run lint-staged 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "index.js": { 3 | "bundled": 884, 4 | "minified": 437, 5 | "gzipped": 278, 6 | "treeshaked": { 7 | "rollup": { 8 | "code": 0, 9 | "import_statements": 0 10 | }, 11 | "webpack": { 12 | "code": 951 13 | } 14 | } 15 | }, 16 | "index.cjs.js": { 17 | "bundled": 968, 18 | "minified": 570, 19 | "gzipped": 302 20 | }, 21 | "index.iife.js": { 22 | "bundled": 1094, 23 | "minified": 446, 24 | "gzipped": 265 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Caleb Larsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, targets) => { 2 | // https://babeljs.io/docs/en/config-files#config-function-api 3 | const isTestEnv = api.env('test') 4 | 5 | return { 6 | babelrc: false, 7 | ignore: ['./node_modules'], 8 | presets: [ 9 | [ 10 | '@babel/preset-env', 11 | { 12 | loose: true, 13 | modules: isTestEnv ? 'commonjs' : false, 14 | targets: isTestEnv ? { node: 'current' } : targets, 15 | }, 16 | ], 17 | ], 18 | plugins: [ 19 | '@babel/plugin-transform-react-jsx', 20 | ['@babel/plugin-transform-typescript', { isTSX: true }], 21 | ], 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /eslintrc.back: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "shared-node-browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": [ 9 | "prettier", 10 | "prettier/react", 11 | "prettier/@typescript-eslint", 12 | "plugin:prettier/recommended", 13 | "plugin:react-hooks/recommended", 14 | "plugin:import/errors", 15 | "plugin:import/warnings" 16 | ], 17 | "plugins": ["@typescript-eslint", "react", "prettier", "react-hooks", "import", "jest"], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "ecmaVersion": 2018, 21 | "sourceType": "module", 22 | "ecmaFeatures": { 23 | "jsx": true 24 | }, 25 | "rules": { 26 | "curly": ["warn", "multi-line", "consistent"], 27 | "no-console": "off", 28 | "no-empty-pattern": "warn", 29 | "no-duplicate-imports": "error", 30 | "import/no-unresolved": ["error", { "commonjs": true, "amd": true }], 31 | "import/export": "error", 32 | "import/named": "off", 33 | "import/namespace": "off", 34 | "import/default": "off", 35 | "@typescript-eslint/explicit-module-boundary-types": "off", 36 | "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 37 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], 38 | "@typescript-eslint/no-use-before-define": "off", 39 | "@typescript-eslint/no-empty-function": "off", 40 | "@typescript-eslint/no-empty-interface": "off", 41 | "@typescript-eslint/no-explicit-any": "off", 42 | "jest/consistent-test-it": ["error", { "fn": "it", "withinDescribe": "it" }] 43 | } 44 | }, 45 | "settings": { 46 | "react": { 47 | "version": "detect" 48 | }, 49 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"], 50 | "import/parsers": { 51 | "@typescript-eslint/parser": [".js", ".jsx", ".ts", ".tsx"] 52 | }, 53 | "import/resolver": { 54 | "node": { 55 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 56 | "paths": ["src"] 57 | }, 58 | "alias": { 59 | "extensions": [".js", ".jsx", ".ts", ".tsx", ".json"], 60 | "map": [["zustand", "./src/index.ts"]] 61 | } 62 | } 63 | }, 64 | "overrides": [ 65 | { 66 | "files": ["src"], 67 | "parserOptions": { 68 | "project": "./tsconfig.json" 69 | } 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zustand-middleware-computed-state", 3 | "private": true, 4 | "version": "0.1.2", 5 | "description": "Computed state middleware for Zustand", 6 | "main": "index.cjs.js", 7 | "module": "index.js", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "**" 11 | ], 12 | "sideEffects": false, 13 | "scripts": { 14 | "prebuild": "rimraf dist", 15 | "build": "rollup -c --bundleConfigAsCjs", 16 | "postbuild": "yarn copy", 17 | "eslint": "eslint --fix 'src/**/*.{js,ts,jsx,tsx}'", 18 | "eslint-examples": "eslint --fix 'examples/src/**/*.{js,ts,jsx,tsx}'", 19 | "eslint:ci": "eslint '{src,examples/src}/**/*.{js,ts,jsx,tsx}'", 20 | "prepare": "yarn build", 21 | "postinstall": "husky install", 22 | "pretest": "tsc --noEmit", 23 | "test": "jest", 24 | "test:dev": "jest --watch --no-coverage", 25 | "test:coverage:watch": "jest --watch", 26 | "copy": "mv dist/src/* dist && rm -rf dist/{src,tests} && copyfiles -f package.json readme.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.husky=undefined; this.prettier=undefined; this.jest=undefined; this['lint-staged']=undefined;\"" 27 | }, 28 | "prettier": { 29 | "semi": false, 30 | "trailingComma": "es5", 31 | "singleQuote": true, 32 | "jsxBracketSameLine": true, 33 | "tabWidth": 2, 34 | "printWidth": 80 35 | }, 36 | "lint-staged": { 37 | "*.{js,ts,tsx}": [ 38 | "prettier --write" 39 | ] 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/cmlarsen/zustand-middleware-computed-state.git" 44 | }, 45 | "keywords": [ 46 | "react", 47 | "state", 48 | "manager", 49 | "management", 50 | "redux", 51 | "store", 52 | "zustand", 53 | "computed", 54 | "derived", 55 | "selector", 56 | "middleware" 57 | ], 58 | "author": "Caleb Larsen", 59 | "license": "MIT", 60 | "bugs": { 61 | "url": "https://github.com/cmlarsen/zustand-middleware-computed-state/issues" 62 | }, 63 | "homepage": "https://github.com/cmlarsen/zustand-middleware-computed-state", 64 | "jest": { 65 | "testRegex": "test.(js|ts|tsx)$", 66 | "coverageDirectory": "./coverage/", 67 | "collectCoverage": true, 68 | "coverageReporters": [ 69 | "json", 70 | "html", 71 | "text", 72 | "text-summary" 73 | ], 74 | "collectCoverageFrom": [ 75 | "src/**/*.{js,ts,tsx}", 76 | "tests/**/*.{js,ts,tsx}" 77 | ], 78 | "modulePathIgnorePatterns": [ 79 | "dist/" 80 | ] 81 | }, 82 | "devDependencies": { 83 | "@babel/core": "^7.20.12", 84 | "@babel/plugin-external-helpers": "^7.18.6", 85 | "@babel/plugin-transform-react-jsx": "^7.20.13", 86 | "@babel/plugin-transform-runtime": "^7.19.6", 87 | "@babel/plugin-transform-typescript": "^7.20.13", 88 | "@babel/preset-env": "^7.20.2", 89 | "@rollup/plugin-babel": "^6.0.3", 90 | "@rollup/plugin-node-resolve": "^15.0.1", 91 | "@rollup/plugin-typescript": "^11.0.0", 92 | "@testing-library/react": "^13.4.0", 93 | "@types/jest": "^29.4.0", 94 | "@types/react": "^18.0.27", 95 | "@types/react-dom": "^18.0.10", 96 | "@typescript-eslint/eslint-plugin": "^5.50.0", 97 | "@typescript-eslint/parser": "^5.50.0", 98 | "copyfiles": "^2.4.1", 99 | "eslint": "^8.33.0", 100 | "eslint-config-prettier": "^8.6.0", 101 | "eslint-import-resolver-alias": "^1.1.2", 102 | "eslint-plugin-import": "^2.27.5", 103 | "eslint-plugin-jest": "^27.2.1", 104 | "eslint-plugin-prettier": "^4.2.1", 105 | "eslint-plugin-react": "^7.32.2", 106 | "eslint-plugin-react-hooks": "^4.6.0", 107 | "husky": "^8.0.3", 108 | "jest": "^29.4.1", 109 | "json": "^11.0.0", 110 | "lint-staged": "^13.1.0", 111 | "prettier": "^2.8.3", 112 | "react": "^18.2.0", 113 | "react-dom": "^18.2.0", 114 | "rimraf": "^4.1.2", 115 | "rollup": "^3.12.0", 116 | "rollup-plugin-size-snapshot": "^0.12.0", 117 | "typescript": "^4.9.5", 118 | "zustand": "^4.3.2" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Zustand Computed State Middleware 2 | 3 | This is a dead simple middleware for adding computed state to state management library [Zustand](https://github.com/pmndrs/zustand). 4 | 5 | ``` 6 | npm install zustand-middleware-computed-state 7 | -- or -- 8 | yarn add zustand-middleware-computed-state 9 | ``` 10 | 11 | The computed values are defined as a function passed as the second argument passed into the `computed(store, computedStore)` middleware. The resulting values will be merged into the store and accessible just like any other bit of Zustand state. 12 | 13 | Since this computed state is updated on every state change, these should be kept light. 14 | 15 | ```javascript 16 | import create from 'zustand' 17 | import { computed } from 'zustand-middleware-computed-state' 18 | 19 | const useState = create(computed(store, computedStore)) 20 | ``` 21 | 22 | ## Example 23 | 24 | ```javascript 25 | import create from 'zustand' 26 | import { computed } from 'zustand-middleware-computed-state' 27 | 28 | const useStore = create( 29 | computed( 30 | (set) => ({ 31 | count: 0, 32 | inc: () => set((state) => ({ count: state.count + 1 })), 33 | }), 34 | (state) => { 35 | function isEnough() { 36 | if (state.count > 100) { 37 | return 'Is enough' 38 | } else { 39 | return 'Is not enough' 40 | } 41 | } 42 | 43 | return { 44 | computedCount: state.count + 10, 45 | isEnough: isEnough(), 46 | } 47 | } 48 | ) 49 | ) 50 | 51 | function Counter() { 52 | const { count, computedCount, isEnough, inc } = useStore() 53 | 54 | return ( 55 |