├── .prettierignore ├── .babelrc ├── .eslintignore ├── commitlint.config.js ├── prettier.config.js ├── types ├── tslint.json ├── tsconfig.json ├── index.d.ts └── test.ts ├── test ├── nanoid-node.js ├── nanoid-web.js ├── nanoid-native.js └── index.js ├── .eslintrc ├── .travis.yml ├── src ├── nanoid.js └── index.js ├── LICENSE ├── package.json ├── .gitignore ├── .all-contributorsrc ├── code-of-conduct.md ├── assets └── logo.svg └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | coverage 3 | .nyc_output 4 | types 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | } 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | semi: false, 4 | trailingComma: "all", 5 | arrowParens: "always", 6 | } 7 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules": { 4 | "semicolon": [true, "always"], 5 | "indent": [true, "spaces", 2] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/nanoid-node.js: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | 3 | const expected = require("nanoid/non-secure") 4 | const imported = require("../src/nanoid") 5 | 6 | test("should load non-secure nanoid", (t) => { 7 | t.true(expected === imported) 8 | }) 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier", "@atomix/eslint-config"], 3 | "plugins": ["prettier"], 4 | "env": { 5 | "browser": true 6 | }, 7 | "rules": { 8 | "no-use-before-define": "off", 9 | "no-param-reassign": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/nanoid-web.js: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | 3 | global.document = {} 4 | global.crypto = {} 5 | 6 | const expected = require("nanoid") 7 | const imported = require("../src/nanoid") 8 | 9 | test("should load secure nanoid", (t) => { 10 | t.true(expected === imported) 11 | }) 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 8 5 | - 9 6 | - 10 7 | 8 | sudo: false 9 | 10 | script: 11 | - npm test 12 | # - npm i -g dtslint 13 | # - rm -rf node_modules 14 | # - npm run types 15 | 16 | after_success: 17 | - npm install 18 | - npm run report 19 | -------------------------------------------------------------------------------- /test/nanoid-native.js: -------------------------------------------------------------------------------- 1 | import test from "ava" 2 | 3 | global.navigator = { 4 | product: "ReactNative", 5 | } 6 | 7 | const expected = require("nanoid/non-secure") 8 | const imported = require("../src/nanoid") 9 | 10 | test("should load non-secure nanoid", (t) => { 11 | t.true(expected === imported) 12 | }) 13 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "noEmit": true, 10 | 11 | "baseUrl": ".", 12 | "paths": { "redux-symbiote": ["."] } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/nanoid.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require, no-restricted-globals */ 2 | let globalSelf = null 3 | 4 | if (typeof window !== "undefined") { 5 | globalSelf = window 6 | } else if (typeof global !== "undefined") { 7 | globalSelf = global 8 | } else if (typeof self !== "undefined") { 9 | globalSelf = self 10 | } 11 | 12 | const document = globalSelf && globalSelf.document 13 | const crypto = globalSelf && (globalSelf.crypto || globalSelf.msCrypto) 14 | const navigator = globalSelf && globalSelf.navigator 15 | 16 | if (document && crypto) { 17 | // web 18 | module.exports = require("nanoid") 19 | } else if (navigator && navigator.product === "ReactNative") { 20 | // react native 21 | module.exports = require("nanoid/non-secure") 22 | } else { 23 | // nodejs 24 | module.exports = require("nanoid/non-secure") 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Sergey Sova (https://sergeysova.com) 2 | Copyright (c) 2018 Viacheslav Bereza (http://betula.co) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 3.0 2 | 3 | export {}; 4 | 5 | export interface NamespaceOptions { 6 | namespace?: string; 7 | defaultReducer?: Reducer; 8 | separator?: string; 9 | } 10 | 11 | export type Symbiote = (state: State, ...payload: Arguments) => State; 12 | 13 | export type Symbiotes = { 14 | [Key in any]: Symbiote | Symbiotes; 15 | }; 16 | 17 | interface BasicAction { 18 | type: string | number | symbol; 19 | } 20 | 21 | export type Reducer = (state: State, action: BasicAction) => State; 22 | 23 | export type ActionCreator = TSymbiote extends Symbiote 24 | ? (...payload: Arguments) => Action 25 | : never; 26 | 27 | export type ActionsCreators> = { 28 | [Key in keyof TSymbiotes]: 29 | TSymbiotes[Key] extends Symbiote ? ActionCreator : 30 | TSymbiotes[Key] extends Symbiotes ? ActionsCreators : 31 | never 32 | }; 33 | 34 | export interface Action { 35 | type: string; 36 | payload: Payload[0]; 37 | "symbiote-payload": Payload; 38 | } 39 | 40 | export function createSymbiote>( 41 | initialState: State, 42 | actionsConfig: TSymbiotes, 43 | namespaceOptions?: string | NamespaceOptions, 44 | ): { 45 | actions: ActionsCreators 46 | reducer: Reducer 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-symbiote", 3 | "version": "3.4.0", 4 | "description": "Write your actions and reducers without pain", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "npm run test:lint && npm run test:code", 8 | "test:lint": "eslint .", 9 | "test:code": "nyc ava", 10 | "--types": "rm -rf node_modules && npm i -g dtslint && dtslint types", 11 | "types": "dtslint types", 12 | "build": "babel ./src -d ./lib", 13 | "coverage": "NODE_ENV=test nyc report --reporter=text-lcov", 14 | "report": "nyc report --reporter=text-lcov | coveralls", 15 | "prepublish": "npm run build" 16 | }, 17 | "types": "types", 18 | "files": [ 19 | "lib", 20 | "src", 21 | "types" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/sergeysova/redux-symbiote.git" 26 | }, 27 | "contributors": [ 28 | "Sergey Sova (https://sergeysova.com)", 29 | "Viacheslav Bereza (http://betula.co)" 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/sergeysova/redux-symbiote/issues" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "pre-commit": "lint-staged", 38 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 39 | "pre-push": "npm run test:lint" 40 | } 41 | }, 42 | "lint-staged": { 43 | "*.js": [ 44 | "eslint --fix", 45 | "prettier --write", 46 | "git add" 47 | ], 48 | "*.{json,eslintrc}": [ 49 | "prettier --write", 50 | "git add" 51 | ] 52 | }, 53 | "keywords": [ 54 | "redux", 55 | "flux", 56 | "fsa", 57 | "actions", 58 | "react", 59 | "act", 60 | "reducer" 61 | ], 62 | "homepage": "https://github.com/sergeysova/redux-symbiote#readme", 63 | "dependencies": { 64 | "nanoid": "^2.0.1", 65 | "symbiote-symbol": "^3.0.0" 66 | }, 67 | "devDependencies": { 68 | "@atomix/eslint-config": "^7.0.0-next.1", 69 | "@babel/cli": "^7.2.3", 70 | "@babel/core": "^7.3.4", 71 | "@babel/preset-env": "^7.3.4", 72 | "@commitlint/cli": "^8.3.1", 73 | "@commitlint/config-conventional": "^7.5.0", 74 | "ava": "^1.2.1", 75 | "commitizen": "^4.0.3", 76 | "coveralls": "^3.0.3", 77 | "cz-conventional-changelog": "^2.1.0", 78 | "dtslint": "^0.5.3", 79 | "eslint": "^5.15.1", 80 | "eslint-plugin-import": "^2.16.0", 81 | "eslint-plugin-prettier": "^3.0.1", 82 | "eslint-plugin-unicorn": "^7.1.0", 83 | "husky": "^1.3.1", 84 | "lint-staged": "^8.1.5", 85 | "nyc": "^14.1.1", 86 | "prettier": "^1.16.4", 87 | "typescript": "^3.3.3333" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,windows,linux,macos 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### Node ### 47 | # Logs 48 | logs 49 | *.log 50 | npm-debug.log* 51 | yarn-debug.log* 52 | yarn-error.log* 53 | 54 | # Runtime data 55 | pids 56 | *.pid 57 | *.seed 58 | *.pid.lock 59 | 60 | # Directory for instrumented libs generated by jscoverage/JSCover 61 | lib-cov 62 | 63 | # Coverage directory used by tools like istanbul 64 | coverage 65 | 66 | # nyc test coverage 67 | .nyc_output 68 | 69 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 70 | .grunt 71 | 72 | # Bower dependency directory (https://bower.io/) 73 | bower_components 74 | 75 | # node-waf configuration 76 | .lock-wscript 77 | 78 | # Compiled binary addons (http://nodejs.org/api/addons.html) 79 | build/Release 80 | 81 | # Dependency directories 82 | node_modules/ 83 | jspm_packages/ 84 | 85 | # Typescript v1 declaration files 86 | typings/ 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional REPL history 95 | .node_repl_history 96 | 97 | # Output of 'npm pack' 98 | *.tgz 99 | 100 | # Yarn Integrity file 101 | .yarn-integrity 102 | 103 | # dotenv environment variables file 104 | .env 105 | 106 | 107 | ### Windows ### 108 | # Windows thumbnail cache files 109 | Thumbs.db 110 | ehthumbs.db 111 | ehthumbs_vista.db 112 | 113 | # Folder config file 114 | Desktop.ini 115 | 116 | # Recycle Bin used on file shares 117 | $RECYCLE.BIN/ 118 | 119 | # Windows Installer files 120 | *.cab 121 | *.msi 122 | *.msm 123 | *.msp 124 | 125 | # Windows shortcuts 126 | *.lnk 127 | 128 | 129 | # End of https://www.gitignore.io/api/node,windows,linux,macos 130 | lib/* 131 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "redux-symbiote", 3 | "projectOwner": "sergeysova", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "contributors": [ 12 | { 13 | "login": "sergeysova", 14 | "name": "Sergey Sova", 15 | "avatar_url": "https://avatars0.githubusercontent.com/u/5620073?v=4", 16 | "profile": "https://sergeysova.com", 17 | "contributions": [ 18 | "doc", 19 | "code", 20 | "example", 21 | "ideas", 22 | "test" 23 | ] 24 | }, 25 | { 26 | "login": "artalar", 27 | "name": "Arutyunyan Artyom", 28 | "avatar_url": "https://avatars0.githubusercontent.com/u/27290320?v=4", 29 | "profile": "https://t.me/artalar", 30 | "contributions": [ 31 | "review", 32 | "ideas", 33 | "bug", 34 | "code" 35 | ] 36 | }, 37 | { 38 | "login": "igorkamyshev", 39 | "name": "Igor Kamyshev", 40 | "avatar_url": "https://avatars3.githubusercontent.com/u/26767722?v=4", 41 | "profile": "https://kamyshev.me", 42 | "contributions": [ 43 | "platform", 44 | "test" 45 | ] 46 | }, 47 | { 48 | "login": "ilyaagarkov", 49 | "name": "Ilya", 50 | "avatar_url": "https://avatars2.githubusercontent.com/u/10822601?v=4", 51 | "profile": "https://github.com/ilyaagarkov", 52 | "contributions": [ 53 | "bug" 54 | ] 55 | }, 56 | { 57 | "login": "ivanov-v", 58 | "name": "Ivanov Vadim", 59 | "avatar_url": "https://avatars2.githubusercontent.com/u/13759065?v=4", 60 | "profile": "https://github.com/ivanov-v", 61 | "contributions": [ 62 | "doc" 63 | ] 64 | }, 65 | { 66 | "login": "antonkri97", 67 | "name": "Аnton Krivokhizhin", 68 | "avatar_url": "https://avatars0.githubusercontent.com/u/16399895?v=4", 69 | "profile": "https://github.com/antonkri97", 70 | "contributions": [ 71 | "platform", 72 | "infra" 73 | ] 74 | }, 75 | { 76 | "login": "betula", 77 | "name": "Viacheslav", 78 | "avatar_url": "https://avatars0.githubusercontent.com/u/421161?v=4", 79 | "profile": "http://betula.co", 80 | "contributions": [ 81 | "ideas", 82 | "review" 83 | ] 84 | }, 85 | { 86 | "login": "RusTorg", 87 | "name": "Dmitri Razin", 88 | "avatar_url": "https://avatars1.githubusercontent.com/u/1402016?v=4", 89 | "profile": "https://ootm.ru/", 90 | "contributions": [ 91 | "bug", 92 | "design" 93 | ] 94 | }, 95 | { 96 | "login": "Finesse", 97 | "name": "Surgie Finesse", 98 | "avatar_url": "https://avatars3.githubusercontent.com/u/9006227?v=4", 99 | "profile": "https://github.com/Finesse", 100 | "contributions": [ 101 | "code" 102 | ] 103 | } 104 | ], 105 | "contributorsPerLine": 7 106 | } 107 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mail@sergeysova.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const symbioteSymbols = require("symbiote-symbol") 2 | const nanoid = require("./nanoid") 3 | 4 | module.exports = { 5 | createSymbiote, 6 | } 7 | 8 | /** 9 | * @param {{}} initialState 10 | * @param {{}} actionsConfig 11 | * @param {defaultOptions | string} namespaceOptions 12 | * @returns {{ actions: {}, reducer: Function }} 13 | */ 14 | function createSymbiote( 15 | initialState, 16 | actionsConfig, 17 | namespaceOptions = nanoid(), 18 | ) { 19 | const builder = new SymbioteBuilder({ 20 | state: initialState, 21 | options: createOptions(namespaceOptions), 22 | }) 23 | 24 | return builder.createSymbioteFor(actionsConfig) 25 | } 26 | 27 | /** 28 | * @param {defaultOptions | string} options 29 | * @return {defaultOptions} 30 | */ 31 | function createOptions(options) { 32 | if (typeof options === "string") { 33 | return Object.assign({}, defaultOptions, { 34 | namespace: options, 35 | }) 36 | } 37 | return Object.assign({}, defaultOptions, options) 38 | } 39 | 40 | const defaultOptions = { 41 | /** @type {string} */ 42 | namespace: undefined, 43 | /** @type {Function} */ 44 | defaultReducer: undefined, 45 | /** @type {string} */ 46 | separator: "/", 47 | } 48 | 49 | class SymbioteBuilder { 50 | constructor({ state, options }) { 51 | this.initialReducerState = state 52 | this.options = options 53 | this.actions = {} 54 | this.reducers = {} 55 | this.namespacePath = options.namespace ? [options.namespace] : [] 56 | } 57 | 58 | createSymbioteFor(actions) { 59 | const actionCreators = this.createActionsForScopeOfHandlers( 60 | actions, 61 | this.namespacePath, 62 | ) 63 | 64 | return { 65 | actions: actionCreators, 66 | reducer: this.createReducer(), 67 | } 68 | } 69 | 70 | createActionsForScopeOfHandlers(reducersMap, parentPath) { 71 | const actionsMap = {} 72 | 73 | Object.keys(reducersMap).forEach((key) => { 74 | const currentPath = createPathFor(parentPath, key) 75 | const currentHandlerOrScope = reducersMap[key] 76 | const currentType = this.createTypeFromPath(currentPath) 77 | 78 | if (isHandler(currentHandlerOrScope)) { 79 | const currentHandler = currentHandlerOrScope 80 | 81 | actionsMap[key] = makeActionCreatorFor(currentType, currentHandler) 82 | this.saveHandlerAsReducerFor(currentType, currentHandler) 83 | } else if (isScope(currentHandlerOrScope)) { 84 | actionsMap[key] = this.createActionsForScopeOfHandlers( 85 | currentHandlerOrScope, 86 | currentPath, 87 | ) 88 | } else { 89 | throw new TypeError( 90 | "createSymbiote supports only function handlers and object scopes in actions config", 91 | ) 92 | } 93 | }) 94 | 95 | return actionsMap 96 | } 97 | 98 | createTypeFromPath(path) { 99 | return path.join(this.options.separator) 100 | } 101 | 102 | saveHandlerAsReducerFor(type, handler) { 103 | this.reducers[type] = handler 104 | } 105 | 106 | createReducer() { 107 | return (previousState = this.initialReducerState, action) => { 108 | if (!action) throw new TypeError("Action should be passed") 109 | const reducer = this.findReducerFor(action.type) 110 | 111 | if (reducer) { 112 | return reducer(previousState, action) 113 | } 114 | 115 | return previousState 116 | } 117 | } 118 | 119 | findReducerFor(type) { 120 | const expectedReducer = this.reducers[type] 121 | 122 | if (expectedReducer) { 123 | return (state, { "symbiote-payload": payload = [] }) => 124 | expectedReducer(state, ...payload) 125 | } 126 | 127 | return this.options.defaultReducer 128 | } 129 | } 130 | 131 | function createPathFor(path, ...chunks) { 132 | return path.concat(...chunks) 133 | } 134 | 135 | function isHandler(handler) { 136 | return typeof handler === "function" 137 | } 138 | 139 | function isScope(scope) { 140 | return !Array.isArray(scope) && scope !== null && typeof scope === "object" 141 | } 142 | 143 | const createDefaultActionCreator = (type) => (...args) => ({ 144 | type, 145 | payload: args[0], 146 | "symbiote-payload": args, 147 | }) 148 | 149 | function makeActionCreatorFor(type, handler) { 150 | const createActionCreator = 151 | handler[symbioteSymbols.getActionCreator] || createDefaultActionCreator 152 | const actionCreator = createActionCreator(type) 153 | 154 | actionCreator.toString = () => type 155 | return actionCreator 156 | } 157 | -------------------------------------------------------------------------------- /types/test.ts: -------------------------------------------------------------------------------- 1 | import { Action, createSymbiote, Reducer } from "redux-symbiote"; 2 | 3 | // empty 4 | { 5 | // Works correct with empty types 6 | 7 | const { actions, reducer } = createSymbiote({}, {}); 8 | 9 | // dtslint doesn't support duck typing so the following line emits an error 10 | // actions; // $ExpectType {} 11 | actions as {}; 12 | reducer as Reducer<{}>; 13 | } 14 | 15 | // plain symbiote 16 | interface PlainState { 17 | count: number; 18 | } 19 | 20 | interface PlainActionCreators { 21 | inc: () => Action; 22 | dec: () => Action; 23 | } 24 | 25 | { 26 | // Works correct with plain state and actions 27 | 28 | const initialState = { count: 0 }; 29 | const { actions, reducer } = createSymbiote( 30 | initialState, 31 | { 32 | inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), 33 | dec: (state: PlainState) => ({ ...state, count: state.count + 1 }), 34 | }, 35 | ); 36 | 37 | actions as PlainActionCreators; 38 | reducer as Reducer; 39 | 40 | actions.inc(); 41 | reducer(initialState, actions.dec()); 42 | reducer(initialState, { type: 'other' }); 43 | } 44 | { 45 | // Throw error if the initial state type doesn't match the symbiotes state type 46 | 47 | const symbiotes = { 48 | inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), 49 | dec: (state: PlainState) => ({ ...state, count: state.count + 1 }), 50 | }; 51 | 52 | createSymbiote({ count: "hello!" }, symbiotes); // $ExpectError 53 | } 54 | { 55 | // Throw error if a symbiote return state type doesn't match the initial state type 56 | 57 | const symbiotes = { 58 | inc: (state: PlainState) => ({ ...state, count: 'inc' }), 59 | dec: (state: PlainState) => ({ ...state, count: 'dec' }), 60 | }; 61 | 62 | createSymbiote({ count: 0 }, symbiotes); // $ExpectError 63 | } 64 | 65 | // symbiotes with arguments 66 | interface ArgumentedActionCreators { 67 | oneArg: (one: number) => Action<[number]>; 68 | oneOptionalArg: (one?: number) => Action<[number | undefined]>; 69 | manyArgs: (one: number, two: boolean, three: string) => Action<[number, boolean, string]>; 70 | } 71 | 72 | const argumentedSymbiotes = { 73 | oneArg: (state: PlainState, one: number) => state, 74 | oneOptionalArg: (state: PlainState, one?: number) => state, 75 | manyArgs: (state: PlainState, one: number, two: boolean, three: string) => state, 76 | }; 77 | 78 | { 79 | // Works correct with plain state and actions with argument 80 | 81 | const { actions, reducer } = createSymbiote( 82 | { count: 0 }, 83 | argumentedSymbiotes, 84 | ); 85 | 86 | actions as ArgumentedActionCreators; 87 | reducer as Reducer; 88 | 89 | actions.oneArg(1); 90 | actions.oneOptionalArg(); 91 | actions.oneOptionalArg(1); 92 | actions.manyArgs(1, true, 'str'); 93 | } 94 | { 95 | // Throw error if an action payload has an incorrect type 96 | 97 | const { actions } = createSymbiote({ count: 0 }, argumentedSymbiotes); 98 | 99 | actions.oneArg(); // $ExpectError 100 | actions.oneArg('wrong'); // $ExpectError 101 | actions.oneArg(1, 'excess'); // $ExpectError 102 | actions.manyArgs(1, true); // $ExpectError 103 | actions.manyArgs('wrong', 'wrong', true); // $ExpectError 104 | actions.manyArgs(1, true, 'str', 'excess'); // $ExpectError 105 | } 106 | 107 | // nested symbiote 108 | interface NestedState { 109 | counter: { 110 | count: number 111 | }; 112 | } 113 | 114 | interface NestedActionCreators { 115 | counter: { 116 | inc: (amount: number) => Action<[number]> 117 | }; 118 | } 119 | 120 | { 121 | // Works correct with nested state and actions 122 | 123 | const { actions, reducer } = createSymbiote( 124 | { counter: { count: 0 } }, 125 | { 126 | counter: { 127 | inc: (state: NestedState, amount: number) => ({ 128 | ...state, 129 | counter: { ...state.counter, count: state.counter.count + amount }, 130 | }), 131 | }, 132 | }, 133 | ); 134 | 135 | actions as NestedActionCreators; 136 | reducer as Reducer; 137 | 138 | actions.counter.inc(1); 139 | } 140 | { 141 | // Throws error if nested state have an incorrect type 142 | 143 | const symbiote = { 144 | counter: { 145 | inc: (state: NestedState) => ({ 146 | ...state, 147 | counter: { ...state.counter, count: state.counter.count + 1 }, 148 | }), 149 | }, 150 | }; 151 | 152 | const { actions, reducer } = createSymbiote({ counter: { cnt: 0 } }, symbiote); // $ExpectError 153 | 154 | actions as NestedActionCreators; 155 | reducer as Reducer; 156 | } 157 | { 158 | // Throws error if nested action have an incorrect type 159 | 160 | const symbiote = { 161 | counter: { 162 | inc: (state: NestedState) => ({ 163 | ...state, 164 | counter: { ...state.counter, count: "newString" }, 165 | }), 166 | }, 167 | }; 168 | 169 | const { actions, reducer } = createSymbiote({ counter: { count: 0 } }, symbiote); // $ExpectError 170 | 171 | actions as NestedActionCreators; 172 | reducer as Reducer; 173 | } 174 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 196 | 197 | 210 | 211 | 212 | 213 | 215 | 217 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-symbiote [![Build Status](https://travis-ci.org/sergeysova/redux-symbiote.svg?branch=master)](https://travis-ci.org/sergeysova/redux-symbiote) [![Coverage Status](https://coveralls.io/repos/github/sergeysova/redux-symbiote/badge.svg?branch=master)](https://coveralls.io/github/sergeysova/redux-symbiote?branch=master) [![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg)](#contributors) 2 | 3 | 4 | 5 | 6 | 7 | Write your actions and reducers without pain 8 | 9 | ## Usage 10 | 11 | ```js 12 | import { createSymbiote } from 'redux-symbiote' 13 | 14 | 15 | const initialState = { 16 | error: null, 17 | accounts: [], 18 | loading: false, 19 | } 20 | 21 | const symbiotes = { 22 | accounts: { 23 | loading: { 24 | start: (state) => ({ ...state, loading: true }), 25 | failed: (state, error) => ({ ...state, loading: false, error }), 26 | finish: (state, accounts) => ({ ...state, loading: false, accounts }), 27 | }, 28 | }, 29 | } 30 | 31 | export const { actions, reducer } = createSymbiote(initialState, symbiotes) 32 | ``` 33 | 34 | Also you can use CommonJS: 35 | 36 | ```js 37 | const { createSymbiote } = require('redux-symbiote') 38 | 39 | // ... 40 | ``` 41 | 42 | ## Demo 43 | 44 | [![Edit Redux Symbiote Todos](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/3x7n34n336?module=%2Fsrc%2Fstore%2Fsymbiotes%2Ftodos.js) 45 | 46 | ## API 47 | 48 | ### Create symbiote 49 | 50 | ```js 51 | function createSymbiote( 52 | initialState, 53 | symbiotes, 54 | ?namespace = '' 55 | ) 56 | ``` 57 | 58 | ### Create action handlers + reducer 59 | 60 | ```js 61 | createSymbiote(initialState, { 62 | actionType: actionReducer, 63 | nestedType: { 64 | actionType: nestedActionReducer, 65 | } 66 | }) 67 | ``` 68 | 69 | Example: 70 | 71 | ```js 72 | const initialState = { value: 1, data: 'another' } 73 | 74 | const symbiotes = { 75 | increment: (state) => ({ ...state, value: state.value + 1 }), 76 | decrement: (state) => ({ ...state, value: state.value - 1 }), 77 | setValue: (state, value) => ({ ...state, value }), 78 | setData: (state, data) => ({ ...state, data }), 79 | concatData: (state, data) => ({ ...state, data: data + state.data }), 80 | } 81 | 82 | export const { actions, reducer } = createSymbiote(initialState, symbiotes) 83 | 84 | dispatch(actions.increment()) // { type: 'increment' } 85 | dispatch(actions.setValue(4)) // { type: 'setValue', payload: [4] } 86 | dispatch(actions.decrement()) // { type: 'decrement' } 87 | dispatch(actions.setData('bar')) // { type: 'setData', payload: ['bar'] } 88 | dispatch(actions.concatData('foo ')) // { type: 'concatData', payload: ['foo '] } 89 | 90 | // State here { value: 3, data: 'foo bar' } 91 | ``` 92 | 93 | When you call `actions.setValue` symbiote calls your action handler with previousState and all arguments spreaded after state. 94 | 95 | #### Nested example 96 | 97 | ```js 98 | const initialState = { value: 1, data: 'another' } 99 | 100 | const symbiotes = { 101 | value: { 102 | increment: (state) => ({ ...state, value: state.value + 1 }), 103 | decrement: (state) => ({ ...state, value: state.value - 1 }), 104 | }, 105 | data: { 106 | set: (state, data) => ({ ...state, data }), 107 | concat: (state, data) => ({ ...state, data: data + state.data }), 108 | }, 109 | } 110 | 111 | export const { actions, reducer } = createSymbiote(initialState, symbiotes) 112 | 113 | dispatch(actions.value.increment()) // { type: 'value/increment' } 114 | dispatch(actions.value.decrement()) // { type: 'value/decrement' } 115 | dispatch(actions.data.set('bar')) // { type: 'data/set', payload: ['bar'] } 116 | dispatch(actions.data.concat('foo ')) // { type: 'data/concat', payload: ['foo '] } 117 | ``` 118 | 119 | #### Options 120 | 121 | Third parameter in `createSymbiote` is optional `string` or `object`. 122 | 123 | If `string` passed, symbiote converts it to `{ namespace: 'string' }`. 124 | 125 | Object has optional properties: 126 | 127 | - `namespace` is `string` — set prefix for each action type 128 | - `defaultReducer` is `(previousState, action) -> newState` — called instead of return previous state 129 | - `separator` is `string` — change separator of nested action types (default `/`) 130 | 131 | #### ActionHandler##toString 132 | 133 | You can use action as action type in classic reducer or in [`handleAction(s)`](https://redux-actions.js.org/docs/api/handleAction.html) in [`redux-actions`](https://npmjs.com/redux-actions) 134 | 135 | ```js 136 | import { handleActions } from 'redux-actions' 137 | import { createSymbiote } from 'redux-symbiote' 138 | 139 | const initialState = { /* ... */ } 140 | 141 | const symbiotes = { 142 | foo: { 143 | bar: { 144 | baz: (state, arg1, arg2) => ({ ...state, data: arg1, atad: arg2 }), 145 | }, 146 | }, 147 | } 148 | 149 | const { actions } = createSymbiote(initialState, symbiotes) 150 | 151 | const reducer = handleActions({ 152 | [actions.foo.bar.baz]: (state, { payload: [arg1, arg2] }) => ({ 153 | ...state, 154 | data: arg1, 155 | atad: arg2, 156 | }), 157 | }, initialState) 158 | ``` 159 | 160 | ### How to use reducer 161 | 162 | `createSymbiote` returns object with `actions` and `reducer`. 163 | 164 | Created reducer already handles created actions. You don't need to handle actions from symbiote. 165 | 166 | ```js 167 | // accounts.js 168 | export const { actions, reducer } = createSymbiote(initialState, symbiotes, options) 169 | 170 | // reducer.js 171 | import { reducer as accounts } from '../accounts/symbiote' 172 | // another imports 173 | 174 | export const reducer = combineReducers({ 175 | accounts, 176 | // another reducers 177 | }) 178 | ``` 179 | 180 | ## Why? 181 | 182 | Redux recommends creating constants, action creators and reducers separately. 183 | 184 | https://redux.js.org/basics/ 185 | 186 | ```js 187 | const ACCOUNTS_LOADING_START = 'ACCOUNTS_LOADING_START' 188 | const ACCOUNTS_LOADING_FAILED = 'ACCOUNTS_LOADING_FAILED' 189 | const ACCOUNTS_LOADING_FINISH = 'ACCOUNTS_LOADING_FINISH' 190 | 191 | 192 | export function loadingStart() { 193 | return { 194 | type: ACCOUNTS_LOADING_START, 195 | } 196 | } 197 | 198 | export function loadingFailed(error) { 199 | return { 200 | type: ACCOUNTS_LOADING_FAILED, 201 | payload: { 202 | error, 203 | }, 204 | } 205 | } 206 | 207 | export function loadingFinish(accounts) { 208 | return { 209 | type: ACCOUNTS_LOADING_FINISH, 210 | payload: { 211 | accounts, 212 | }, 213 | } 214 | } 215 | 216 | const initialState = { 217 | error: null, 218 | accounts: [], 219 | loading: false, 220 | } 221 | 222 | export function accountsReducer(state = initialState, action) { 223 | switch (action.type) { 224 | case ACCOUNTS_LOADING_START: 225 | return Object.assign({}, state, { 226 | loading: true, 227 | }) 228 | 229 | case ACCOUNTS_LOADING_FAILED: 230 | return Object.assign({}, state, { 231 | loading: false, 232 | error: action.payload.error, 233 | }) 234 | 235 | case ACCOUNTS_LOADING_FINISH: 236 | return Object.assign({}, state, { 237 | loading: false, 238 | accounts: action.payload.accounts, 239 | }) 240 | } 241 | 242 | return state 243 | } 244 | ``` 245 | 246 | So much boilerplate. 247 | 248 | Let's look at [redux-actions](https://npmjs.com/redux-actions). 249 | 250 | ```js 251 | import { createActions, handleActions, combineActions } from 'redux-actions' 252 | 253 | 254 | export const actions = createActions({ 255 | accounts: { 256 | loading: { 257 | start: () => ({ loading: true }), 258 | failed: (error) => ({ loading: false, error }), 259 | finish: (accounts) => ({ loading: false, accounts }), 260 | }, 261 | }, 262 | }).accounts 263 | 264 | const initialState = { 265 | error: null, 266 | accounts: [], 267 | loading: false, 268 | } 269 | 270 | export const accountsReducer = handleActions({ 271 | [combineActions(actions.loading.start, actions.loading.failed, actions.loading.finish)]: 272 | (state, { payload: { loading } }) => ({ ...state, loading }), 273 | 274 | [actions.loading.failed]: (state, { payload: { error } }) => ({ ...state, error }), 275 | 276 | [actions.loading.finish]: (state, { payload: { accounts } }) => ({ ...state, accounts }), 277 | }, initialState) 278 | ``` 279 | 280 | But we have some duplicate in action creators properties and reducer. 281 | 282 | Let's rewrite it to redux-symbiote: 283 | 284 | 285 | ```js 286 | import { createSymbiote } from 'redux-symbiote' 287 | 288 | const initialState = { 289 | error: null, 290 | accounts: [], 291 | loading: false, 292 | } 293 | 294 | const symbiotes = { 295 | start: (state) => ({ ...state, loading: true }), 296 | finish: (state, { accounts }) => ({ ...state, loading: false, accounts }), 297 | failed: (state, { error }) => ({ ...state, loading: false, error }), 298 | } 299 | 300 | export const { actions, reducer: accountsReducer } = 301 | createSymbiote(initialState, symbiotes, 'accounts/loading') 302 | ``` 303 | 304 | That's all. `accounts/loading` is an optional namespace for actions types. 305 | 306 | To reduce noise around loading actions try [`symbiote-fetching`](https://npmjs.com/symbiote-fetching). 307 | 308 | ## Contributors 309 | 310 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 |
Sergey Sova
Sergey Sova

📖 💻 💡 🤔 ⚠️
Arutyunyan Artyom
Arutyunyan Artyom

👀 🤔 🐛 💻
Igor Kamyshev
Igor Kamyshev

📦 ⚠️
Ilya
Ilya

🐛
Ivanov Vadim
Ivanov Vadim

📖
Аnton Krivokhizhin
Аnton Krivokhizhin

📦 🚇
Viacheslav
Viacheslav

🤔 👀
Dmitri Razin
Dmitri Razin

🐛 🎨
Surgie Finesse
Surgie Finesse

💻
329 | 330 | 331 | 332 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 333 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-magic-numbers */ 2 | import test from "ava" 3 | import symbioteSymbol from "symbiote-symbol" 4 | import { createSymbiote } from "../src/index" 5 | 6 | test("createSymbiote return actions and reducer", (t) => { 7 | const result = createSymbiote({}, {}) 8 | 9 | t.deepEqual(result.actions, {}, "actions is not an object") 10 | t.is(typeof result.reducer, "function", "reducer is not a function") 11 | }) 12 | 13 | test("symbioteSymbol is map", (t) => { 14 | t.is(typeof symbioteSymbol, "object") 15 | t.deepEqual(Object.keys(symbioteSymbol), ["getActionCreator"]) 16 | }) 17 | 18 | test("createSymbiote throws on not function symbiotes", (t) => { 19 | t.throws(() => { 20 | createSymbiote({}, { foo: 1 }) 21 | }, /createSymbiote supports only function handlers/) 22 | t.throws(() => { 23 | createSymbiote({}, { foo: true }) 24 | }, /createSymbiote supports only function handlers/) 25 | t.throws(() => { 26 | createSymbiote({}, { foo: "foo" }) 27 | }, /createSymbiote supports only function handlers/) 28 | t.throws(() => { 29 | createSymbiote({}, { foo: Symbol("foo") }) 30 | }, /createSymbiote supports only function handlers/) 31 | t.throws(() => { 32 | createSymbiote({}, { foo: null }) 33 | }, /createSymbiote supports only function handlers/) 34 | }) 35 | 36 | test("reducer return previous state", (t) => { 37 | const exampleState = { foo: 1 } 38 | const { reducer } = createSymbiote({}, {}) 39 | 40 | t.is(reducer(exampleState, {}), exampleState) 41 | }) 42 | 43 | test("reducer throws if passed no action", (t) => { 44 | const { reducer } = createSymbiote({}, {}) 45 | 46 | t.throws(() => { 47 | reducer({ state: 1 }, undefined) 48 | }, /Action should be passed/) 49 | }) 50 | 51 | test("reducer return initial state", (t) => { 52 | const initialState = { foo: 1 } 53 | const { reducer } = createSymbiote(initialState, {}) 54 | 55 | t.is(reducer(undefined, {}), initialState) 56 | }) 57 | 58 | test("simple actions returns type and payload", (t) => { 59 | const { actions } = createSymbiote( 60 | {}, 61 | { 62 | foo: (arg) => ({ arg }), 63 | }, 64 | ) 65 | 66 | const action = actions.foo(1) 67 | 68 | t.is(action.payload, 1) 69 | t.deepEqual(action["symbiote-payload"], [1]) 70 | t.true(action.type.endsWith("foo")) 71 | }) 72 | 73 | test("actions with state returns type and payload", (t) => { 74 | const { actions } = createSymbiote( 75 | {}, 76 | { 77 | bar: (arg) => (state) => ({ arg, state }), 78 | }, 79 | ) 80 | 81 | const action = actions.bar(1) 82 | 83 | t.is(action.payload, 1) 84 | t.deepEqual(action["symbiote-payload"], [1]) 85 | t.true(action.type.endsWith("bar")) 86 | 87 | t.true( 88 | actions.bar.toString().endsWith("bar"), 89 | ".toString() return correct type", 90 | ) 91 | }) 92 | 93 | test("nested actions returns type and payload", (t) => { 94 | const { actions } = createSymbiote( 95 | {}, 96 | { 97 | bar: { 98 | foo: (arg) => ({ arg }), 99 | }, 100 | }, 101 | ) 102 | 103 | const action = actions.bar.foo(1) 104 | 105 | t.true(action.type.endsWith("bar/foo")) 106 | t.is(action.payload, 1) 107 | t.deepEqual(action["symbiote-payload"], [1]) 108 | 109 | t.true( 110 | actions.bar.foo.toString().endsWith("bar/foo"), 111 | ".toString() return correct type", 112 | ) 113 | }) 114 | 115 | test("nested actions with state returns type and payload", (t) => { 116 | const { actions } = createSymbiote( 117 | {}, 118 | { 119 | foo: { 120 | bar: (arg) => (state) => ({ arg, state }), 121 | }, 122 | }, 123 | ) 124 | 125 | const action = actions.foo.bar(1) 126 | 127 | t.true(action.type.endsWith("foo/bar")) 128 | t.is(action.payload, 1) 129 | t.deepEqual(action["symbiote-payload"], [1]) 130 | 131 | t.true( 132 | actions.foo.bar.toString().endsWith("foo/bar"), 133 | ".toString() return correct type", 134 | ) 135 | }) 136 | 137 | test("reducer return action resul", (t) => { 138 | const { actions, reducer } = createSymbiote( 139 | { value: 0, data: "foo" }, 140 | { 141 | foo: () => 100, 142 | }, 143 | ) 144 | 145 | t.deepEqual(reducer(undefined, actions.foo(1)), 100) 146 | }) 147 | 148 | test("createSymbiote with extended action creator", (t) => { 149 | const testValue = Math.random() 150 | const handler1 = () => {} 151 | const handler2 = () => {} 152 | 153 | handler1[symbioteSymbol.getActionCreator] = () => () => testValue 154 | handler2[symbioteSymbol.getActionCreator] = (type) => () => type 155 | 156 | const { actions } = createSymbiote({}, { handler1, handler2 }, "test") 157 | 158 | t.is(actions.handler1(), testValue) 159 | t.is(actions.handler2(), "test/handler2") 160 | }) 161 | 162 | test("action accepts state in first argument", (t) => { 163 | const initialState = Symbol("initial state") 164 | 165 | const { actions, reducer } = createSymbiote(initialState, { 166 | foo: (state) => state, 167 | }) 168 | 169 | t.is(reducer(undefined, actions.foo(1)), initialState) 170 | }) 171 | 172 | test("action accepts arguments in call", (t) => { 173 | const initialState = Symbol("initial state") 174 | const a1 = Symbol("a1") 175 | const a2 = Symbol("a2") 176 | const a3 = Symbol("a3") 177 | const a4 = Symbol("a4") 178 | const a5 = Symbol("a5") 179 | const a6 = Symbol("a6") 180 | 181 | const { actions, reducer } = createSymbiote(initialState, { 182 | foo: (state, a, b, c, d, e, f) => [a, b, c, d, e, f], 183 | }) 184 | 185 | t.deepEqual(reducer(undefined, actions.foo(a1, a2, a3, a4, a5, a6)), [ 186 | a1, 187 | a2, 188 | a3, 189 | a4, 190 | a5, 191 | a6, 192 | ]) 193 | }) 194 | 195 | test("reducer handle simple action and return result of action", (t) => { 196 | const { actions, reducer } = createSymbiote( 197 | { value: 0, data: "foo" }, 198 | { 199 | foo: (state, value) => ({ ...state, value }), 200 | }, 201 | ) 202 | 203 | t.deepEqual(reducer(undefined, actions.foo(1)), { value: 1, data: "foo" }) 204 | }) 205 | 206 | test("reducer not merges state under the hood", (t) => { 207 | const { actions, reducer } = createSymbiote( 208 | { a: 1, b: 2, c: 3 }, 209 | { 210 | foo: (state, b) => ({ b }), 211 | }, 212 | ) 213 | 214 | t.deepEqual(reducer(undefined, actions.foo(1)), { b: 1 }) 215 | }) 216 | 217 | test("reducer handle nested action and merges it", (t) => { 218 | const { actions, reducer } = createSymbiote( 219 | { value: 0, data: "foo" }, 220 | { 221 | bar: { 222 | foo: (state, value) => ({ ...state, value }), 223 | }, 224 | }, 225 | ) 226 | 227 | t.deepEqual(reducer(undefined, actions.bar.foo(1)), { 228 | value: 1, 229 | data: "foo", 230 | }) 231 | }) 232 | 233 | test("reducer handle simple action with state", (t) => { 234 | const { actions, reducer } = createSymbiote( 235 | { value: 0, data: "foo" }, 236 | { 237 | foo: (state, data) => ({ ...state, value: state.value + 1, data }), 238 | }, 239 | ) 240 | 241 | t.deepEqual(reducer(undefined, actions.foo("bar")), { 242 | value: 1, 243 | data: "bar", 244 | }) 245 | }) 246 | 247 | test("reducer handle nested action with state", (t) => { 248 | const { actions, reducer } = createSymbiote( 249 | { value: 0, data: "foo" }, 250 | { 251 | bar: { 252 | foo: (state, data) => ({ ...state, value: state.value + 1, data }), 253 | }, 254 | }, 255 | ) 256 | 257 | t.deepEqual(reducer(undefined, actions.bar.foo("bar")), { 258 | value: 1, 259 | data: "bar", 260 | }) 261 | }) 262 | 263 | test("prefix", (t) => { 264 | const { actions, reducer } = createSymbiote( 265 | { value: 0, data: "foo" }, 266 | { 267 | foo: (state, value) => ({ ...state, value }), 268 | bar: { 269 | foo: (state, data) => ({ ...state, value: state.value + 1, data }), 270 | }, 271 | }, 272 | "baz", 273 | ) 274 | 275 | t.deepEqual( 276 | actions.foo(1), 277 | { type: "baz/foo", payload: 1, "symbiote-payload": [1] }, 278 | "simple action type", 279 | ) 280 | t.deepEqual( 281 | actions.bar.foo("bar"), 282 | { type: "baz/bar/foo", payload: "bar", "symbiote-payload": ["bar"] }, 283 | "nested action with state type", 284 | ) 285 | t.deepEqual( 286 | reducer(undefined, actions.foo(1)), 287 | { value: 1, data: "foo" }, 288 | "reduce simple action", 289 | ) 290 | t.deepEqual( 291 | reducer(undefined, actions.bar.foo("bar")), 292 | { value: 1, data: "bar" }, 293 | "reduce nested action with state", 294 | ) 295 | t.is(actions.foo.toString(), "baz/foo", "foo.toString() return correct type") 296 | t.is( 297 | actions.bar.foo.toString(), 298 | "baz/bar/foo", 299 | "bar.foo.toString() return correct type", 300 | ) 301 | }) 302 | 303 | test("prefix as option namespace in object", (t) => { 304 | const { actions, reducer } = createSymbiote( 305 | { value: 0, data: "foo" }, 306 | { 307 | foo: (state, value) => ({ ...state, value }), 308 | bar: { 309 | foo: (state, data) => ({ ...state, value: state.value + 1, data }), 310 | }, 311 | }, 312 | { namespace: "baz" }, 313 | ) 314 | 315 | t.deepEqual( 316 | actions.foo(1), 317 | { type: "baz/foo", payload: 1, "symbiote-payload": [1] }, 318 | "simple action type", 319 | ) 320 | t.deepEqual( 321 | actions.bar.foo("bar"), 322 | { type: "baz/bar/foo", payload: "bar", "symbiote-payload": ["bar"] }, 323 | "nested action with state type", 324 | ) 325 | t.deepEqual( 326 | reducer(undefined, actions.foo(1)), 327 | { value: 1, data: "foo" }, 328 | "reduce simple action", 329 | ) 330 | t.deepEqual( 331 | reducer(undefined, actions.bar.foo("bar")), 332 | { value: 1, data: "bar" }, 333 | "reduce nested action with state", 334 | ) 335 | t.is(actions.foo.toString(), "baz/foo", "foo.toString() return correct type") 336 | t.is( 337 | actions.bar.foo.toString(), 338 | "baz/bar/foo", 339 | "bar.foo.toString() return correct type", 340 | ) 341 | }) 342 | 343 | test("supernested with prefix", (t) => { 344 | const { actions, reducer } = createSymbiote( 345 | { value: 0, data: "foo" }, 346 | { 347 | a: { 348 | b: { 349 | c: { 350 | d: { 351 | e: { 352 | g: (state, data) => ({ 353 | ...state, 354 | value: state.value + 1, 355 | data, 356 | }), 357 | }, 358 | }, 359 | }, 360 | }, 361 | }, 362 | }, 363 | "prefix", 364 | ) 365 | 366 | t.deepEqual( 367 | actions.a.b.c.d.e.g("bar"), 368 | { type: "prefix/a/b/c/d/e/g", payload: "bar", "symbiote-payload": ["bar"] }, 369 | "nested action with state type", 370 | ) 371 | t.deepEqual( 372 | reducer(undefined, actions.a.b.c.d.e.g("bar")), 373 | { value: 1, data: "bar" }, 374 | "reduce nested action with state", 375 | ) 376 | t.is( 377 | actions.a.b.c.d.e.g.toString(), 378 | "prefix/a/b/c/d/e/g", 379 | ".toString() return correct type", 380 | ) 381 | }) 382 | 383 | test("defaultReducer option", (t) => { 384 | const { actions, reducer } = createSymbiote( 385 | { value: 0, data: "foo" }, 386 | { 387 | foo: () => 100, 388 | }, 389 | { defaultReducer: () => "CUSTOM" }, 390 | ) 391 | 392 | t.deepEqual(reducer(undefined, actions.foo(1)), 100) 393 | t.deepEqual(reducer(undefined, { type: "UNKNOWN" }), "CUSTOM") 394 | }) 395 | 396 | test("defaultReducer receives prevState and action", (t) => { 397 | const { actions, reducer } = createSymbiote( 398 | 0, 399 | { 400 | foo: () => 100, 401 | }, 402 | { defaultReducer: (state, action) => action.payload.value }, 403 | ) 404 | 405 | t.deepEqual(reducer(undefined, actions.foo(1)), 100) 406 | t.deepEqual(reducer(undefined, { type: "UNKNOWN", payload: { value: 2 } }), 2) 407 | }) 408 | 409 | test("defaultReducer receives original action", (t) => { 410 | const { reducer } = createSymbiote( 411 | 0, 412 | { 413 | foo: () => 100, 414 | }, 415 | { defaultReducer: (state, action) => action }, 416 | ) 417 | const customAction = { type: "Some", value: 1 } 418 | 419 | t.deepEqual(reducer(undefined, customAction), customAction) 420 | }) 421 | 422 | test("defaultReducer receive previous state", (t) => { 423 | const { actions, reducer } = createSymbiote( 424 | { value: 0, data: "foo" }, 425 | { 426 | foo: () => 100, 427 | }, 428 | { defaultReducer: (state) => state + 1 }, 429 | ) 430 | 431 | t.deepEqual(reducer(undefined, actions.foo(1)), 100) 432 | t.deepEqual(reducer(900, { type: "UNKNOWN" }), 901) 433 | }) 434 | 435 | test("defaultReducer option do not break namespace", (t) => { 436 | const { actions, reducer } = createSymbiote( 437 | { value: 0, data: "foo" }, 438 | { 439 | foo: () => 100, 440 | bar: { 441 | baz: () => 200, 442 | }, 443 | }, 444 | { defaultReducer: () => "CUSTOM", namespace: "NAMESPACE/T" }, 445 | ) 446 | 447 | t.deepEqual(reducer(undefined, actions.foo()), 100) 448 | t.deepEqual(reducer(undefined, actions.bar.baz()), 200) 449 | t.deepEqual(reducer(undefined, { type: "UNKNOWN" }), "CUSTOM") 450 | t.deepEqual(reducer(undefined, { type: "NAMESPACE/T/foo" }), 100) 451 | t.deepEqual(reducer(undefined, { type: "NAMESPACE/T/bar/baz" }), 200) 452 | }) 453 | 454 | test("separator", (t) => { 455 | const { actions, reducer } = createSymbiote( 456 | { value: 0, data: "foo" }, 457 | { 458 | foo: (state, value) => ({ ...state, value }), 459 | bar: { 460 | foo: (state, data) => ({ ...state, value: state.value + 1, data }), 461 | }, 462 | }, 463 | { separator: "::" }, 464 | ) 465 | 466 | t.deepEqual( 467 | actions.foo(1), 468 | { type: "foo", payload: 1, "symbiote-payload": [1] }, 469 | "simple action type", 470 | ) 471 | t.deepEqual( 472 | actions.bar.foo("bar"), 473 | { type: "bar::foo", payload: "bar", "symbiote-payload": ["bar"] }, 474 | "nested action with state type", 475 | ) 476 | t.deepEqual( 477 | reducer(undefined, actions.foo(1)), 478 | { value: 1, data: "foo" }, 479 | "reduce simple action", 480 | ) 481 | t.deepEqual( 482 | reducer(undefined, actions.bar.foo("bar")), 483 | { value: 1, data: "bar" }, 484 | "reduce nested action with state", 485 | ) 486 | t.is(actions.foo.toString(), "foo", "foo.toString() return correct type") 487 | t.is( 488 | actions.bar.foo.toString(), 489 | "bar::foo", 490 | "bar.foo.toString() return correct type", 491 | ) 492 | }) 493 | 494 | test("separator and namespace", (t) => { 495 | const { actions, reducer } = createSymbiote( 496 | { value: 0, data: "foo" }, 497 | { 498 | foo: (state, value) => ({ ...state, value }), 499 | bar: { 500 | foo: (state, data) => ({ ...state, value: state.value + 1, data }), 501 | }, 502 | }, 503 | { separator: "::", namespace: "ns" }, 504 | ) 505 | 506 | t.deepEqual( 507 | actions.foo(1), 508 | { type: "ns::foo", payload: 1, "symbiote-payload": [1] }, 509 | "simple action type", 510 | ) 511 | t.deepEqual( 512 | actions.bar.foo("bar"), 513 | { type: "ns::bar::foo", payload: "bar", "symbiote-payload": ["bar"] }, 514 | "nested action with state type", 515 | ) 516 | t.deepEqual( 517 | reducer(undefined, actions.foo(1)), 518 | { value: 1, data: "foo" }, 519 | "reduce simple action", 520 | ) 521 | t.deepEqual( 522 | reducer(undefined, actions.bar.foo("bar")), 523 | { value: 1, data: "bar" }, 524 | "reduce nested action with state", 525 | ) 526 | t.is(actions.foo.toString(), "ns::foo", "foo.toString() return correct type") 527 | t.is( 528 | actions.bar.foo.toString(), 529 | "ns::bar::foo", 530 | "bar.foo.toString() return correct type", 531 | ) 532 | }) 533 | --------------------------------------------------------------------------------