├── .dockerignore ├── src ├── leafReducer │ ├── index.ts │ └── leafReducer.ts ├── use-riducer │ ├── index.ts │ ├── useRiducer.ts │ └── useRiducer.test.ts ├── types │ ├── index.ts │ ├── util-types.ts │ ├── creator-types.ts │ └── action-types.ts ├── proxy │ ├── index.ts │ ├── wrapWithCreate.ts │ └── createActionsProxy.ts ├── index.ts ├── string │ ├── stringLeafReducer.ts │ ├── string-types.ts │ └── makeStringCreators.ts ├── bundle.ts ├── number │ ├── numberLeafReducer.ts │ ├── number-types.ts │ └── makeNumberCreators.ts ├── boolean │ ├── booleanLeafReducer.ts │ ├── makeBooleanCreators.ts │ └── boolean-types.ts ├── README.dts-jest.ts ├── utils │ ├── update-state.test.ts │ ├── array-utils.ts │ ├── update-state.ts │ └── generatePushID.ts ├── universal │ ├── universalLeafReducer.ts │ ├── makeUniversalCreators.ts │ └── universal-types.ts ├── array │ ├── makeArrayCreators.ts │ ├── arrayLeafReducer.ts │ └── array-types.ts ├── object │ ├── makeObjectCreators.ts │ ├── objectLeafReducer.ts │ └── object-types.ts ├── README.spec.ts ├── create │ ├── makeCreatorOfTypeFromPath.ts │ └── makeTypedCreators.ts ├── custom │ ├── makeCustomCreators.ts │ └── custom-types.ts ├── undefined │ └── undefinedLeafReducer.ts ├── riduce.ts └── riduce.dts-jest.ts ├── .npmignore ├── .babelrc ├── .travis.yml ├── babel.config.js ├── docs ├── old │ ├── defaults │ │ ├── clear.dts-jest.ts │ │ ├── reset.dts-jest.ts │ │ ├── on.dts-jest.ts │ │ ├── off.dts-jest.ts │ │ ├── toggle.dts-jest.ts │ │ ├── increment.dts-jest.ts │ │ ├── increment.spec.ts │ │ ├── drop.spec.ts │ │ ├── drop.dts-jest.ts │ │ ├── on.spec.ts │ │ ├── off.spec.ts │ │ ├── toggle.spec.ts │ │ ├── filter.spec.ts │ │ ├── filter.dts-jest.ts │ │ ├── path.dts-jest.ts │ │ ├── path.spec.ts │ │ ├── noop.spec.ts │ │ ├── concat.spec.ts │ │ ├── set.spec.ts │ │ ├── on.md │ │ ├── assign.spec.ts │ │ ├── off.md │ │ ├── do.dts-jest.ts │ │ ├── toggle.md │ │ ├── README.md │ │ ├── push.dts-jest.ts │ │ ├── drop.md │ │ ├── increment.md │ │ ├── concat.dts-jest.ts │ │ ├── push.spec.ts │ │ ├── set.md │ │ ├── concat.md │ │ ├── noop.dts-jest.ts │ │ ├── pushedSet.dts-jest.ts │ │ ├── reset.spec.ts │ │ ├── path.md │ │ ├── assign.md │ │ ├── pushedSet.spec.ts │ │ ├── filter.md │ │ ├── reset.md │ │ ├── update.spec.ts │ │ ├── clear.spec.ts │ │ ├── do.spec.ts │ │ ├── update.dts-jest.ts │ │ ├── push.md │ │ ├── update.md │ │ ├── do.md │ │ ├── pushedSet.md │ │ ├── clear.md │ │ ├── set.dts-jest.ts │ │ └── assign.dts-jest.ts │ ├── typescript │ │ ├── README.md │ │ └── actions.dts-jest.ts │ ├── examples │ │ ├── basicExample.spec.ts │ │ ├── useReducerExample.md │ │ ├── intermediateExample.spec.ts │ │ ├── basicExample.md │ │ ├── intermediateExample.md │ │ ├── typescriptExample.md │ │ ├── advancedExample.spec.ts │ │ └── advancedExample.md │ ├── api │ │ ├── actions.spec.ts │ │ ├── creatorKeys.md │ │ ├── actions.md │ │ ├── create.spec.ts │ │ ├── bundle.spec.ts │ │ ├── leafReducers.md │ │ ├── create.md │ │ └── bundle.md │ ├── motivation.md │ ├── README.md │ └── intro │ │ ├── README.md │ │ └── features.md └── riduce-advanced.dts-jest.ts ├── .gitignore ├── package.json ├── tsconfig.json ├── jest.config.js └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /src/leafReducer/index.ts: -------------------------------------------------------------------------------- 1 | import leafReducer from './leafReducer'; 2 | 3 | export default leafReducer -------------------------------------------------------------------------------- /src/use-riducer/index.ts: -------------------------------------------------------------------------------- 1 | import useRiducer from "./useRiducer"; 2 | 3 | export default useRiducer; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #docusaurus 2 | /website 3 | 4 | # source 5 | /src 6 | /docs 7 | 8 | # testing 9 | /coverage 10 | .coveralls.yml -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export { ActionsProxy } from '../proxy' 2 | 3 | export * from './action-types' 4 | export * from './creator-types' 5 | -------------------------------------------------------------------------------- /src/proxy/index.ts: -------------------------------------------------------------------------------- 1 | import createActionsProxy, { ActionsProxy } from './createActionsProxy'; 2 | 3 | export { 4 | createActionsProxy, 5 | ActionsProxy 6 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/typescript" 5 | ], 6 | "plugins": [ 7 | "@babel/proposal-class-properties", 8 | "@babel/proposal-object-rest-spread" 9 | ] 10 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | jobs: 4 | include: 5 | - stage: Check compilation 6 | node_js: node 7 | script: npm run build 8 | 9 | - stage: Run tests 10 | node_js: node 11 | script: jest --runInBand -------------------------------------------------------------------------------- /src/types/util-types.ts: -------------------------------------------------------------------------------- 1 | export type KnownKeys = { 2 | [K in keyof T]: string extends K ? never : number extends K ? never : K 3 | } extends { [_ in keyof T]: infer U } ? U : never; 4 | 5 | export type Unpacked = IsArrayT extends (infer U)[] ? U : unknown; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import riduce, { Reducer } from "./riduce"; 2 | import bundle from "./bundle"; 3 | import useRiducer from "./use-riducer"; 4 | export { Action, ActionWithPayload, ActionsProxy } from "./types"; 5 | export { 6 | RiducerDict, 7 | Riducer, 8 | PermissiveRiducer, 9 | ShorthandRiducer, 10 | LonghandRiducer, 11 | } from "./types"; 12 | 13 | export default riduce; 14 | 15 | export { bundle, Reducer, useRiducer }; 16 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | "@babel/env", 4 | { 5 | targets: { 6 | edge: "17", 7 | firefox: "60", 8 | chrome: "67", 9 | safari: "11.1", 10 | }, 11 | useBuiltIns: "usage", 12 | }, 13 | ], 14 | // [ 15 | // "@babel/preset-env", 16 | // { 17 | // targets: { 18 | // node: "current" 19 | // }, 20 | // }, 21 | // ], 22 | ]; 23 | 24 | module.exports = { presets }; -------------------------------------------------------------------------------- /docs/old/defaults/clear.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | num: 2, 5 | arr: [1, 2, 3], 6 | bool: true 7 | } 8 | 9 | const [_, actions] = riduce(initialState) 10 | 11 | // @dts-jest:pass Allows clear on an array branch 12 | actions.arr.create.clear() 13 | 14 | // @dts-jest:pass Allows clear on an array element 15 | actions.arr[1].create.clear() 16 | 17 | // @dts-jest:fail Type error on an argument provided 18 | actions.arr.create.clear([]) -------------------------------------------------------------------------------- /docs/old/defaults/reset.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | num: 2, 5 | arr: [1, 2, 3], 6 | bool: true 7 | } 8 | 9 | const [_, actions] = riduce(initialState) 10 | 11 | // @dts-jest:pass Allows reset on an array branch 12 | actions.arr.create.reset() 13 | 14 | // @dts-jest:pass Allows reset on an array element 15 | actions.arr[1].create.reset() 16 | 17 | // @dts-jest:fail Type error on an argument provided 18 | actions.arr.create.reset([]) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /website/node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | .coveralls.yml 12 | 13 | # production 14 | /build 15 | /website/build 16 | /dist 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .vscode 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | -------------------------------------------------------------------------------- /src/string/stringLeafReducer.ts: -------------------------------------------------------------------------------- 1 | import { isConcatActionString } from './string-types' 2 | import { Action, isClearAction } from "../types" 3 | import universalLeafReducer from '../universal/universalLeafReducer'; 4 | 5 | function stringLeafReducer(leafState: L, treeState: T, action: A, originalState: T): L { 6 | 7 | if (isConcatActionString(action)) { 8 | return leafState.concat(action.payload) as L 9 | } 10 | 11 | 12 | if (isClearAction(action)) { 13 | return '' as L 14 | } 15 | 16 | return universalLeafReducer(leafState, treeState, action, originalState) 17 | } 18 | 19 | export default stringLeafReducer -------------------------------------------------------------------------------- /docs/old/defaults/on.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']]} 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Incrememnt default creator 14 | { 15 | // @dts-jest:pass exists on boolean state 16 | actions.bool.create.on 17 | 18 | // @dts-jest:fail does not exist on number state 19 | actions.num.create.on 20 | 21 | // @dts-jest:pass does not need an argument 22 | actions.bool.create.on() 23 | 24 | // @dts-jest:fail rejects arguments 25 | actions.num.create.on(true) 26 | } -------------------------------------------------------------------------------- /docs/old/defaults/off.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']]} 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Incrememnt default creator 14 | { 15 | // @dts-jest:pass exists on boolean state 16 | actions.bool.create.off 17 | 18 | // @dts-jest:fail does not exist on number state 19 | actions.num.create.off 20 | 21 | // @dts-jest:pass does not need an argument 22 | actions.bool.create.off() 23 | 24 | // @dts-jest:fail rejects arguments 25 | actions.num.create.off(true) 26 | } -------------------------------------------------------------------------------- /src/bundle.ts: -------------------------------------------------------------------------------- 1 | import { prop } from "ramda"; 2 | import { Action, BundledAction, isCallbackAction, RiduceAction } from "./types"; 3 | 4 | function bundle( 5 | actions: RiduceAction[], 6 | type?: string 7 | ): BundledAction { 8 | const actionTypes = actions.map((action) => 9 | isCallbackAction(action) ? action.name : action.type 10 | ); 11 | 12 | return { 13 | type: type || actionTypes.join("; "), 14 | payload: actions, 15 | leaf: { 16 | creatorKey: "bundle", 17 | CREATOR_KEY: "bundle", 18 | custom: false, 19 | bundled: actionTypes, 20 | path: [], 21 | }, 22 | }; 23 | } 24 | 25 | export default bundle; 26 | -------------------------------------------------------------------------------- /src/number/numberLeafReducer.ts: -------------------------------------------------------------------------------- 1 | import { defaultTo } from "ramda"; 2 | import { Action, isClearAction } from "../types" 3 | import universalLeafReducer from '../universal/universalLeafReducer'; 4 | import { isIncrementAction } from "./number-types"; 5 | 6 | function numberLeafReducer(leafState: L, treeState: T, action: A, originalState: T): L { 7 | 8 | if (isIncrementAction(action)) { 9 | return leafState + defaultTo(1, action.payload) as L 10 | } 11 | 12 | if (isClearAction(action)) { 13 | return 0 as L 14 | } 15 | 16 | return universalLeafReducer(leafState, treeState, action, originalState) 17 | } 18 | 19 | export default numberLeafReducer -------------------------------------------------------------------------------- /docs/old/defaults/toggle.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']]} 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Incrememnt default creator 14 | { 15 | // @dts-jest:pass exists on boolean state 16 | actions.bool.create.toggle 17 | 18 | // @dts-jest:fail does not exist on number state 19 | actions.num.create.toggle 20 | 21 | // @dts-jest:pass does not need an argument 22 | actions.bool.create.toggle() 23 | 24 | // @dts-jest:fail rejects arguments 25 | actions.num.create.toggle(true) 26 | } -------------------------------------------------------------------------------- /src/boolean/booleanLeafReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, isClearAction } from "../types" 2 | import universalLeafReducer from '../universal/universalLeafReducer'; 3 | import { isOffAction, isOnAction, isToggleAction } from "./boolean-types"; 4 | 5 | function booleanLeafReducer(leafState: L, treeState: T, action: A, originalState: T): L { 6 | 7 | if (isClearAction(action)) return false as L 8 | 9 | if (isOffAction(action)) return false as L 10 | 11 | if (isOnAction(action)) return true as L 12 | 13 | if (isToggleAction(action)) return !leafState as L 14 | 15 | return universalLeafReducer(leafState, treeState, action, originalState) 16 | } 17 | 18 | export default booleanLeafReducer -------------------------------------------------------------------------------- /docs/old/typescript/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | hide_title: true 5 | --- 6 | 7 | Redux-Leaves provides typings out of the box. 8 | 9 | ## `actions` 10 | Actions is typed to keep to your provided state structure. 11 | 12 | ```ts 13 | import riduce from 'redux-leaves' 14 | 15 | const initialState = { 16 | shallow: true, 17 | nested: { 18 | counter: 0, 19 | state: { 20 | deep: 'somewhat' 21 | } 22 | } 23 | } 24 | 25 | const [reducer, actions] = riduce(initialState) 26 | 27 | actions.shallow // compiles 28 | actions.foobar // (ts 2339) Property 'foobar' does not exist on type... 29 | 30 | actions.nested.counter // compiles 31 | actions.nested.string // (ts 2339) Property 'nested' does not exist on type... 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /src/README.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Reducer } from "redux" 2 | import riduce from "." 3 | 4 | const museumState = { 5 | isOpen: false, 6 | visitor: { 7 | counter: 0, 8 | guestbook: ['richard woz here'] 9 | } 10 | } 11 | 12 | const [reducer, actions] = riduce(museumState) 13 | const { getState, dispatch } = createStore(reducer as Reducer) 14 | 15 | // @dts-jest:group Typed actions 16 | { 17 | // @dts-jest:fail 18 | actions.isOpen.create.push() 19 | 20 | // @dts-jest:fail 21 | actions.visitor.guestbook.create.push() 22 | 23 | // @dts-jest:fail 24 | actions.visitor.guestbook.create.push(10) 25 | 26 | // @dts-jest:pass 27 | actions.visitor.guestbook.create.push('10') 28 | 29 | // @dts-jest:pass 30 | dispatch(actions.visitor.guestbook.create.push('10')) 31 | } -------------------------------------------------------------------------------- /src/string/string-types.ts: -------------------------------------------------------------------------------- 1 | import { ActionWithPayload, Action } from "../types"; 2 | 3 | export enum StringCreatorKeys { 4 | CONCAT = "CONCAT_STR", 5 | } 6 | 7 | export type StringCreators = { 8 | concat(str: string): ActionWithPayload; 9 | }; 10 | 11 | export type StringActions< 12 | KeyT extends keyof StringCreators, 13 | LeafT extends string = string, 14 | TreeT = unknown 15 | > = ReturnType[KeyT]>; 16 | 17 | export function isStringAction(action: Action): boolean { 18 | return isConcatActionString(action); 19 | } 20 | 21 | export function isConcatActionString( 22 | action: Action 23 | ): action is StringActions<"concat"> { 24 | return action.leaf.CREATOR_KEY === StringCreatorKeys.CONCAT; 25 | } 26 | -------------------------------------------------------------------------------- /src/number/number-types.ts: -------------------------------------------------------------------------------- 1 | import { ActionWithPayload, Action } from "../types"; 2 | 3 | export enum NumberCreatorKeys { 4 | INCREMENT = "INCREMENT", 5 | } 6 | 7 | export type NumberCreators = { 8 | increment(n?: number): ActionWithPayload; 9 | }; 10 | 11 | export type NumberActions< 12 | KeyT extends keyof NumberCreators, 13 | LeafT extends number = number, 14 | TreeT = unknown 15 | > = ReturnType[KeyT]>; 16 | 17 | export function isNumberAction(action: Action): boolean { 18 | return isIncrementAction(action); 19 | } 20 | 21 | export function isIncrementAction( 22 | action: Action 23 | ): action is NumberActions<"increment"> { 24 | return action.leaf.CREATOR_KEY === NumberCreatorKeys.INCREMENT; 25 | } 26 | -------------------------------------------------------------------------------- /docs/old/defaults/increment.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']]} 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Incrememnt default creator 14 | { 15 | // @dts-jest:fail does not exist on boolean state 16 | actions.bool.create.increment 17 | 18 | // @dts-jest:pass exists on number state 19 | actions.num.create.increment 20 | 21 | // @dts-jest:pass does not need an argument 22 | actions.num.create.increment() 23 | 24 | // @dts-jest:pass can take one numerical argument 25 | actions.num.create.increment(5) 26 | 27 | // @dts-jest:fail does not take non-numerical argument 28 | actions.num.create.increment('5') 29 | } -------------------------------------------------------------------------------- /src/utils/update-state.test.ts: -------------------------------------------------------------------------------- 1 | import { updateState } from "./update-state" 2 | 3 | describe('updateState', () => { 4 | test("Handles nested state pointing to an array", () => { 5 | const initialState = { arr: [{ nested: true }] } 6 | 7 | const result = updateState(initialState, ['arr', 0, 'nested'], false) 8 | expect(initialState).toEqual({ arr: [{ nested: true }] }) 9 | expect(result).toEqual({ arr: [{ nested: false }] }) 10 | }) 11 | 12 | test("Handles nested state pointing to an object with numeric keys", () => { 13 | const initialState = { arr: { 0: { nested: true } } } 14 | 15 | const result = updateState(initialState, ['arr', 0, 'nested'], false) 16 | expect(initialState).toEqual({ arr: { 0: { nested: true } } }) 17 | expect(result).toEqual({ arr: { 0: { nested: false } } }) 18 | }) 19 | }) -------------------------------------------------------------------------------- /docs/old/defaults/increment.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.increment(n = 1): returns an action that, when dispatched, updates the leaf's state by non-mutatively incrementing it by n", () => { 5 | const initialState = { 6 | foo: 5, 7 | bar: 5 8 | } 9 | 10 | const [reducer, actions] = riduce(initialState) 11 | const store = createStore(reducer) 12 | 13 | test("No argument provided", () => { 14 | const incrementFoo = actions.foo.create.increment 15 | store.dispatch(incrementFoo()) 16 | expect(store.getState().foo).toBe(6) 17 | }) 18 | 19 | test("Providing an argument", () => { 20 | const incrementBar = actions.bar.create.increment 21 | store.dispatch(incrementBar(37)) 22 | expect(store.getState().bar).toBe(42) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/universal/universalLeafReducer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isDoAction, 3 | isUpdateAction, 4 | isResetAction, 5 | isNoopAction, 6 | } from "./universal-types"; 7 | import { Action } from "../types"; 8 | import { getState } from "../utils/update-state"; 9 | 10 | function universalLeafReducer( 11 | leafState: L, 12 | treeState: T, 13 | action: A, 14 | originalState: T 15 | ): L { 16 | if (isDoAction(action)) { 17 | return action.payload(leafState, treeState); 18 | } 19 | 20 | if (isUpdateAction(action)) { 21 | return action.payload; 22 | } 23 | 24 | if (isResetAction(action)) { 25 | return getState(originalState, action.leaf.path) as L; 26 | } 27 | 28 | if (isNoopAction(action)) { 29 | return leafState; 30 | } 31 | 32 | return leafState; 33 | } 34 | 35 | export default universalLeafReducer; 36 | -------------------------------------------------------------------------------- /docs/old/defaults/drop.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.drop(n = 1): returns an action that, when dispatched, updates the leaf's state by non-mutatively dropping the first n values", () => { 5 | const initialState = { 6 | foo: ['a', 'b', 'c'], 7 | bar: ['a', 'b', 'c'] 8 | } 9 | 10 | const [reducer, actions] = riduce(initialState) 11 | const store = createStore(reducer) 12 | 13 | test("No argument provided", () => { 14 | const dropFromFoo = actions.foo.create.drop 15 | store.dispatch(dropFromFoo()) 16 | expect(store.getState().foo).toEqual(['b', 'c']) 17 | }) 18 | 19 | test("Providing an argument", () => { 20 | const dropFromBar = actions.bar.create.drop 21 | store.dispatch(dropFromBar(2)) 22 | expect(store.getState().bar).toEqual(['c']) 23 | }) 24 | }) -------------------------------------------------------------------------------- /docs/old/defaults/drop.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']]} 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Drop default creator 14 | { 15 | // @dts-jest:fail does not exist on boolean state 16 | actions.bool.create.drop 17 | 18 | // @dts-jest:pass exists on array state 19 | actions.arr.create.drop 20 | 21 | // @dts-jest:pass exists on nested array state 22 | actions.obj.names[0].create.drop 23 | 24 | // @dts-jest:pass does not require an argument 25 | actions.arr.create.drop() 26 | 27 | // @dts-jest:pass can take a number as an argument 28 | actions.arr.create.drop(2) 29 | 30 | // @dts-jest:fail refuses non-number arguments 31 | actions.arr.create.drop('2') 32 | } -------------------------------------------------------------------------------- /docs/old/defaults/on.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.on(): returns an action that, when dispatched, updates the leaf's state to false", () => { 5 | 6 | describe("GIVEN initialState is an object", () => { 7 | const initialState = { 8 | foo: false, 9 | bar: false 10 | } 11 | 12 | const [reducer, actions] = riduce(initialState) 13 | const store = createStore(reducer) 14 | 15 | test('Calling create.on', () => { 16 | const turnOnFoo = actions.foo.create.on 17 | store.dispatch(turnOnFoo()) 18 | expect(store.getState().foo).toBe(true) 19 | }) 20 | 21 | test('Calling create(actionType).on', () => { 22 | const turnOnBar = actions.bar.create("TURN_ON_BAR").on 23 | store.dispatch(turnOnBar()) 24 | expect(store.getState().bar).toBe(true) 25 | }) 26 | }) 27 | }) -------------------------------------------------------------------------------- /docs/old/defaults/off.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.off(): returns an action that, when dispatched, updates the leaf's state to false", () => { 5 | 6 | describe("GIVEN initialState is an object", () => { 7 | const initialState = { 8 | foo: true, 9 | bar: true 10 | } 11 | 12 | const [reducer, actions] = riduce(initialState) 13 | const store = createStore(reducer) 14 | 15 | test('Calling create.off', () => { 16 | const turnOffFoo = actions.foo.create.off 17 | store.dispatch(turnOffFoo()) 18 | expect(store.getState().foo).toBe(false) 19 | }) 20 | 21 | test('Calling create(actionType).off', () => { 22 | const turnOffBar = actions.bar.create("TURN_OFF_BAR").off 23 | store.dispatch(turnOffBar()) 24 | expect(store.getState().bar).toBe(false) 25 | }) 26 | }) 27 | }) -------------------------------------------------------------------------------- /docs/old/defaults/toggle.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.toggle(): returns an action that, when dispatched, updates the leaf's state to false", () => { 5 | 6 | describe("GIVEN initialState is an object", () => { 7 | const initialState = { 8 | foo: true, 9 | bar: false 10 | } 11 | 12 | const [reducer, actions] = riduce(initialState) 13 | const store = createStore(reducer) 14 | 15 | test('Calling create.toggle', () => { 16 | const turnOnFoo = actions.foo.create.toggle 17 | store.dispatch(turnOnFoo()) 18 | expect(store.getState().foo).toBe(false) 19 | }) 20 | 21 | test('Calling create(actionType).toggle', () => { 22 | const turnOnBar = actions.bar.create("TURN_ON_BAR").toggle 23 | store.dispatch(turnOnBar()) 24 | expect(store.getState().bar).toBe(true) 25 | }) 26 | }) 27 | }) -------------------------------------------------------------------------------- /docs/old/defaults/filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.filter(callback): returns an action that, when dispatched, updates the leaf's state by filtering it with the callback", () => { 5 | const initialState = { 6 | foo: [1, 2, 3, 4, 5], 7 | bar: ['cat', 'dog', 'bat'] 8 | } 9 | 10 | const [reducer, actions] = riduce(initialState) 11 | const store = createStore(reducer) 12 | 13 | test('Calling create.filter', () => { 14 | const filterFoo = actions.foo.create.filter 15 | store.dispatch(filterFoo(e => !(e % 2))) 16 | expect(store.getState().foo).toEqual([2, 4]) 17 | }) 18 | 19 | test('Calling create(actionType).filter', () => { 20 | const filterBar = actions.bar.create('FILTER_BAR').filter 21 | store.dispatch(filterBar(e => e.includes('at'))) 22 | expect(store.getState().bar).toEqual(['cat', 'bat']) 23 | }) 24 | 25 | 26 | }) -------------------------------------------------------------------------------- /src/universal/makeUniversalCreators.ts: -------------------------------------------------------------------------------- 1 | import { UniversalCreators, UniversalCreatorKeys, CreateFn } from "../types"; 2 | import makeCreatorOfTypeFromPath from "../create/makeCreatorOfTypeFromPath"; 3 | 4 | function makeUniversalCreators( 5 | leafState: L, 6 | path: (string | number)[] 7 | ): CreateFn> { 8 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path); 9 | 10 | return (passedType?: string) => { 11 | const creatorOfType = makeCreatorOfType(passedType); 12 | return { 13 | clear: () => creatorOfType(UniversalCreatorKeys.CLEAR), 14 | do: (cb) => creatorOfType(UniversalCreatorKeys.DO, cb), 15 | noop: () => creatorOfType(UniversalCreatorKeys.NOOP), 16 | reset: () => creatorOfType(UniversalCreatorKeys.RESET), 17 | update: (newVal: L) => creatorOfType(UniversalCreatorKeys.UPDATE, newVal), 18 | }; 19 | }; 20 | } 21 | 22 | export default makeUniversalCreators; 23 | -------------------------------------------------------------------------------- /docs/old/defaults/filter.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']] } 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Filter default creator 14 | { 15 | // @dts-jest:fail does not exist on boolean state 16 | actions.bool.create.filter 17 | 18 | // @dts-jest:pass exists on array state 19 | actions.arr.create.filter 20 | 21 | // @dts-jest:pass exists on nested array state 22 | actions.obj.names[0].create.filter 23 | 24 | // @dts-jest:fail requires an argument 25 | actions.arr.create.filter() 26 | 27 | // @dts-jest:pass infers a callback argument of a numnber 28 | actions.arr.create.filter(n => (n % 2) === 0) 29 | 30 | // @dts-jest:fail refuses callback that doesn't treat as number 31 | actions.arr.create.filter(n => n.toLowerCase()) 32 | } -------------------------------------------------------------------------------- /docs/old/defaults/path.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | nested: { deep: true }, 9 | obj: { num: 5, names: [['a', 'e'], ['b, c']] } 10 | } 11 | 12 | const [_, actions] = riduce(initialState) 13 | 14 | // @dts-jest:fail does not exist on boolean state 15 | actions.bool.create.path 16 | 17 | // @dts-jest:pass exists on object state 18 | actions.obj.create.path 19 | 20 | // @dts-jest:pass exists on root if object 21 | actions.create.path 22 | 23 | // @dts-jest:fail needs an argument 24 | actions.obj.create.path() 25 | 26 | // @dts-jest:fail needs two arguments 27 | actions.obj.create.path(['go', 'deep']) 28 | 29 | // @dts-jest:pass accepts two arguments, string of arrays and a value 30 | actions.obj.create.path(['go', 'deep'], true) 31 | 32 | // @dts-jest:fail must be string of arrays followed by value 33 | actions.obj.create.path(true, ['go', 'deep']) -------------------------------------------------------------------------------- /docs/old/defaults/path.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.path(path, value): returns an action that, when dispatched, updates the leaf's state by setting a proprety at path as value", () => { 5 | const initialState = { 6 | foo: {}, 7 | bar: { arbitrary: { keys: 3 } } 8 | } 9 | 10 | const [reducer, actions] = riduce(initialState) 11 | const store = createStore(reducer) 12 | 13 | test("Setting a new property", () => { 14 | const setAtPathInFoo = actions.foo.create.path 15 | store.dispatch(setAtPathInFoo(['nested', 'deep'], true)) 16 | expect(store.getState().foo).toEqual({ nested: { deep: true } }) 17 | }) 18 | 19 | test("Overwriting a property", () => { 20 | const setAtPathInBar = actions.bar.create.path 21 | store.dispatch(setAtPathInBar(['arbitrary', 'keys'], 5)) 22 | expect(store.getState().bar).toEqual({ arbitrary: { keys: 5 }}) 23 | }) 24 | }) -------------------------------------------------------------------------------- /docs/old/defaults/noop.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from "../../../src"; 3 | 4 | describe("leaf.create.noop(): returns an action that, when dispatched, makes no changes to state", () => { 5 | const initialState = { 6 | num: 10, 7 | arr: ["foo", "bar"], 8 | nested: { 9 | arbitrarily: { 10 | deep: true, 11 | }, 12 | }, 13 | }; 14 | 15 | const [reducer, actions] = riduce(initialState); 16 | const { dispatch, getState } = createStore(reducer); 17 | 18 | test("noop makes no changes on a leaf", () => { 19 | dispatch(actions.num.create.noop()); 20 | dispatch(actions.arr[0].create.noop()); 21 | dispatch(actions.nested.arbitrarily.deep.create.noop()); 22 | expect(getState()).toStrictEqual(initialState); 23 | }); 24 | 25 | test("noop makes no changes on the root", () => { 26 | dispatch(actions.create.noop()); 27 | expect(getState()).toStrictEqual(initialState); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /docs/old/examples/basicExample.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import riduce from '../../../src'; 3 | 4 | describe('Basic example', () => { 5 | const initialState = { 6 | counterOne: 0, 7 | counterTwo: 0 8 | } 9 | 10 | const [reducer, actions] = riduce(initialState) 11 | const { dispatch, getState } = createStore(reducer) 12 | 13 | test("Store initialises with the provided initialState", () => { 14 | expect(getState()).toEqual({ counterOne: 0, counterTwo: 0 }) 15 | }) 16 | 17 | test("We can increment counterOne by 3", () => { 18 | const actionToIncrementCounterOneByThree = actions.counterOne.create.increment(3) 19 | dispatch(actionToIncrementCounterOneByThree) 20 | expect(getState()).toEqual({ counterOne: 3, counterTwo: 0 }) 21 | }) 22 | 23 | test("We can increment counterTwo by 10", () => { 24 | dispatch(actions.counterTwo.create.increment(10)) 25 | expect(getState()).toEqual({ counterOne: 3, counterTwo: 10 }) 26 | }) 27 | }) -------------------------------------------------------------------------------- /src/use-riducer/useRiducer.ts: -------------------------------------------------------------------------------- 1 | import { useReducer, Dispatch, useMemo } from "react"; 2 | import riduce, { Reducer } from "../riduce"; 3 | import { Action, ActionsProxy, RiducerDict } from "../types"; 4 | 5 | type UsedRiducer> = { 6 | state: TreeT; 7 | dispatch: Dispatch; 8 | actions: ActionsProxy; 9 | reducer: Reducer; 10 | }; 11 | 12 | function useRiducer>( 13 | initialState: TreeT, 14 | riducerDict: RiducerDictT = {} as RiducerDictT 15 | ): UsedRiducer { 16 | const [reducer, actions] = useMemo(() => riduce(initialState, riducerDict), [ 17 | riduce, 18 | initialState, 19 | riducerDict, 20 | ]); 21 | const [state, dispatch] = useReducer(reducer, initialState); 22 | 23 | return { 24 | state, 25 | dispatch, 26 | actions, 27 | reducer, 28 | }; 29 | } 30 | 31 | export default useRiducer; 32 | -------------------------------------------------------------------------------- /docs/old/defaults/concat.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.concat(array): returns an action that, when dispatched, updates the leaf's state by non-mutatively concatenating it with array", () => { 5 | interface State { 6 | arr: (string|number)[], 7 | str: string 8 | } 9 | 10 | const initialState: State = { 11 | arr: [1, 2, 3], 12 | str: 'foo' 13 | } 14 | 15 | const [reducer, actions] = riduce(initialState) 16 | const store = createStore(reducer) 17 | 18 | test('Concatenating an array', () => { 19 | const concatToArr = actions.arr.create.concat 20 | store.dispatch(concatToArr(['a', 'b', 'c'])) 21 | expect(store.getState().arr).toEqual([1, 2, 3, 'a', 'b', 'c']) 22 | }) 23 | 24 | test('Concatenating a string', () => { 25 | const concatToStr = actions.str.create.concat 26 | store.dispatch(concatToStr('bar')) 27 | expect(store.getState().str).toBe('foobar') 28 | }) 29 | }) -------------------------------------------------------------------------------- /src/number/makeNumberCreators.ts: -------------------------------------------------------------------------------- 1 | import makeCreatorOfTypeFromPath from "../create/makeCreatorOfTypeFromPath"; 2 | import { CreateFn } from "../types"; 3 | import { NumberCreators, NumberCreatorKeys } from "./number-types"; 4 | 5 | function makeNumberCreators( 6 | leafState: L, 7 | path: (string | number)[] 8 | ): CreateFn> { 9 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path); 10 | return (passedType?: string) => 11 | madeNumberCreators(leafState, path, makeCreatorOfType, passedType); 12 | } 13 | 14 | export function madeNumberCreators( 15 | leafState: L, 16 | path: (string | number)[], 17 | makeCreatorOfType: ReturnType, 18 | passedType?: string 19 | ): NumberCreators { 20 | const creatorOfType = makeCreatorOfType(passedType); 21 | return { 22 | increment: (n?) => creatorOfType(NumberCreatorKeys.INCREMENT, n), 23 | }; 24 | } 25 | 26 | export default makeNumberCreators; 27 | -------------------------------------------------------------------------------- /docs/old/defaults/set.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.set(path, value): returns an action that, when dispatched, updates the leaf's state by non-mutatively setting value at state object's path", () => { 5 | interface State { 6 | foo: { accessed?: boolean }, 7 | bar: { props: boolean } 8 | } 9 | 10 | const initialState: State = { 11 | foo: {}, 12 | bar: { props: true } 13 | } 14 | 15 | const [reducer, actions] = riduce(initialState) 16 | const store = createStore(reducer) 17 | 18 | test("Setting a new property", () => { 19 | const setInFoo = actions.foo.create.set 20 | store.dispatch(setInFoo('accessed', true)) 21 | expect(store.getState().foo).toEqual({ accessed: true }) 22 | }) 23 | 24 | test("Overwriting a property", () => { 25 | const setInBar = actions.bar.create.set 26 | store.dispatch(setInBar('props', false)) 27 | expect(store.getState().bar).toEqual({ props: false }) 28 | }) 29 | }) -------------------------------------------------------------------------------- /docs/old/defaults/on.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: on 3 | title: on 4 | hide_title: true 5 | sidebar_label: on 6 | --- 7 | 8 | # `on()` 9 | **`create.on`** 10 | **`create(actionType).on`** 11 | *Appropriate leaf state: boolean* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to update the leaf's state to `true`. 14 | 15 | ## Returns 16 | `action` *(object)*: an object to dispatch to the store 17 | 18 | ## Example 19 | ```js 20 | import { createStore } from 'redux' 21 | import riduce from 'riduce' 22 | 23 | const initialState = { 24 | foo: false, 25 | bar: false 26 | } 27 | 28 | const [reducer, actions] = riduce(initialState) 29 | const store = createStore(reducer) 30 | ``` 31 | 32 | ### Calling `create.on` 33 | ```js 34 | const turnOffFoo = actions.foo.create.on 35 | store.dispatch(turnOffFoo()) 36 | console.log(store.getState().foo) // true 37 | ``` 38 | 39 | ### Calling `create(actionType).on` 40 | ```js 41 | const turnOffBar = actions.bar.create('TURN_ON_BAR').on 42 | store.dispatch(turnOffBar()) 43 | console.log(store.getState().bar) // ture 44 | ``` -------------------------------------------------------------------------------- /docs/old/defaults/assign.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.assign(...sources): returns an action that, when dispatched, updates the leaf's state by non-mutatively copying over properties from the sources", () => { 5 | interface State { 6 | foo: { props: boolean, count?: number }, 7 | bar: { props: boolean } 8 | } 9 | 10 | const initialState: State = { 11 | foo: { props: true }, 12 | bar: { props: false } 13 | } 14 | 15 | const [reducer, actions] = riduce(initialState) 16 | const store = createStore(reducer) 17 | 18 | test("Assigning new properties", () => { 19 | const assignToFoo = actions.foo.create.assign 20 | store.dispatch(assignToFoo({ count: 2 })) 21 | expect(store.getState().foo).toEqual({ props: true, count: 2 }) 22 | }) 23 | 24 | test("Overwriting properties", () => { 25 | const assignToBar = actions.bar.create.assign 26 | store.dispatch(assignToBar({ props: true })) 27 | expect(store.getState().bar).toEqual({ props: true }) 28 | }) 29 | }) -------------------------------------------------------------------------------- /docs/old/defaults/off.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: off 3 | title: off 4 | hide_title: true 5 | sidebar_label: off 6 | --- 7 | 8 | # `off()` 9 | **`create.off`** 10 | **`create(actionType).off`** 11 | *Appropriate leaf state: boolean* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to update the leaf's state to `false`. 14 | 15 | ## Returns 16 | `action` *(object)*: an object to dispatch to the store 17 | 18 | ## Example 19 | ```js 20 | import { createStore } from 'redux' 21 | import riduce from 'riduce' 22 | 23 | const initialState = { 24 | foo: true, 25 | bar: true 26 | } 27 | 28 | const [reducer, actions] = riduce(initialState) 29 | const store = createStore(reducer) 30 | ``` 31 | 32 | ### Calling `create.off` 33 | ```js 34 | const turnOffFoo = actions.foo.create.off 35 | store.dispatch(turnOffFoo()) 36 | console.log(store.getState().foo) // false 37 | ``` 38 | 39 | ### Calling `create(actionType).off` 40 | ```js 41 | const turnOffBar = actions.bar.create('TURN_OFF_BAR').off 42 | store.dispatch(turnOffBar()) 43 | console.log(store.getState().bar) // false 44 | ``` -------------------------------------------------------------------------------- /docs/old/defaults/do.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, str: 'bar' } 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:pass Allows string callback for string state 14 | actions.str.create.do(state => state.toUpperCase()) 15 | 16 | // @dts-jest:fail Doesn't allow array callback for string state 17 | actions.str.create.do(state => state.push(4)) 18 | 19 | // @dts-jest:pass Allows returning appropriate branch 20 | actions.obj.create.do(state => ({ num: 7, str: 'foo' })) 21 | 22 | // @dts-jest:fail Disallows returning non-conforming branch 23 | actions.obj.create.do(state => ({ num: '7', str: 'foo' })) 24 | 25 | // @dts-jest:pass Allows using a callback with a second argument of treeState 26 | actions.arr.create.do((state, treeState) => [...state, treeState.num]) 27 | 28 | // @dts-jest:fail Disallows using a callback with a non-conforming argument of treeState 29 | actions.arr.create.do((state, treeState) => [...state, treeState.str]) -------------------------------------------------------------------------------- /src/string/makeStringCreators.ts: -------------------------------------------------------------------------------- 1 | import makeCreatorOfTypeFromPath from "../create/makeCreatorOfTypeFromPath"; 2 | import { CreateFn } from "../types"; 3 | import { StringCreators, StringCreatorKeys } from "./string-types"; 4 | 5 | function makeStringCreators( 6 | leafState: L, 7 | path: (string | number)[] 8 | ): CreateFn> { 9 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path); 10 | return (passedType?: string) => { 11 | const creatorOfType = makeCreatorOfType(passedType); 12 | return { 13 | concat: (n) => creatorOfType(StringCreatorKeys.CONCAT, n), 14 | }; 15 | }; 16 | } 17 | 18 | export function madeStringCreators( 19 | leafState: L, 20 | path: (string | number)[], 21 | makeCreatorOfType: ReturnType, 22 | passedType?: string 23 | ): StringCreators { 24 | const creatorOfType = makeCreatorOfType(passedType); 25 | return { 26 | concat: (n) => creatorOfType(StringCreatorKeys.CONCAT, n), 27 | }; 28 | } 29 | 30 | export default makeStringCreators; 31 | -------------------------------------------------------------------------------- /src/proxy/wrapWithCreate.ts: -------------------------------------------------------------------------------- 1 | import makeUniversalCreators from '../universal/makeUniversalCreators' 2 | import makeTypedCreators from '../create/makeTypedCreators' 3 | import { RiducerDict } from '../types' 4 | import makeCustomCreators from '../custom/makeCustomCreators' 5 | 6 | function wrapWithCreate< 7 | LeafT, 8 | TreeT, 9 | RiducerDictT extends RiducerDict 10 | >( 11 | leafState: LeafT, 12 | treeState: TreeT, 13 | riducerDict: RiducerDictT, 14 | path: (string | number)[] = [] 15 | ) { 16 | const universalCreators = makeUniversalCreators(leafState, path) 17 | const typedCreators = makeTypedCreators(leafState, path) 18 | const customCreators = makeCustomCreators(leafState, treeState, path, riducerDict) 19 | const makeCreators = (passedType?: string) => { 20 | return Object.assign( 21 | universalCreators(passedType), 22 | typedCreators(passedType), 23 | customCreators(passedType) 24 | ) 25 | } 26 | 27 | const create = Object.assign(makeCreators, makeCreators()) 28 | 29 | return Object.assign({ create }, leafState) 30 | } 31 | 32 | export default wrapWithCreate -------------------------------------------------------------------------------- /docs/old/defaults/toggle.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: toggle 3 | title: toggle 4 | hide_title: true 5 | sidebar_label: toggle 6 | --- 7 | 8 | # `toggle()` 9 | **`create.toggle`** 10 | **`create(actionType).toggle`** 11 | *Appropriate leaf state: boolean* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to update the leaf's state to `!leafState`. 14 | 15 | ## Returns 16 | `action` *(object)*: an object to dispatch to the store 17 | 18 | ## Example 19 | ```js 20 | import { createStore } from 'redux' 21 | import riduce from 'riduce' 22 | 23 | const initialState = { 24 | foo: true, 25 | bar: false 26 | } 27 | 28 | const [reducer, actions] = riduce(initialState) 29 | const store = createStore(reducer) 30 | ``` 31 | 32 | ### Calling `create.toggle` 33 | ```js 34 | const toggleFoo = actions.foo.create.toggle 35 | store.dispatch(toggleFoo()) 36 | console.log(store.getState().foo) // false 37 | ``` 38 | 39 | ### Calling `create(actionType).toggle` 40 | ```js 41 | const toggleBar = actions.bar.create('TOGGLE_BAR').toggle 42 | store.dispatch(toggleBar()) 43 | console.log(store.getState().bar) // true 44 | ``` -------------------------------------------------------------------------------- /src/boolean/makeBooleanCreators.ts: -------------------------------------------------------------------------------- 1 | import makeCreatorOfTypeFromPath from "../create/makeCreatorOfTypeFromPath"; 2 | import { CreateFn } from "../types"; 3 | import { BooleanCreators, BooleanCreatorKeys } from "./boolean-types"; 4 | 5 | function makeBooleanCreators( 6 | leafState: L, 7 | path: (string | number)[] 8 | ): CreateFn> { 9 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path); 10 | return (passedType?: string) => 11 | madeBooleanCreators(leafState, path, makeCreatorOfType, passedType); 12 | } 13 | 14 | export function madeBooleanCreators( 15 | leafState: L, 16 | path: (string | number)[], 17 | makeCreatorOfType: ReturnType, 18 | passedType?: string 19 | ): BooleanCreators { 20 | const creatorOfType = makeCreatorOfType(passedType); 21 | return { 22 | off: () => creatorOfType(BooleanCreatorKeys.OFF), 23 | on: () => creatorOfType(BooleanCreatorKeys.ON), 24 | toggle: () => creatorOfType(BooleanCreatorKeys.TOGGLE), 25 | }; 26 | } 27 | 28 | export default makeBooleanCreators; 29 | -------------------------------------------------------------------------------- /docs/old/defaults/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Default action creators 4 | hide_title: true 5 | sidebar_label: Overview 6 | --- 7 | 8 | # Default action creators 9 | 10 | All of the below action creators are availble through the [`create`](../api/create.md) API at any arbitrary leaf of [`actions`](../api/actions.md). 11 | 12 | Some are more useful depending on the type of leaf state you are operating with: 13 | 14 | ## any 15 | - [`.do(callback)`](do.md) 16 | - [`.clear([toNull = false])`](clear.md) 17 | - [`.reset()`](reset.md) 18 | - [`.update(value)`](update.md) 19 | 20 | ## array 21 | - [`.concat(array)`](concat.md) 22 | - [`.drop([n = 1])`](drop.md) 23 | - [`.filter(callback)`](filter.md) 24 | - [`.push(element, [index = -1], [replace = false])`](push.md) 25 | 26 | ## boolean 27 | - [`.off()`](off.md) 28 | - [`.on()`](on.md) 29 | - [`.toggle()`](toggle.md) 30 | 31 | # number 32 | - [`.increment([n = 1])`](increment.md) 33 | 34 | ## object 35 | - [`.assign(...sources)`](assign.md) 36 | - [`.path(path, value)`](path.md) 37 | - [`.set(key, value)`](set.md) 38 | 39 | ## string 40 | - [`.concat(string)`](concat.md) 41 | 42 | -------------------------------------------------------------------------------- /docs/old/defaults/push.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']]} 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Push default creator 14 | { 15 | // @dts-jest:fail does not exist on boolean state 16 | actions.bool.create.push 17 | 18 | // @dts-jest:pass exists on array state 19 | actions.arr.create.push 20 | 21 | // @dts-jest:pass exists on nested array state 22 | actions.obj.names[0].create.push 23 | 24 | // @dts-jest:fail requires an argument 25 | actions.arr.create.push() 26 | 27 | // @dts-jest:pass can take appropriate element type as argument 28 | actions.arr.create.push(2) 29 | 30 | // @dts-jest:fail refuses non-appropriate element types 31 | actions.arr.create.push('2') 32 | 33 | // @dts-jest:pass can take an index as a second argument 34 | actions.obj.names.create.push(['x', 'y', 'z'], 1) 35 | 36 | // @dts-jest:pass can take boolean as a third argument 37 | actions.obj.names.create.push(['x', 'y', 'z'], 1, true) 38 | } -------------------------------------------------------------------------------- /src/utils/array-utils.ts: -------------------------------------------------------------------------------- 1 | export const atIndex = (array: any[], index: number) => array[positiveIndex(array, index)]; 2 | 3 | export const deleteAtIndex = (arr: any[], idx: number) => [ 4 | ...arr.slice(0, positiveIndex(arr, idx)), 5 | ...arr.slice(positiveIndex(arr, idx) + 2) 6 | ] 7 | 8 | export const insertAtIndex = (arr: any[], idx: number, newVal: any) => ( 9 | idx < 0 10 | ? insertAfterIndex(arr, idx, newVal) 11 | : insertBeforeIndex(arr, idx, newVal) 12 | ) 13 | 14 | export const replaceAtIndex = (arr: any[], idx: number, newVal: any) => 15 | arr.map((value, index) => positiveIndex(arr, idx) === index ? newVal : value); 16 | 17 | const insertAfterIndex = (arr: any[], idx: number, newVal: any) => [ 18 | ...arr.slice(0, positiveIndex(arr, idx) + 1), 19 | newVal, 20 | ...arr.slice(positiveIndex(arr, idx) + 1) 21 | ]; 22 | 23 | const insertBeforeIndex = (arr: any[], idx: number, newVal: any) => [ 24 | ...arr.slice(0, positiveIndex(arr, idx)), 25 | newVal, 26 | ...arr.slice(positiveIndex(arr, idx)) 27 | ]; 28 | 29 | const positiveIndex = (array: any[], index: number | string) => 30 | index < 0 ? array.length + parseInt(`${index}`) : parseInt(`${index}`); -------------------------------------------------------------------------------- /docs/old/defaults/drop.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: drop 3 | title: drop 4 | hide_title: true 5 | sidebar_label: drop 6 | --- 7 | 8 | # `drop([n = 1])` 9 | **`create.drop`** 10 | **`create(actionType).drop`** 11 | *Appropriate leaf state: array* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively drop the first `n` elements from the leaf's state. 14 | 15 | ## Parameters 16 | - `n` *(number, optional)*: the number of elements to drop 17 | 18 | ## Returns 19 | `action` *(object)*: an object to dispatch to the `store` 20 | 21 | ## Example 22 | ```js 23 | import { createStore } from 'redux' 24 | import riduce from 'riduce' 25 | 26 | const initialState = { 27 | foo: ['a', 'b', 'c'], 28 | bar: ['a', 'b', 'c'] 29 | } 30 | 31 | const [reducer, actions] = riduce(initialState) 32 | const store = createStore(reducer) 33 | ``` 34 | ### No argument provided 35 | ```js 36 | const dropFromFoo = actions.foo.create.drop 37 | store.dispatch(dropFromFoo()) 38 | console.log(store.getState().foo) // ['b', 'c'] 39 | ``` 40 | 41 | ### Providing an argument 42 | ```js 43 | const dropFromBar = actions.bar.create.drop 44 | store.dispatch(dropFromBar(2)) 45 | console.log(store.getState().bar) // ['c'] 46 | ``` -------------------------------------------------------------------------------- /docs/old/defaults/increment.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: increment 3 | title: increment 4 | hide_title: true 5 | sidebar_label: increment 6 | --- 7 | 8 | # `increment([n = 1])` 9 | **`create.increment`** 10 | **`create(actionType).increment`** 11 | *Appropriate leaf state: number* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to increment the leaf's state by `n`. 14 | 15 | ## Parameters 16 | - `n` *(number, optional)*: the number to increment the leaf's state by, defaulting to 1 17 | 18 | ## Returns 19 | `action` *(object)*: an object to dispatch to the store 20 | 21 | ## Example 22 | ```js 23 | import { createStore } from 'redux' 24 | import riduce from 'riduce' 25 | 26 | const initialState = { 27 | foo: 5, 28 | bar: 5 29 | } 30 | 31 | const [reducer, actions] = riduce(initialState) 32 | const store = createStore(reducer) 33 | ``` 34 | ### No argument provided 35 | ```js 36 | const incrementFoo = actions.foo.create.increment 37 | store.dispatch(incrementFoo()) 38 | console.log(store.getState().foo) // 6 39 | ``` 40 | ### Providing an argument 41 | ```js 42 | const incrementBar = actions.bar.create.increment 43 | store.dispatch(incrementBar(37)) 44 | console.log(store.getState().bar) // 42 45 | ``` -------------------------------------------------------------------------------- /src/boolean/boolean-types.ts: -------------------------------------------------------------------------------- 1 | import { ActionWithPayload, Action } from "../types"; 2 | 3 | export enum BooleanCreatorKeys { 4 | OFF = "OFF", 5 | ON = "ON", 6 | TOGGLE = "TOGGLE", 7 | } 8 | 9 | export type BooleanCreators< 10 | LeafT extends boolean = boolean, 11 | TreeT = unknown 12 | > = { 13 | off(): Action; 14 | on(): Action; 15 | toggle(): Action; 16 | }; 17 | 18 | export type BooleanActions< 19 | KeyT extends keyof BooleanCreators, 20 | LeafT extends boolean = boolean, 21 | TreeT = unknown 22 | > = ReturnType[KeyT]>; 23 | 24 | export function isBooleanAction(action: Action): boolean { 25 | return isOffAction(action) || isOnAction(action) || isToggleAction(action); 26 | } 27 | 28 | export function isOffAction(action: Action): action is BooleanActions<"off"> { 29 | return action.leaf.CREATOR_KEY === BooleanCreatorKeys.OFF; 30 | } 31 | 32 | export function isOnAction(action: Action): action is BooleanActions<"on"> { 33 | return action.leaf.CREATOR_KEY === BooleanCreatorKeys.ON; 34 | } 35 | 36 | export function isToggleAction( 37 | action: Action 38 | ): action is BooleanActions<"toggle"> { 39 | return action.leaf.CREATOR_KEY === BooleanCreatorKeys.TOGGLE; 40 | } 41 | -------------------------------------------------------------------------------- /docs/old/defaults/concat.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | const initialState = { 4 | bool: false, 5 | num: 2, 6 | str: 'foo', 7 | arr: [1, 2, 3], 8 | obj: { num: 5, names: [['a', 'e'], ['b, c']]} 9 | } 10 | 11 | const [_, actions] = riduce(initialState) 12 | 13 | // @dts-jest:group Concat default creator 14 | { 15 | // @dts-jest:fail does not exist on boolean state 16 | actions.bool.create.concat 17 | 18 | // @dts-jest:pass exists on array state 19 | actions.arr.create.concat 20 | 21 | // @dts-jest:pass exists on nested array state 22 | actions.obj.names[0].create.concat 23 | 24 | // @dts-jest:pass exists on string state 25 | actions.str.create.concat 26 | 27 | // @dts-jest:pass exists on nested string state 28 | actions.obj.names[0][0].create.concat 29 | 30 | // @dts-jest:pass takes suitable array on an array 31 | actions.arr.create.concat([4, 5, 6]) 32 | 33 | // @dts-jest:fail type error on non-conforming array argument on array 34 | actions.arr.create.concat(['4']) 35 | 36 | // @dts-jest:fail type error on array argument on string 37 | actions.str.create.concat(['4']) 38 | 39 | // @dts-jest:pass takes string argument on a string 40 | actions.str.create.concat('4') 41 | } -------------------------------------------------------------------------------- /src/array/makeArrayCreators.ts: -------------------------------------------------------------------------------- 1 | import makeCreatorOfTypeFromPath from "../create/makeCreatorOfTypeFromPath"; 2 | import { CreateFn } from "../types"; 3 | import { ArrayCreators, ArrayCreatorKeys } from "./array-types"; 4 | 5 | function makeArrayCreators( 6 | leafState: L, 7 | path: (string | number)[] 8 | ): CreateFn> { 9 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path); 10 | return (passedType?: string) => 11 | madeArrayCreators(leafState, path, makeCreatorOfType, passedType); 12 | } 13 | 14 | export function madeArrayCreators( 15 | leafState: L, 16 | path: (string | number)[], 17 | makeCreatorOfType: ReturnType, 18 | passedType?: string 19 | ): ArrayCreators { 20 | const creatorOfType = makeCreatorOfType(passedType); 21 | return { 22 | concat: (arr: L) => creatorOfType(ArrayCreatorKeys.CONCAT, arr), 23 | drop: (n) => creatorOfType(ArrayCreatorKeys.DROP, n), 24 | filter: (cb) => creatorOfType(ArrayCreatorKeys.FILTER, cb), 25 | push: (element, index, replace) => 26 | creatorOfType(ArrayCreatorKeys.PUSH, { element, index, replace }), 27 | }; 28 | } 29 | 30 | export default makeArrayCreators; 31 | -------------------------------------------------------------------------------- /src/utils/update-state.ts: -------------------------------------------------------------------------------- 1 | import { path } from 'ramda' 2 | import produce from 'immer' 3 | 4 | export const getState = (state: S, propPath: (string | number)[]) => ( 5 | (pathIsEmpty(propPath)) 6 | ? state 7 | : path(propPath, state) 8 | ) 9 | 10 | export const pathIsEmpty = (propPath: string | (string | number)[]) => ( 11 | ['', null, undefined].includes(propPath as string) || propPath.length === 0 12 | ) 13 | 14 | export const setValue = (obj: S, propPath: (string | number)[], value: V) => { 15 | const pathTo = [...propPath] 16 | const finalProp = pathTo.pop() 17 | let currentObj = obj 18 | 19 | pathTo.forEach(prop => { 20 | // @ts-ignore 21 | if (currentObj[prop] == null) { 22 | // @ts-ignore 23 | currentObj[prop] = {} 24 | } 25 | // @ts-ignore 26 | currentObj = currentObj[prop]; 27 | }) 28 | 29 | // @ts-ignore 30 | currentObj[finalProp as string | number] = value; 31 | } 32 | 33 | export const updateState = (state: S, propPath: (string | number)[], val: V): S => { 34 | if (pathIsEmpty(propPath)) return val as unknown as S 35 | return produce(state, (draftState) => { 36 | setValue(draftState, propPath, val) 37 | }) 38 | } 39 | 40 | export default updateState -------------------------------------------------------------------------------- /docs/old/defaults/push.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.push(element, [index = -1], [replace = false]): returns an action that, when dispatched, updates the leaf's state by non-mutatively pushing element into leaf's state at index. If replace === true, then element replaces the existing element with that index.", () => { 5 | const initialState = { 6 | foo: [1, 2, 3], 7 | bar: [1, 2, 3], 8 | foobar: [1, 2, 3] 9 | } 10 | 11 | const [reducer, actions] = riduce(initialState) 12 | const store = createStore(reducer) 13 | 14 | test("Providing element", () => { 15 | const pushToFoo = actions.foo.create.push 16 | store.dispatch(pushToFoo(4)) 17 | expect(store.getState().foo).toEqual([1, 2, 3, 4]) 18 | }) 19 | 20 | test("Providing element and index", () => { 21 | const pushToBar = actions.bar.create.push 22 | store.dispatch(pushToBar(4, 0)) 23 | expect(store.getState().bar).toEqual([4, 1, 2, 3]) 24 | }) 25 | 26 | test("Providing element, index and replace", () => { 27 | const pushToFoobar = actions.foobar.create.push 28 | store.dispatch(pushToFoobar(4, 0, true)) 29 | expect(store.getState().foobar).toEqual([4, 2, 3]) 30 | }) 31 | }) -------------------------------------------------------------------------------- /src/object/makeObjectCreators.ts: -------------------------------------------------------------------------------- 1 | import makeCreatorOfTypeFromPath from "../create/makeCreatorOfTypeFromPath"; 2 | import { CreateFn } from "../types"; 3 | import { ObjectCreators, ObjectCreatorKeys } from "./object-types"; 4 | 5 | function makeObjectCreators( 6 | leafState: L, 7 | path: (string | number)[] 8 | ): CreateFn> { 9 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path); 10 | return (passedType?: string) => 11 | madeObjectCreators(leafState, path, makeCreatorOfType, passedType); 12 | } 13 | 14 | export function madeObjectCreators( 15 | leafState: L, 16 | path: (string | number)[], 17 | makeCreatorOfType: ReturnType, 18 | passedType?: string 19 | ): ObjectCreators { 20 | const creatorOfType = makeCreatorOfType(passedType); 21 | return { 22 | assign: (props: L) => creatorOfType(ObjectCreatorKeys.ASSIGN, props), 23 | path: (route, value) => 24 | creatorOfType(ObjectCreatorKeys.PATH, { path: route, value }), 25 | pushedSet: (arg: any) => creatorOfType(ObjectCreatorKeys.PUSHED_SET, arg), 26 | set: (key, value) => creatorOfType(ObjectCreatorKeys.SET, { key, value }), 27 | }; 28 | } 29 | 30 | export default makeObjectCreators; 31 | -------------------------------------------------------------------------------- /docs/old/defaults/set.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: set 3 | title: set 4 | hide_title: true 5 | sidebar_label: set 6 | --- 7 | 8 | # `set(key, value)` 9 | **`create.set`** 10 | **`create(actionType).set`** 11 | *Appropriate leaf type: object* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively update the leaf's state at the property `key` with `value`. 14 | 15 | ## Parameters 16 | - `key` *(string)*: the path of the property to set 17 | - `value` *(any)*: the value to set 18 | 19 | ## Returns 20 | `action` *(object)*: an object to dispatch to the store 21 | 22 | ## Example 23 | ```js 24 | import { createStore } from 'redux' 25 | import riduce from 'riduce' 26 | 27 | const initialState = { 28 | foo: {}, 29 | bar: { props: true } 30 | } 31 | 32 | const [reducer, actions] = riduce(initialState) 33 | const store = createStore(reducer) 34 | ``` 35 | 36 | ### Setting a new property 37 | ```js 38 | const setInFoo = actions.foo.create.set 39 | store.dispatch(setInFoo('accessed', true)) 40 | console.log(store.getState().foo) // { accessed: true } 41 | ``` 42 | 43 | ### Overwriting a property 44 | ```js 45 | const setInBar = actions.bar.create.set 46 | store.dispatch(setInBar('props', false)) 47 | console.log(store.getState().bar) // { props: false } 48 | ``` -------------------------------------------------------------------------------- /docs/old/defaults/concat.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: concat 3 | title: concat 4 | hide_title: true 5 | sidebar_label: concat 6 | --- 7 | 8 | # `concat(arrayOrString)` 9 | **`create.concat`** 10 | **`create(actionType).concat`** 11 | *Appropriate leaf state: array | string* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to update the leaf's state by concatening it with `arrayOrString`. 14 | 15 | ## Parameters 16 | - `arrayOrString` *(array | string)*: the array to concatenate 17 | 18 | ## Returns 19 | `action` *(object)*: an object to dispatch to the `store` 20 | 21 | ## Example 22 | ```js 23 | import { createStore } from 'redux' 24 | import riduce from 'riduce' 25 | 26 | const initialState = { 27 | arr: [1, 2, 3], 28 | str: 'foo' 29 | } 30 | 31 | const [reducer, actions] = riduce(initialState) 32 | const store = createStore(reducer) 33 | ``` 34 | 35 | ### Concatenating an array 36 | ```js 37 | const concatToArr = actions.arr.create.concat 38 | store.dispatch(concatToArr(['a', 'b', 'c'])) 39 | console.log(store.getState().arr) // [1, 2, 3, 'a', 'b', 'c'] 40 | ``` 41 | 42 | ### Concatenating a string 43 | ```js 44 | const concatToStr = actions.str.create.concat 45 | store.dispatch(concatToStr('bar')) 46 | console.log(store.getState().str) // 'foobar' 47 | ``` 48 | -------------------------------------------------------------------------------- /src/README.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | import riduce from '.' 3 | 4 | describe('README', () => { 5 | const museumState = { 6 | isOpen: false, 7 | visitor: { 8 | counter: 0, 9 | guestbook: ['richard woz here'] 10 | } 11 | } 12 | 13 | const [reducer, actions] = riduce(museumState) 14 | const { getState, dispatch } = createStore(reducer) 15 | 16 | test('Scalable state management', () => { 17 | // at `state.isOpen`, create an action to toggle the boolean 18 | dispatch(actions.isOpen.create.toggle()) 19 | 20 | // at `state.vistor.counter`, create an action to add 5 21 | dispatch(actions.visitor.counter.create.increment(5)) 22 | 23 | // at `state.vistor.guestbook`, create an action to push a string 24 | dispatch(actions.visitor.guestbook.create.push('LOL from js fan')) 25 | 26 | // at `state.visitor.guestbook[0]`, create an action to concat a string 27 | dispatch(actions.visitor.guestbook[0].create.concat('!!!')) 28 | 29 | const result = getState() 30 | expect(result).toStrictEqual({ 31 | isOpen: true, 32 | visitor: { 33 | counter: 5, 34 | guestbook: [ 35 | 'richard woz here!!!', 36 | 'LOL from js fan' 37 | ] 38 | } 39 | }) 40 | }) 41 | }) -------------------------------------------------------------------------------- /docs/old/api/actions.spec.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | describe('actions can take an arbitrary path of properties after it', () => { 4 | const initialState = { 5 | counter: 0, 6 | arbitrary: { 7 | nested: { 8 | path: ['hi!'] 9 | } 10 | } 11 | } 12 | 13 | const [reducer, actions] = riduce(initialState) 14 | 15 | test('Any arbitrary path returns an object', () => { 16 | expect(typeof actions.counter).toBe('object') 17 | expect(typeof actions.arbitrary.nested).toBe('object') 18 | expect(typeof actions.arbitrary.nested.path).toBe('object') 19 | // @ts-ignore 20 | expect(typeof actions.not.in.my.initial.state).toBe('object') 21 | }) 22 | 23 | test('Any arbitrary path has the create function property', () => { 24 | expect(typeof actions.create).toBe('function') 25 | expect(typeof actions.counter.create).toBe('function') 26 | expect(typeof actions.arbitrary.nested.create).toBe('function') 27 | expect(typeof actions.arbitrary.nested.path.create).toBe('function') 28 | // @ts-ignore 29 | expect(typeof actions.not.in.my.initial.state.create).toBe('function') 30 | }) 31 | 32 | test("You can't access arbitrary paths from create", () => { 33 | // @ts-ignore 34 | expect(typeof actions.create.arbitrary).toBe('undefined') 35 | }) 36 | }) -------------------------------------------------------------------------------- /docs/old/defaults/noop.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from "../../../src"; 2 | 3 | // @dts-jest:group Explicit state typing 4 | { 5 | interface State { 6 | bool: boolean; 7 | num: number; 8 | str: string; 9 | arr: { number: number }[]; 10 | random?: any; 11 | } 12 | 13 | const initialState: State = { 14 | bool: false, 15 | num: 2, 16 | str: "foo", 17 | arr: [{ number: 4 }, { number: 2 }, { number: 7 }], 18 | }; 19 | 20 | const [_, actions] = riduce(initialState); 21 | 22 | // @dts-jest:pass Root state can take noop 23 | actions.create.noop(); 24 | 25 | // @dts-jest:pass Exists on all leaves 26 | actions.bool.create.noop(); 27 | actions.num.create.noop(); 28 | actions.str.create.noop(); 29 | actions.arr[0].number.create.noop(); 30 | } 31 | 32 | // @dts-jest:group Inferred state typing 33 | { 34 | const initialState = { 35 | bool: false, 36 | num: 2, 37 | str: "foo", 38 | arr: [{ number: 4 }, { number: 2 }, { number: 7 }], 39 | }; 40 | 41 | const [_, actions] = riduce(initialState); 42 | 43 | // @dts-jest:pass Root state can take noop 44 | actions.create.noop(); 45 | 46 | // @dts-jest:pass Exists on all leaves 47 | actions.bool.create.noop(); 48 | actions.num.create.noop(); 49 | actions.str.create.noop(); 50 | actions.arr[0].number.create.noop(); 51 | } 52 | -------------------------------------------------------------------------------- /src/array/arrayLeafReducer.ts: -------------------------------------------------------------------------------- 1 | import { drop, defaultTo } from 'ramda'; 2 | import { isDropAction, isConcatActionArray, isFilterAction, isPushAction } from './array-types' 3 | import { Action, isClearAction } from "../types" 4 | import universalLeafReducer from '../universal/universalLeafReducer'; 5 | import { replaceAtIndex, insertAtIndex } from '../utils/array-utils'; 6 | 7 | function arrayLeafReducer(leafState: L, treeState: T, action: A, originalState: T): L { 8 | 9 | if (isDropAction(action)) { 10 | return drop(defaultTo(1, action.payload), leafState) as L 11 | } 12 | 13 | if (isConcatActionArray(action)) { 14 | return [...leafState, ...action.payload] as L 15 | } 16 | 17 | if (isPushAction(action)) { 18 | const { element, index = -1, replace = false } = action.payload 19 | return replace 20 | ? replaceAtIndex(leafState, index, element) as L 21 | : insertAtIndex(leafState, index, element) as L 22 | } 23 | 24 | if (isFilterAction(action)) { 25 | // @ts-ignore 26 | return leafState.filter(action.payload) as L 27 | } 28 | 29 | if (isClearAction(action)) { 30 | // @ts-ignore TODO fix 31 | return [] 32 | } 33 | 34 | return universalLeafReducer(leafState, treeState, action, originalState) 35 | } 36 | 37 | export default arrayLeafReducer -------------------------------------------------------------------------------- /docs/old/defaults/pushedSet.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | // @dts-jest:group Explicitly typed state 4 | { 5 | interface State { 6 | num: number, 7 | foo: { [key: string]: string }, 8 | bar: { [key: string]: { id: string, text: string } } 9 | } 10 | 11 | const initialState: State = { 12 | num: 100, 13 | foo: {}, 14 | bar: {} 15 | } 16 | 17 | const [_, actions] = riduce(initialState) 18 | 19 | // @dts-jest:fail does not exist on number state 20 | actions.num.create.pushedSet 21 | 22 | // @dts-jest:pass exists on object state 23 | actions.foo.create.pushedSet 24 | 25 | // @dts-jest:fail refuses inappropriate value 26 | actions.foo.create.pushedSet(4) 27 | 28 | // @dts-jest:pass accepts appropriate value 29 | actions.foo.create.pushedSet('4') 30 | 31 | // @dts-jest:pass accepts a callback with string argument 32 | actions.foo.create.pushedSet(id => id.toUpperCase()) 33 | 34 | // @dts-jest:fail reject a callback with array argument 35 | actions.foo.create.pushedSet((id: string[]) => id.pop()) 36 | 37 | // @dts-jest:pass accepts a callback returning correct shape 38 | actions.bar.create.pushedSet(id => ({ id, text: 'hello world!' })) 39 | 40 | // @dts-jest:fail refuses callback returning incorrect shape 41 | actions.bar.create.pushedSet(id => ({ id, text: 1010 })) 42 | } -------------------------------------------------------------------------------- /docs/old/defaults/reset.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.reset(): returns an action that, when dispatched, updates the leaf's state to the reducer's initialised state", () => { 5 | const initialState = { 6 | num: 2, 7 | arr: [1, 2, 3], 8 | bool: true 9 | } 10 | 11 | const otherState = { 12 | num: 11, 13 | arr: [4, 5, 6], 14 | bool: false 15 | } 16 | 17 | const [reducer, actions] = riduce(initialState) 18 | const store = createStore(reducer, otherState) 19 | 20 | test("State is the preloaded state", () => { 21 | expect(store.getState()).toEqual(otherState) 22 | }) 23 | 24 | test("Calling create.reset on a leaf", () => { 25 | const resetNum = actions.num.create.reset 26 | store.dispatch(resetNum()) 27 | expect(store.getState().num).toBe(2) 28 | }) 29 | 30 | test("Calling create(actionType).reset on a leaf", () => { 31 | const resetBool = actions.bool.create.reset 32 | store.dispatch(resetBool()) 33 | expect(store.getState().bool).toBe(true) 34 | }) 35 | 36 | 37 | test("Calling create.reset on a branch", () => { 38 | const resetState = actions.create.reset 39 | store.dispatch(resetState()) 40 | expect(store.getState()).toEqual({ 41 | num: 2, 42 | arr: [1, 2, 3], 43 | bool: true 44 | }) 45 | }) 46 | }) -------------------------------------------------------------------------------- /src/create/makeCreatorOfTypeFromPath.ts: -------------------------------------------------------------------------------- 1 | import { isNotUndefined } from "ramda-adjunct"; 2 | import { camelCase, constantCase } from 'change-case'; 3 | import { ActionWithPayload, Action } from "../types" 4 | 5 | const makeCreatorOfTypeFromPath = (path: (string | number)[], custom: boolean = false) => (passedType?: string) => { 6 | const makeType = passedType 7 | ? (_: string) => passedType 8 | : (str: string) => [...path, constantCase(str)].join('/') 9 | 10 | function creatorOfType< 11 | PayloadT, 12 | CreatorKeyT extends string = string 13 | >( 14 | str: CreatorKeyT, 15 | payload: PayloadT, 16 | typeOverload?: string 17 | ): ActionWithPayload 18 | 19 | function creatorOfType< 20 | PayloadT, 21 | CreatorKeyT extends string = string 22 | >(str: CreatorKeyT): Action 23 | 24 | function creatorOfType< 25 | PayloadT, 26 | CreatorKeyT extends string = string 27 | >(str: CreatorKeyT, payload?: PayloadT, typeOverload?: string) { 28 | return { 29 | type: typeOverload || makeType(str), 30 | leaf: { 31 | path, 32 | CREATOR_KEY: constantCase(str), 33 | creatorKey: camelCase(str), 34 | custom 35 | }, 36 | ...isNotUndefined(payload) ? { payload } : {} 37 | } 38 | } 39 | 40 | return creatorOfType 41 | } 42 | 43 | export default makeCreatorOfTypeFromPath -------------------------------------------------------------------------------- /docs/old/defaults/path.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: path 3 | title: path 4 | hide_title: true 5 | sidebar_label: path 6 | --- 7 | 8 | # `path(path, value)` 9 | **`create.path`** 10 | **`create(actionType).path`** 11 | *Appropriate leaf type: object* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively set a property at `path` from the leaf as `value`. 14 | 15 | ## Parameters 16 | - `path` *(string[])*: an array of strings which represent the property path to set at 17 | - `value` *(any)*: the value to set 18 | 19 | ## Returns 20 | `action` *(object)*: an object to dispatch to the store 21 | 22 | ## Example 23 | ```js 24 | import { createStore } from 'redux' 25 | import riduce from 'riduce' 26 | 27 | const initialState = { 28 | foo: {} 29 | bar: { arbitrary: { keys: 3 } } 30 | } 31 | 32 | const [reducer, actions] = riduce(initialState) 33 | const store = createStore(reducer) 34 | ``` 35 | 36 | ### Setting a new property 37 | ```js 38 | const setAtPathInFoo = actions.foo.create.path 39 | store.dispatch(setAtPathInFoo(['nested', 'deep'], true)) 40 | console.log(store.getState().foo) // { nested: { deep: true } } 41 | 42 | ``` 43 | ### Overwriting a property 44 | ```js 45 | const setAtPathInBar = actions.bar.create("SET_AT_PATH_IN_BAR").path 46 | store.dispatch(setAtPathInBar(['arbitrary', 'keys'], 5)) 47 | console.log(store.getState().bar) // { arbitrary: { keys: 5 } } 48 | ``` -------------------------------------------------------------------------------- /src/object/objectLeafReducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, isClearAction } from "../types" 2 | import universalLeafReducer from '../universal/universalLeafReducer'; 3 | import { isAssignAction, isPathAction, isPushedSetAction, isPushedSetCallbackAction, isPushedSetValueAction, isSetAction } from './object-types'; 4 | import updateState from "../utils/update-state"; 5 | import generatePushID from "../utils/generatePushID"; 6 | 7 | function objectLeafReducer(leafState: L, treeState: T, action: A, originalState: T): L { 8 | if (isAssignAction(action)) { 9 | return { 10 | ...leafState, 11 | ...action.payload 12 | } 13 | } 14 | 15 | if (isPathAction(action)) { 16 | return updateState(leafState, action.payload.path, action.payload.value) 17 | } 18 | 19 | if (isPushedSetValueAction(action)) { 20 | const id = generatePushID() 21 | return { ...leafState, [id]: action.payload } 22 | } 23 | 24 | if (isPushedSetCallbackAction(action)) { 25 | const id = generatePushID() 26 | return { ...leafState, [id]: action.payload(id) } 27 | } 28 | 29 | if (isSetAction(action)) { 30 | const { key, value } = action.payload 31 | return { ...leafState, [key]: value } 32 | } 33 | 34 | if (isClearAction(action)) { 35 | return {} as L 36 | } 37 | 38 | 39 | return universalLeafReducer(leafState, treeState, action, originalState) 40 | } 41 | 42 | export default objectLeafReducer -------------------------------------------------------------------------------- /docs/old/defaults/assign.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: assign 3 | title: assign 4 | hide_title: true 5 | sidebar_label: assign 6 | --- 7 | 8 | # `assign(...sources)` 9 | **`create.assign`** 10 | **`create(actionType).assign`** 11 | 12 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively copy all properties from `sources` into the leaf's state. 13 | 14 | (This is essentially a convenience wrapper on top of the vanilla JavaScript [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).) 15 | 16 | ## Parameters 17 | - `sources` *(...objects)*: the path of the property to set 18 | 19 | ## Returns 20 | `action` *(object)*: an object to dispatch to the store 21 | 22 | ## Example 23 | ```js 24 | import { createStore } from 'redux' 25 | import riduce from 'riduce' 26 | 27 | const initialState = { 28 | foo: { props: true }, 29 | bar: { props: false } 30 | } 31 | 32 | const [reducer, actions] = riduce(initialState) 33 | const store = createStore(reducer) 34 | ``` 35 | 36 | ### Assigning new properties 37 | ```js 38 | const assignToFoo = actions.foo.create.assign 39 | store.dispatch(assignToFoo({ count: 2 })) 40 | console.log(store.getState().foo) // { props: true, count: 2 } 41 | ``` 42 | ### Overwriting properties 43 | ```js 44 | const assignToBar = actions.bar.create.assign 45 | store.dispatch(assignToBar({ props: true })) 46 | console.log(store.getState().bar) // { props: true } 47 | ``` -------------------------------------------------------------------------------- /docs/old/defaults/pushedSet.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.set(path, value): returns an action that, when dispatched, updates the leaf's state at an auto-generated key (that orders chronologically after previous keys) with value", () => { 5 | interface State { 6 | foo: { [key: string]: string }, 7 | bar: { [key: string]: { id: string, text: string } } 8 | } 9 | 10 | const initialState: State = { 11 | foo: {}, 12 | bar: {} 13 | } 14 | 15 | const [reducer, actions] = riduce(initialState) 16 | const store = createStore(reducer) 17 | 18 | test("Passing a value", () => { 19 | const pushedSetInFoo = actions.foo.create.pushedSet 20 | store.dispatch(pushedSetInFoo('my first item')) 21 | store.dispatch(pushedSetInFoo('my second item')) 22 | expect(Object.values(store.getState().foo)).toEqual(['my first item', 'my second item']) 23 | }) 24 | 25 | test("Passing a callback", () => { 26 | const pushedSetInBar = actions.bar.create.pushedSet 27 | store.dispatch(pushedSetInBar((key: string) => ({ id: key, text: 'my first item' }))) 28 | const barState = store.getState().bar 29 | expect(Object.values(barState)[0]).toStrictEqual({ 30 | id: Object.keys(barState)[0], 31 | text: 'my first item' 32 | }) 33 | // expect(Object.values(barState)[0]).toHaveProperty('id', Object.keys(barState)[0]) 34 | // expect(Object.values(barState)[0]).toHaveProperty('text', 'my first item') 35 | }) 36 | }) -------------------------------------------------------------------------------- /src/custom/makeCustomCreators.ts: -------------------------------------------------------------------------------- 1 | import makeCreatorOfTypeFromPath from '../create/makeCreatorOfTypeFromPath'; 2 | import { CreateFn, RiducerDict, CustomCreators, isLonghandReducer } from '../types'; 3 | 4 | function makeCustomCreators< 5 | LeafT, 6 | TreeT, 7 | RiducerDictT extends RiducerDict 8 | >( 9 | leafState: LeafT, 10 | treeState: TreeT, 11 | path: (string | number)[], 12 | riducerDict: RiducerDictT 13 | ): CreateFn> { 14 | 15 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path, true) 16 | 17 | return (passedType?: string) => { 18 | const creatorOfType = makeCreatorOfType(passedType) 19 | 20 | const entries = Object.entries(riducerDict) 21 | 22 | const creators = entries.reduce( 23 | (acc, [key, definition]) => { 24 | if (isLonghandReducer(definition)) { 25 | const { argsToPayload, type } = definition 26 | return { 27 | ...acc, 28 | [key]: (...args: Parameters) => creatorOfType(key, argsToPayload(...args), type) 29 | } 30 | } else { 31 | const argsToPayload = (first: any) => first 32 | return { 33 | ...acc, 34 | [key]: (...args: Parameters) => creatorOfType(key, argsToPayload(...args)) 35 | } 36 | } 37 | }, 38 | {} 39 | ) 40 | 41 | return creators as CustomCreators 42 | } 43 | } 44 | 45 | export default makeCustomCreators -------------------------------------------------------------------------------- /docs/old/defaults/filter.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: filter 3 | title: filter 4 | hide_title: true 5 | sidebar_label: filter 6 | --- 7 | 8 | # `filter(callback)` 9 | **`create.filter`** 10 | **`create(actionType).filter`** 11 | *Appropriate leaf state: array* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively update the leaf's state by selecting elements that return true when passed to `callback`. 14 | 15 | (Effectively, this uses the vanilla javascript [`Array.prototype.filter(callback)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) API.) 16 | 17 | ## Parameters 18 | - `callback` *(function)*: the callback function to test each element with 19 | 20 | ## Returns 21 | `action` *(object)*: an object to dispatch to the `store` 22 | 23 | ## Example 24 | ```js 25 | import { createStore } from 'redux' 26 | import riduce from 'riduce' 27 | 28 | const initialState = { 29 | foo: [1, 2, 3, 4, 5], 30 | bar: ['cat', 'dog', 'bat'] 31 | } 32 | 33 | const [reducer, actions] = riduce(initialState) 34 | const store = createStore(reducer) 35 | ``` 36 | 37 | ### Calling create.filter 38 | ```js 39 | const filterFoo = actions.foo.create.filter 40 | store.dispatch(filterFoo(e => !(e % 2))) 41 | console.log(store.getState().foo) // [2, 4] 42 | ``` 43 | 44 | ### Calling create(actionType).filter 45 | ```js 46 | const filterBar = actions.bar.create('FILTER_BAR').filter 47 | store.dispatch(filterBar(e => e.includes('at'))) 48 | console.log(store.getState().bar) // ['cat', 'bat'] 49 | ``` -------------------------------------------------------------------------------- /docs/old/motivation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: motivation 3 | title: Motivation 4 | hide_title: true 5 | sidebar_label: Motivation 6 | --- 7 | 8 | # Motivation 9 | 10 | ## Problem 11 | 12 | [Redux](https://redux.js.org/) and [Redux DevTools](https://github.com/zalmoxisus/redux-devtools-extension) both work great for following what is happening in your app.1 13 | 14 | However, there are three pain points that at least one developer has encountered: 15 | 16 | 1. **Ugly boilerplate maintenance**: one more slice of state = another load of action types, creators and reducers to write. 17 | 2. **Unhelpfully named constants**: what was `NONTRIVIAL_THING_HAPPENED` meant to do, again...? 18 | 3. **Repetitive reducer logic**: an action that updates some slice of state to `true`? *How novel!* 19 | 20 | 1 *cf. what you* intended *to happen in your app...* 21 | 22 | ## Solution 23 | 24 | `redux-leaves` is a library that is written to provide: 25 | 26 | 1. **Pleasingly little boilerplate**: set up your reducer and actions in one line 27 | ```js 28 | const [reducer, actions] = riduce(initialState) 29 | ``` 30 | 31 | 2. **Precise updates**: easily increment that counter, no matter how deeply you nested it 32 | ```js 33 | dispatch(actions.distressingly.and.foolishly.deeply.nested.counter.create.increment(2)) 34 | ``` 35 | 3. **Predictable changes**: understand exactly what's happening with clear and consistently named action types: 36 | ```js 37 | // action type dispatched above: 38 | 'distressingly/and/foolishly/deeply/nested/counter/asNumber.INCREMENT' 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/old/defaults/reset.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: reset 3 | title: reset 4 | hide_title: true 5 | sidebar_label: reset 6 | --- 7 | 8 | # `reset()` 9 | **`create.reset`** 10 | **`create(actionType).reset`** 11 | *Appropriate leaf state: any* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively reset the leaf's state to its initial state as passed into `riduce`. 14 | 15 | ## Returns 16 | `action` *(object)*: an object to dispatch to the store 17 | 18 | ## Example 19 | ```js 20 | import { createStore } from 'redux' 21 | import riduce from 'riduce' 22 | 23 | const initialState = { 24 | num: 2, 25 | arr: [1, 2, 3], 26 | bool: true 27 | } 28 | 29 | const otherState = { 30 | num: 11, 31 | arr: [4, 5, 6], 32 | bool: false 33 | } 34 | 35 | const [reducer, actions] = riduce(initialState) 36 | const store = createStore(reducer, otherState) // preloads otherState 37 | 38 | /* store.getState() 39 | * { 40 | * num: 11, 41 | * arr: [4, 5, 6] 42 | * } 43 | */ 44 | 45 | ``` 46 | 47 | ### Calling `create.reset` on a leaf: 48 | ```js 49 | const resetNum = actions.num.create.reset 50 | store.dispatch(resetNum()) 51 | console.log(store.getState().num) // 2 52 | ``` 53 | 54 | ### Calling `create(actionType).reset` on a leaf: 55 | ```js 56 | const resetBool = actions.bool.create.reset 57 | store.dispatch(resetBool()) 58 | console.log(store.getState().bool) // true 59 | ``` 60 | 61 | ### Calling `create.reset` on a branch: 62 | ```js 63 | const resetState = actions.create.reset 64 | store.dispatch(resetState()) 65 | console.log(store.getState()) // { num: 2, arr: [1, 2, 3], bool: true } 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/old/defaults/update.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.update(value): returns an action that, when dispatched, updates the leaf's state to value", () => { 5 | 6 | const initialState = { 7 | bool: false, 8 | num: 2, 9 | str: 'foo', 10 | arr: [{ number: 4 }, { number: 2 }, { number: 7 }] 11 | } 12 | 13 | const [reducer, actions] = riduce(initialState) 14 | const store = createStore(reducer) 15 | 16 | test('Calling create.update on a leaf', () => { 17 | const updateStr = actions.str.create.update 18 | store.dispatch(updateStr("I can put anything here")) 19 | expect(store.getState().str).toBe('I can put anything here') 20 | }) 21 | 22 | test('Calling create.update on an array element', () => { 23 | const updateFirstElementOfArr = actions.arr[0].create.update 24 | store.dispatch(updateFirstElementOfArr({ number: 1 })) 25 | expect(store.getState().arr).toEqual([{ number: 1 }, { number: 2 }, { number: 7 }]) 26 | }) 27 | 28 | test('Calling create.update within an array element', () => { 29 | // @ts-ignore 30 | const updateSecondElementNumberProp = actions.arr[1].number.create.update 31 | store.dispatch(updateSecondElementNumberProp(1337)) 32 | expect(store.getState().arr).toEqual([{ number: 1 }, { number: 1337 }, { number: 7 }]) 33 | }) 34 | 35 | test('Calling create.update on a branch', () => { 36 | const updateState = actions.create.update 37 | // @ts-ignore 38 | store.dispatch(updateState({ any: { properties: true } })) 39 | expect(store.getState()).toEqual({ any: { properties: true } }) 40 | }) 41 | }) -------------------------------------------------------------------------------- /docs/old/defaults/clear.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.clear(toNull = false): returns an action that, when dispatched, clear's the leaf's state", () => { 5 | const initialState = { 6 | bool: true, 7 | num: 2, 8 | str: 'foo', 9 | arr: [1, 2, 3] 10 | } 11 | 12 | const [reducer, actions] = riduce(initialState) 13 | const store = createStore(reducer) 14 | 15 | describe('Boolean state', () => { 16 | const clearBool = actions.bool.create.clear 17 | 18 | it('Clears to false', () => { 19 | store.dispatch(clearBool()) 20 | expect(store.getState().bool).toBe(false) 21 | }) 22 | }) 23 | 24 | describe('Number state', () => { 25 | const clearNum = actions.num.create.clear 26 | 27 | it('Clears to 0', () => { 28 | store.dispatch(clearNum()) 29 | expect(store.getState().num).toBe(0) 30 | }) 31 | }) 32 | 33 | describe('String state', () => { 34 | const clearStr = actions.str.create.clear 35 | 36 | it("Clears to ''", () => { 37 | store.dispatch(clearStr()) 38 | expect(store.getState().str).toBe('') 39 | }) 40 | }) 41 | 42 | describe('Array state', () => { 43 | const clearArr = actions.arr.create.clear 44 | 45 | it("Clears to []", () => { 46 | store.dispatch(clearArr()) 47 | expect(store.getState().arr).toEqual([]) 48 | }) 49 | }) 50 | 51 | describe('Object state', () => { 52 | const clearState = actions.create.clear 53 | 54 | it("Clears to {}", () => { 55 | store.dispatch(clearState()) 56 | expect(store.getState()).toEqual({}) 57 | }) 58 | }) 59 | }) -------------------------------------------------------------------------------- /src/types/creator-types.ts: -------------------------------------------------------------------------------- 1 | import { ArrayCreators } from "../array/array-types"; 2 | import { UniversalCreators } from "../universal/universal-types"; 3 | import { StringCreators } from "../string/string-types"; 4 | import { ObjectCreators } from "../object/object-types"; 5 | import { NumberCreators } from "../number/number-types"; 6 | import { BooleanCreators } from "../boolean/boolean-types"; 7 | import { RiducerDict, CustomCreators } from "../custom/custom-types"; 8 | 9 | export * from "../universal/universal-types"; 10 | export * from "../custom/custom-types"; 11 | 12 | export type CreateFn = (passedType?: string) => T; 13 | 14 | export type WrappedWithCreate = T & { create: CreateFn }; 15 | 16 | export type CreateAPI< 17 | LeafT, 18 | TreeT, 19 | RiducerDictT extends RiducerDict 20 | > = Creators & 21 | CreateFn>; 22 | 23 | export type TypedCreators< 24 | LeafT, 25 | TreeT 26 | > = NonNullable extends Array 27 | ? ArrayCreators, TreeT> 28 | : NonNullable extends number 29 | ? NumberCreators, TreeT> 30 | : NonNullable extends string 31 | ? StringCreators, TreeT> 32 | : NonNullable extends boolean 33 | ? BooleanCreators, TreeT> 34 | : NonNullable extends {} 35 | ? ObjectCreators, TreeT> 36 | : {}; 37 | 38 | export type Creators< 39 | LeafT, 40 | TreeT, 41 | RiducerDictT extends RiducerDict 42 | > = UniversalCreators & 43 | TypedCreators & 44 | CustomCreators; 45 | -------------------------------------------------------------------------------- /src/undefined/undefinedLeafReducer.ts: -------------------------------------------------------------------------------- 1 | import { isArrayAction } from "../array/array-types"; 2 | import arrayLeafReducer from "../array/arrayLeafReducer"; 3 | import { isBooleanAction } from "../boolean/boolean-types"; 4 | import booleanLeafReducer from "../boolean/booleanLeafReducer"; 5 | import { isNumberAction } from "../number/number-types"; 6 | import numberLeafReducer from "../number/numberLeafReducer"; 7 | import { isObjectAction } from "../object/object-types"; 8 | import objectLeafReducer from "../object/objectLeafReducer"; 9 | import { isStringAction } from "../string/string-types"; 10 | import stringLeafReducer from "../string/stringLeafReducer"; 11 | import { Action } from "../types"; 12 | import universalLeafReducer from "../universal/universalLeafReducer"; 13 | 14 | function undefinedLeafReducer( 15 | leafState: L, 16 | treeState: T, 17 | action: A, 18 | originalState: T 19 | ): any { 20 | if (isArrayAction(action)) { 21 | return arrayLeafReducer([], treeState, action, originalState); 22 | } 23 | 24 | if (isBooleanAction(action)) { 25 | return booleanLeafReducer(false, treeState, action, originalState); 26 | } 27 | 28 | if (isNumberAction(action)) { 29 | return numberLeafReducer(0, treeState, action, originalState); 30 | } 31 | 32 | if (isObjectAction(action)) { 33 | return objectLeafReducer({}, treeState, action, originalState); 34 | } 35 | 36 | if (isStringAction(action)) { 37 | return stringLeafReducer("", treeState, action, originalState); 38 | } 39 | 40 | return universalLeafReducer(leafState, treeState, action, originalState); 41 | } 42 | 43 | export default undefinedLeafReducer; 44 | -------------------------------------------------------------------------------- /src/types/action-types.ts: -------------------------------------------------------------------------------- 1 | export interface LeafData { 2 | path: (string | number)[]; 3 | creatorKey: string; 4 | CREATOR_KEY: string; 5 | custom: boolean; 6 | bundled?: string[]; 7 | } 8 | 9 | export interface OrdinaryAction { 10 | type: string; 11 | leaf?: unknown; 12 | } 13 | 14 | export interface Action { 15 | type: string; 16 | leaf: LeafData; 17 | payload?: PayloadT; 18 | } 19 | 20 | export interface ActionWithPayload extends Action { 21 | payload: PayloadT; 22 | } 23 | 24 | export interface BundledAction 25 | extends ActionWithPayload[]> { 26 | leaf: LeafData & { 27 | bundled: string[]; 28 | }; 29 | } 30 | 31 | export interface CallbackAction { 32 | (treeState: TreeT): Action | BundledAction; 33 | leaf?: unknown; 34 | } 35 | 36 | export type RiduceAction = Action | CallbackAction; 37 | 38 | export function calledBackAction( 39 | action: CallbackAction, 40 | treeState: TreeT 41 | ): Action { 42 | return action(treeState); 43 | } 44 | 45 | export function isBundledAction( 46 | action: Action 47 | ): action is BundledAction { 48 | return !!action.leaf.bundled; 49 | } 50 | 51 | export function isCallbackAction( 52 | action: RiduceAction 53 | ): action is CallbackAction { 54 | return typeof action === "function"; 55 | } 56 | 57 | export function isRiduceAction( 58 | action: RiduceAction | OrdinaryAction, 59 | treeState: TreeT 60 | ): action is RiduceAction { 61 | return ( 62 | !!action.leaf || (typeof action === "function" && !!action(treeState)?.leaf) 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/proxy/createActionsProxy.ts: -------------------------------------------------------------------------------- 1 | import wrapWithCreate from "./wrapWithCreate"; 2 | import { RiducerDict, CreateAPI } from "../types"; 3 | 4 | export type ActionsProxy< 5 | LeafT, 6 | TreeT = LeafT, 7 | RiducerDictT extends RiducerDict = RiducerDict 8 | > = { 9 | create: CreateAPI; 10 | } & { 11 | [K in keyof Required]: ActionsProxy; 12 | }; 13 | 14 | function createActionsProxy< 15 | LeafT, 16 | TreeT, 17 | RiducerDictT extends RiducerDict 18 | >( 19 | leafState: LeafT, 20 | treeState: TreeT, 21 | riducerDict: RiducerDictT, 22 | path: (string | number)[] = [] 23 | ): ActionsProxy { 24 | const proxy = new Proxy( 25 | wrapWithCreate(leafState, treeState, riducerDict, path), 26 | { 27 | get: (target, prop: Extract | "create") => { 28 | if (prop === 'toJSON') return () => "[[object ActionsProxy]]" 29 | 30 | if (prop === "create") return target.create; 31 | 32 | return createActionsProxy(target[prop], treeState, riducerDict, [ 33 | ...path, 34 | propForPath(prop), 35 | ]); 36 | } 37 | } 38 | ); 39 | 40 | return (proxy as unknown) as ActionsProxy; 41 | } 42 | 43 | const propForPath = (prop: PropertyKey): string | number => { 44 | return isFixedString(prop) ? parseInt(String(prop)) : String(prop); 45 | }; 46 | 47 | const isFixedString = (s: PropertyKey) => { 48 | // causes a bug in DevTools. idk how to fix. sorry. 49 | const n = Number(s); 50 | return !isNaN(n) && isFinite(n) && !/e/i.test(String(s)); 51 | }; 52 | 53 | export default createActionsProxy; 54 | -------------------------------------------------------------------------------- /src/universal/universal-types.ts: -------------------------------------------------------------------------------- 1 | import { ActionWithPayload, Action } from "../types"; 2 | 3 | export enum UniversalCreatorKeys { 4 | CLEAR = "CLEAR", 5 | DO = "DO", 6 | NOOP = "NO-OP", 7 | RESET = "RESET", 8 | UPDATE = "UPDATE", 9 | } 10 | 11 | type DoCallback = (leafState: L, treeState: T) => L; 12 | 13 | export type UniversalCreators = { 14 | clear(): Action; 15 | 16 | do(cb: DoCallback): ActionWithPayload>; 17 | 18 | noop(): Action; 19 | 20 | reset(): Action; 21 | 22 | update(newVal: LeafT): ActionWithPayload; 23 | }; 24 | 25 | export type UniversalActions< 26 | KeyT extends keyof UniversalCreators, 27 | LeafT = unknown, 28 | TreeT = unknown 29 | > = ReturnType[KeyT]>; 30 | 31 | export function isClearAction( 32 | action: Action 33 | ): action is UniversalActions<"clear", L> { 34 | return action.leaf.CREATOR_KEY === UniversalCreatorKeys.CLEAR; 35 | } 36 | 37 | export function isDoAction( 38 | action: Action 39 | ): action is UniversalActions<"do", L, T> { 40 | return action.leaf.CREATOR_KEY === UniversalCreatorKeys.DO; 41 | } 42 | 43 | export function isNoopAction( 44 | action: Action 45 | ): action is UniversalActions<"noop"> { 46 | return action.leaf.CREATOR_KEY === UniversalCreatorKeys.NOOP; 47 | } 48 | 49 | export function isResetAction( 50 | action: Action 51 | ): action is UniversalActions<"reset"> { 52 | return action.leaf.CREATOR_KEY === UniversalCreatorKeys.RESET; 53 | } 54 | 55 | export function isUpdateAction( 56 | action: Action 57 | ): action is UniversalActions<"update", L> { 58 | return action.leaf.CREATOR_KEY === UniversalCreatorKeys.UPDATE; 59 | } 60 | -------------------------------------------------------------------------------- /docs/old/defaults/do.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import riduce from '../../../src'; 3 | 4 | describe("leaf.create.do(callback): returns an action that, when dispatched, updates the leaf's state to the return value of callback(state, entireState)", () => { 5 | interface State { 6 | bool: boolean, 7 | num: number, 8 | str: string, 9 | arr: number[], 10 | obj: { 11 | num?: number, 12 | arr?: number[] 13 | } 14 | } 15 | 16 | const initialState: State = { 17 | bool: false, 18 | num: 2, 19 | str: 'foo', 20 | arr: [1, 2, 3], 21 | obj: {} 22 | } 23 | 24 | const [reducer, actions] = riduce(initialState) 25 | const store = createStore(reducer) 26 | 27 | test("Calling create.do on a leaf", () => { 28 | const doToString = actions.str.create.do 29 | store.dispatch(doToString((state) => state.toUpperCase())) 30 | expect(store.getState().str).toBe('FOO') 31 | }) 32 | 33 | test("Calling create(actionType).do on a leaf", () => { 34 | const doToBoolean = actions.bool.create('APPLY_TO_BOOLEAN').do 35 | store.dispatch(doToBoolean(state => !state)) 36 | expect(store.getState().bool).toBe(true) 37 | }) 38 | 39 | test("Calling create.do on a branch", () => { 40 | const doToState = actions.obj.create.do 41 | store.dispatch(doToState((_, treeState) => ({ num: treeState.num, arr: treeState.arr }))) 42 | expect(store.getState().obj).toEqual({ num: 2, arr: [1, 2, 3] }) 43 | }) 44 | 45 | test("Calling create.do with two arguments", () => { 46 | const doToArray = actions.arr.create.do 47 | store.dispatch(doToArray( 48 | (leafState, treeState) => leafState.map(element => element * treeState.num) 49 | )) 50 | expect(store.getState().arr).toEqual([2, 4, 6]) 51 | }) 52 | }) -------------------------------------------------------------------------------- /docs/old/defaults/update.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | 4 | // @dts-jest:group Explicit state typing 5 | { 6 | interface State { 7 | bool: boolean, 8 | num: number, 9 | str: string, 10 | arr: { number: number }[], 11 | random?: any 12 | } 13 | 14 | const initialState: State = { 15 | bool: false, 16 | num: 2, 17 | str: 'foo', 18 | arr: [{ number: 4 }, { number: 2 }, { number: 7 }] 19 | } 20 | 21 | const [_, actions] = riduce(initialState) 22 | 23 | // @dts-jest:pass Root state can take state shape and doesn't need optional properties 24 | actions.create.update({ 25 | bool: true, 26 | num: 5, 27 | str: 'whatever', 28 | arr: [{ number: 9 }] 29 | }) 30 | 31 | // @dts-jest:pass Root state can take state shape allows optional properties 32 | actions.create.update({ 33 | bool: true, 34 | num: 5, 35 | str: 'whatever', 36 | arr: [], 37 | random: { something: 'deep' } 38 | }) 39 | 40 | // @dts-jest:fail Root state refuses non-conforming shape 41 | actions.create.update({ bool: true }) 42 | } 43 | 44 | // @dts-jest:group Inferred state typing 45 | { 46 | const initialState = { 47 | bool: false, 48 | num: 2, 49 | str: 'foo', 50 | arr: [{ number: 4 }, { number: 2 }, { number: 7 }] 51 | } 52 | 53 | const [_, actions] = riduce(initialState) 54 | 55 | // @dts-jest:pass Root state can take state shape 56 | actions.create.update({ 57 | bool: true, 58 | num: 5, 59 | str: 'whatever', 60 | arr: [{ number: 9 }] 61 | }) 62 | 63 | // @dts-jest:fail Root state refuses extra properties 64 | actions.create.update({ random: 'hello?' }) 65 | 66 | // @dts-jest:fail Root state refuses non-conforming shape 67 | actions.create.update({ bool: true }) 68 | } 69 | -------------------------------------------------------------------------------- /docs/old/defaults/push.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: push 3 | title: push 4 | hide_title: true 5 | sidebar_label: push 6 | --- 7 | 8 | # `push(element, [index = -1], [replace = false])` 9 | **`create.push`** 10 | **`create(actionType).push`** 11 | *Appropriate leaf type: array* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively push `element` to the leaf's state at index `index`. If `replace` is `true`, then `element` replaces the existing element with that index. 14 | 15 | ## Parameters 16 | - `element` *(any)*: the element to insert to the leaf's state 17 | - `index` *(integer, optional)*: the index of the array where `element` should be inserted 18 | - `replace` *(boolean, optional)*: whether or not `element` should replace the current `index`th element 19 | 20 | ## Returns 21 | `action` *(object)*: an object to dispatch to the store 22 | 23 | ## Example 24 | ```js 25 | import { createStore } from 'redux' 26 | import riduce from 'riduce' 27 | 28 | const initialState = { 29 | foo: [1, 2, 3], 30 | bar: [1, 2, 3], 31 | foobar: [1, 2, 3] 32 | } 33 | 34 | const [reducer, actions] = riduce(initialState) 35 | const store = createStore(reducer) 36 | ``` 37 | ### Providing element 38 | ```js 39 | const pushToFoo = actions.foo.create.push 40 | store.dispatch(pushToFoo(4)) 41 | console.log(store.getState().foo) // [1, 2, 3, 4] 42 | ``` 43 | ### Providing element and index 44 | ```js 45 | const pushToBar = actions.bar.create.push 46 | store.dispatch(pushToBar(4, 0)) // push 4 to have index 0 47 | console.log(store.getState().bar) // [4, 1, 2, 3] 48 | ``` 49 | ### Providing element, index and replace 50 | ```js 51 | const pushToFoobar = actions.foobar.create.push 52 | store.dispatch(pushToFoobar(4, 0, true)) // replace 0th element with 4 53 | console.log(store.getState().foobar) // [4, 2, 3] 54 | ``` -------------------------------------------------------------------------------- /docs/old/defaults/update.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: update 3 | title: update 4 | hide_title: true 5 | sidebar_label: update 6 | --- 7 | 8 | # `update(value)` 9 | **`create.update`** 10 | **`create(actionType).update`** 11 | *Appropriate leaf state: any* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to update the leaf's state to `value`. 14 | 15 | ## Parameters 16 | - `value` *(any)*: the new value for the leaf's state 17 | 18 | ## Returns 19 | `action` *(object)*: an object to dispatch to the store 20 | 21 | ## Example 22 | ```js 23 | import { createStore } from 'redux' 24 | import riduce from 'riduce' 25 | 26 | const initialState = { 27 | bool: false, 28 | num: 2, 29 | str: 'foo', 30 | arr: [1, 2, { number: 3 }] 31 | } 32 | 33 | const [reducer, actions] = riduce(initialState) 34 | const store = createStore(reducer) 35 | ``` 36 | 37 | ### Calling `create.update` on a leaf: 38 | 39 | ```js 40 | const updateStr = actions.str.create.update 41 | store.dispatch(updateStr("I can put anything here")) 42 | console.log(store.getState().str) // 'I can put anything here' 43 | ``` 44 | 45 | ### Calling `create.update` on an array element: 46 | 47 | ```js 48 | const updateFirstElementOfArr = actions.arr[1].create.update 49 | store.dispatch(updateFirstElementOfArr('second')) 50 | console.log(store.getState().arr) // [1, 'second', { number: 3 }] 51 | ``` 52 | 53 | ### Calling `create.update` within an array element: 54 | 55 | ```js 56 | const updateSecondElementNumberProp = actions.arr[2].number.create.update 57 | store.dispatch(updateSecondElementNumberProp(1337)) 58 | console.log(store.getState().arr) // [1, 'second', { number: 1337 }] 59 | ``` 60 | 61 | ### Calling `create.update` on a branch: 62 | ```js 63 | const updateState = actions.create.update 64 | store.dispatch(updateState({ any: { properties: true }})) 65 | console.log(store.getState()) // { any: { properties: true } } 66 | ``` -------------------------------------------------------------------------------- /docs/old/examples/useReducerExample.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: usereducer-example 3 | title: Using with useReducer instead of Redux 4 | hide_title: true 5 | sidebar_label: useReducer example 6 | --- 7 | 8 | # `useReducer` example, no Redux 9 | 10 | Because [`riduce`](../README.md) returns a `reducer` and `actions`, it can be used with the React [`useReducer`](https://reactjs.org/docs/hooks-reference.html#usereducer) hook instead of Redux if desired. 11 | 12 | For a demo, check out this [Todo app](https://codesandbox.io/s/todo-app-with-usereducer-react-testing-library-and-redux-leaves-inziu) modelled on the React-Redux tutorial example, refactored to use the `useReducer` hook in combination with `riduce`. 13 | 14 | ## Example 15 | [CodeSandbox demo](https://codesandbox.io/s/redux-leaves-with-usereducer-5xpkz) 16 | 17 | ```jsx 18 | import React, { useReducer } from "react"; 19 | import riduce, { bundle } from 'redux-leaves'; 20 | 21 | const initialState = { 22 | name: "user", 23 | list: [] 24 | }; 25 | 26 | const [reducer, actions] = riduce(initialState); 27 | 28 | function App() { 29 | const [state, dispatch] = useReducer(reducer, initialState); 30 | 31 | const handleChange = e => { 32 | // update state.name to be e.target.value 33 | dispatch(actions.name.create.update(e.target.value)); 34 | }; 35 | 36 | const handleSave = () => { 37 | // 1. push current val of state.name into state.list 38 | // 2. clear state.name 39 | // ... in a single dispatch 40 | const actionBundle = bundle([ 41 | actions.list.create.push(state.name), 42 | actions.name.create.clear() 43 | ]) 44 | 45 | dispatch(actionBundle); 46 | }; 47 | 48 | return ( 49 |
50 |

Hello, {state.name}!

51 |
52 | Name: 53 | 54 | 55 |
56 |
57 | Greeted: 58 | {state.list.join(", ")} 59 |
60 |
61 | ); 62 | } 63 | ``` -------------------------------------------------------------------------------- /docs/old/api/creatorKeys.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: creator-keys 3 | title: Creator Keys 4 | hide_title: true 5 | sidebar_label: Creator keys 6 | --- 7 | 8 | # Creator keys 9 | 10 | A creator key (`creatorKey`) serves two roles: 11 | 12 | 1. In a [`riducerDict`](../README.md#reducersdict), it uniquely identifies a given [leaf reducer](leafReducers.md); and 13 | 2. In the `actions` API, it: 14 | - is an action creator available at a given leaf through [`.create[creatorKey]`](create.md), that 15 | - triggers the corresponding leaf reducer logic (when said action creator is called and its resultant action is dispatched). 16 | 17 | ## Example 18 | 19 | In the below code: 20 | - `addOne` is the creator key that identifies and triggers a reducer to add one to a leaf's state; 21 | - `doubleEach` is the creator key that identifies and triggers a reducer to double each element in a leaf's state. 22 | 23 | ### Setup 24 | 25 | ```js 26 | import riduce from 'redux-leaves' 27 | import { createStore } from 'redux' 28 | 29 | const initialState = { 30 | foo: 5, 31 | bar: [1, 2, 3] 32 | } 33 | ``` 34 | 35 | ### Identifying leaf reducers 36 | ```js 37 | // Use the creator keys to uniquely identify leaf reducers 38 | const riducerDict = { 39 | addOne: leafState => leafState + 1, 40 | doubleEach: leafState => leafState.map(n => 2 * n) 41 | } 42 | ``` 43 | 44 | ### Creating actions 45 | 46 | ```js 47 | // Grab the actions object using riduce 48 | const [reducer, actions] = riduce(initialState, riducerDict) 49 | const store = createStore(reducer) 50 | 51 | // Use the creator keys at a chosen leaf's create property: 52 | const actionToAddOneToFoo = actions.foo.create.addOne() 53 | const actionToDoubleEachAtBar = actions.bar.create.doubleEach() 54 | ``` 55 | 56 | ### Triggering the leaf reducer 57 | ```js 58 | // Dispatch created actions to trigger the matching leaf reducer 59 | store.dispatch(actionToAddOneToFoo) 60 | store.dispatch(actionToDoubleEachAtBar) 61 | 62 | console.log(store.getState()) 63 | /* 64 | { 65 | foo: 6, 66 | bar: [2, 4, 6] 67 | } 68 | */ 69 | ``` -------------------------------------------------------------------------------- /docs/old/api/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: actions 3 | title: actions 4 | hide_title: true 5 | sidebar_label: actions 6 | --- 7 | 8 | # `actions` 9 | 10 | ## Arbitrary property paths 11 | 12 | The `actions` object returned by [`riduce`](../README.md) can take an arbitrary path of properties after it, which typically correspond to a 'leaf' at the corresponding path from your state. 13 | 14 | ```js 15 | import riduce from 'redux-leaves' 16 | 17 | const initialState = { 18 | counter: 0, 19 | arbitrary: { 20 | nested: { 21 | path: ['hi!'] 22 | } 23 | } 24 | } 25 | 26 | const [reducer, actions] = riduce(initialState) 27 | 28 | // state.counter 29 | console.log(typeof actions.counter) // 'object' 30 | 31 | // state.arbitrary.nested 32 | console.log(typeof actions.arbitrary.nested) // 'object' 33 | 34 | // state.arbitrary.nested.path 35 | console.log(typeof actions.arbitrary.nested.path) // 'object' 36 | 37 | // but also works for paths not in your initial state 38 | console.log(typeof actions.not.in.my.initial.state) // 'object' 39 | ``` 40 | 41 | ## Accessing `create` 42 | 43 | For a given arbitrary property path, you have two options: navigating to a deeper property / leaf, or accessing the [`create`](create.md) property. 44 | 45 | ```js 46 | // Access create at the state root 47 | console.log(typeof actions.create) // 'function' 48 | 49 | // Go deeper 50 | console.log(typeof actions.arbitrary) // 'object' 51 | 52 | // Access create at state.arbitrary 53 | console.log(typeof actions.arbitrary.create) // 'function' 54 | 55 | // Go deeper 56 | console.log(typeof actions.arbitrary.nested.path) 57 | 58 | // Access create at state.arbitary.nested.path 59 | console.log(typeof actions.arbitrary.nested.path.create) // 'function' 60 | ``` 61 | 62 | Once you've accessed `create`, you can't go arbitrarily deeper beyond that. 63 | ```js 64 | console.log(typeof actions.create.arbitrary) // 'undefined' 65 | ``` 66 | 67 | This is because the `create` key accesses the Redux-Leaves [action creator (`create`) API](create.md) at the corresponding leaf of state. -------------------------------------------------------------------------------- /docs/old/examples/intermediateExample.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import riduce, { Action, ActionWithPayload } from '../../../src'; 3 | 4 | describe('Intermediate example', () => { 5 | const initialState = { 6 | counter: 2, 7 | list: ['first', 'second'], 8 | nested: { arbitrarily: { deep: 0 } } 9 | } 10 | 11 | const riducerDict = { 12 | double: (leafState: number) => leafState * 2, 13 | appendToEach: (leafState: string[], action: ActionWithPayload) => leafState.map(str => str.concat(action.payload)), 14 | countTreeKeys: (_: any, __: Action, treeState: typeof initialState) => Object.keys(treeState).length 15 | } 16 | 17 | const [reducer, actions] = riduce(initialState, riducerDict) 18 | const store = createStore(reducer) 19 | 20 | test("We can double the counter's state", () => { 21 | expect(store.getState().counter).toBe(2) 22 | store.dispatch(actions.counter.create.double()) 23 | expect(store.getState().counter).toBe(4) 24 | }) 25 | 26 | test("We can append to the list", () => { 27 | expect(store.getState().list).toEqual(['first', 'second']) 28 | store.dispatch(actions.list.create.appendToEach(' item')) 29 | expect(store.getState().list).toEqual(['first item', 'second item']) 30 | }) 31 | 32 | test("We can count the number of keys in the state tree", () => { 33 | expect(store.getState().nested.arbitrarily.deep).toBe(0) 34 | store.dispatch(actions.nested.arbitrarily.deep.create.countTreeKeys()) 35 | expect(store.getState().nested.arbitrarily.deep).toBe(3) 36 | }) 37 | 38 | test("We can double arbitrarily deep state", () => { 39 | expect(store.getState().nested.arbitrarily.deep).toBe(3) 40 | store.dispatch(actions.nested.arbitrarily.deep.create.double()) 41 | expect(store.getState().nested.arbitrarily.deep).toBe(6) 42 | }) 43 | 44 | test("By default, when providing arguments, the first becomes the payload", () => { 45 | // @ts-ignore 46 | const actionToAppend = actions.list.create.appendToEach('foo', 'bar') 47 | expect(actionToAppend.payload).toBe('foo') 48 | }) 49 | }) -------------------------------------------------------------------------------- /docs/old/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: redux-leaves 3 | title: riduce 4 | hide_title: true 5 | sidebar_label: riduce 6 | --- 7 | 8 | # `riduce(initialState, [riducerDict = {}])` 9 | 10 | Returns a reducer function and an actions object. 11 | 12 | **See the [30 second demo](examples/basicExample.md)** for usage. 13 | 14 | ## Parameters 15 | - [`initialState`](#initialstate) *(object)*: the state shape and initial values for your Redux store 16 | - [`riducerDict`](#reducersdict) *(object, optional)*: a collection of [leaf reducers](api/leafReducers.md) keyed by their [creator keys](api/creatorKeys.md) 17 | 18 | ### `initialState` 19 | *(object)* 20 | 21 | This is the state shape and initial values for your Redux store. 22 | 23 | It is described as having state 'branches' and 'leaves'. 24 | 25 | #### Example 26 | 27 | ```js 28 | const initialState = { 29 | todos: { 30 | byId: {}, 31 | allIds: [] 32 | }, 33 | visibilityFilter: "SHOW_ALL" 34 | } 35 | ``` 36 | 37 | ### `riducerDict` 38 | *(object)* 39 | 40 | This is an object where every `key`-`value` pair is such that: 41 | - `value` *(function | object)* is a [leaf reducer](api/leafReducers.md); 42 | - `key` is a [creator key](api/creatorKeys.md) for that leaf reducer. 43 | 44 | #### Example 45 | 46 | ```js 47 | const riducerDict = { 48 | increment: (state, { payload }) => state + payload, 49 | slice: { 50 | argsToPayload: (begin, end) => [begin, end] 51 | reducer: (state, { payload }) => state.slice(payload[0], payload[1]) 52 | } 53 | } 54 | ``` 55 | 56 | ## Returns 57 | `array`, with two elements: 58 | - 0th: `reducer` *(function)*: a reducer function to pass to redux's `createStore` 59 | - 1st: [`actions`](api/actions.md) *(object)*: an object with same shape as `initialState` 60 | 61 | ### `reducer` 62 | 63 | The root reducer for your Redux store. 64 | 65 | It listens to actions created through [`actions`](api/actions.md) at a given leaf for a given [creator key](api/creatorKeys.md), and updates that leaf's state using the [leaf reducer](api/leafReducers.md) keyed by the creator key. 66 | 67 | ### `actions` 68 | 69 | See documentation on the [`actions`](api/actions.md) object. -------------------------------------------------------------------------------- /src/array/array-types.ts: -------------------------------------------------------------------------------- 1 | import { Action, ActionWithPayload } from "../types"; 2 | import { Unpacked } from "../types/util-types"; 3 | 4 | export enum ArrayCreatorKeys { 5 | CONCAT = "CONCAT_ARR", 6 | DROP = "DROP", 7 | FILTER = "FILTER", 8 | PUSH = "PUSH", 9 | } 10 | 11 | export type ArrayCreators = { 12 | concat(arr: LeafT): ActionWithPayload; 13 | drop(n?: number): ActionWithPayload; 14 | filter( 15 | cb: FilterCallback> 16 | ): ActionWithPayload>>; 17 | push( 18 | element: Unpacked, 19 | index?: number, 20 | replace?: boolean 21 | ): ActionWithPayload<{ 22 | element: Unpacked; 23 | index?: number; 24 | replace?: boolean; 25 | }>; 26 | }; 27 | 28 | type FilterCallback = (element: E, index: number, source: E[]) => boolean; 29 | 30 | export type ArrayActions< 31 | KeyT extends keyof ArrayCreators, 32 | LeafT extends unknown[] = unknown[], 33 | TreeT = unknown 34 | > = ReturnType[KeyT]>; 35 | 36 | export function isArrayAction(action: Action): boolean { 37 | return ( 38 | isDropAction(action) || 39 | isConcatActionArray(action) || 40 | isFilterAction(action) || 41 | isPushAction(action) 42 | ); 43 | } 44 | 45 | export function isDropAction(action: Action): action is ArrayActions<"drop"> { 46 | return action.leaf.CREATOR_KEY === ArrayCreatorKeys.DROP; 47 | } 48 | 49 | export function isConcatActionArray( 50 | action: Action 51 | ): action is ArrayActions<"concat", LeafT, unknown> { 52 | return action.leaf.CREATOR_KEY === ArrayCreatorKeys.CONCAT; 53 | } 54 | 55 | export function isFilterAction( 56 | action: Action 57 | ): action is ArrayActions<"filter", LeafT, unknown> { 58 | return action.leaf.CREATOR_KEY === ArrayCreatorKeys.FILTER; 59 | } 60 | 61 | export function isPushAction( 62 | action: Action 63 | ): action is ArrayActions<"push", LeafT, unknown> { 64 | return action.leaf.CREATOR_KEY === ArrayCreatorKeys.PUSH; 65 | } 66 | -------------------------------------------------------------------------------- /src/riduce.ts: -------------------------------------------------------------------------------- 1 | import leafReducer from "./leafReducer"; 2 | import { createActionsProxy } from "./proxy"; 3 | import { ActionsProxy } from "./proxy/createActionsProxy"; 4 | import { 5 | RiducerDict, 6 | isBundledAction, 7 | isCallbackAction, 8 | RiduceAction, 9 | OrdinaryAction, 10 | isRiduceAction, 11 | } from "./types"; 12 | import updateState, { getState } from "./utils/update-state"; 13 | 14 | export type Reducer = ( 15 | treeState: TreeT | undefined, 16 | action: ActionT 17 | ) => TreeT; 18 | 19 | export type Riduce = {}> = [ 20 | Reducer | OrdinaryAction>, 21 | ActionsProxy 22 | ]; 23 | 24 | function makeReducer = {}>( 25 | initialState: TreeT, 26 | riducerDict: RiducerDictT 27 | ): Reducer | OrdinaryAction> { 28 | const reducer = ( 29 | treeState: TreeT = initialState, 30 | action: RiduceAction | OrdinaryAction 31 | ): TreeT => { 32 | if (!isRiduceAction(action, treeState)) return treeState; 33 | 34 | if (isCallbackAction(action)) { 35 | const createdAction = action(treeState); 36 | return reducer(treeState, createdAction); 37 | } else if (isBundledAction(action)) { 38 | return action.payload.reduce(reducer, treeState); 39 | } else { 40 | const prevLeafState = getState(treeState, action.leaf.path); 41 | 42 | const newLeafState = leafReducer( 43 | prevLeafState, 44 | treeState, 45 | action, 46 | initialState, 47 | riducerDict 48 | ); 49 | 50 | return updateState(treeState, action.leaf.path, newLeafState); 51 | } 52 | }; 53 | 54 | return reducer; 55 | } 56 | 57 | export function riduce = {}>( 58 | initialState: TreeT, 59 | riducerDict: RiducerDictT = {} as RiducerDictT 60 | ): Riduce { 61 | const reducer = makeReducer(initialState, riducerDict); 62 | 63 | const actions = createActionsProxy(initialState, initialState, riducerDict); 64 | 65 | return [reducer, actions]; 66 | } 67 | 68 | export default riduce; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riduce", 3 | "version": "0.0.27", 4 | "description": "Get rid of your reducer boilerplate! Zero hassle state management that's typed, flexible and scalable.", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "license": "MIT", 8 | "author": { 9 | "name": "Richard Ng", 10 | "email": "hi@richard.ng", 11 | "url": "https://github.com/richardcrng/" 12 | }, 13 | "dependencies": { 14 | "change-case": "^4.1.1", 15 | "immer": "^3.1.2", 16 | "lodash": "^4.17.15", 17 | "object.fromentries": "^2.0.2", 18 | "ramda": "^0.26.1", 19 | "ramda-adjunct": "^2.23.0", 20 | "set-value": ">=2.0.1", 21 | "utility-types": "^3.10.0" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/richardcrng/riduce" 26 | }, 27 | "scripts": { 28 | "type-check": "tsc --noEmit", 29 | "type-check:watch": "npm run type-check -- --watch", 30 | "build": "npm run build:types && npm run build:js", 31 | "build:types": "tsc --emitDeclarationOnly", 32 | "build:js": "babel src --out-dir dist --extensions \".ts,.tsx\" --source-maps inline", 33 | "prepublishOnly": "npm run build", 34 | "test": "jest --watch --runInBand", 35 | "test:types": "dts-jest" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.7.7", 42 | "@babel/core": "^7.7.7", 43 | "@babel/plugin-proposal-class-properties": "^7.7.4", 44 | "@babel/plugin-proposal-numeric-separator": "^7.7.4", 45 | "@babel/plugin-proposal-object-rest-spread": "^7.7.7", 46 | "@babel/preset-env": "^7.7.7", 47 | "@babel/preset-typescript": "^7.7.7", 48 | "@testing-library/react-hooks": "^3.7.0", 49 | "@types/jest": "^26.0.10", 50 | "@types/node": "^13.13.5", 51 | "@types/ramda": "^0.27.4", 52 | "@types/react": "^16.8.0 || ^17.0.0", 53 | "babel-jest": "^26.3.0", 54 | "coveralls": "^3.0.9", 55 | "dts-jest": "^23.3.0", 56 | "jest": "^26.4.2", 57 | "react": "^16.8.0 || ^17.0.0", 58 | "react-test-renderer": "^16.8.0 || ^17.0.0", 59 | "redux": "^4.0.5", 60 | "ts-jest": "^26.3.0", 61 | "typescript": "^4.1.5", 62 | "util": "^0.12.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/leafReducer/leafReducer.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from "ramda-adjunct"; 2 | import { 3 | Action, 4 | isCustomAction, 5 | RiducerDict, 6 | isLonghandReducer, 7 | } from "../types"; 8 | import universalLeafReducer from "../universal/universalLeafReducer"; 9 | import arrayLeafReducer from "../array/arrayLeafReducer"; 10 | import stringLeafReducer from "../string/stringLeafReducer"; 11 | import objectLeafReducer from "../object/objectLeafReducer"; 12 | import numberLeafReducer from "../number/numberLeafReducer"; 13 | import booleanLeafReducer from "../boolean/booleanLeafReducer"; 14 | import undefinedLeafReducer from "../undefined/undefinedLeafReducer"; 15 | 16 | function leafReducer< 17 | LeafT, 18 | TreeT, 19 | ActionT extends Action, 20 | RiducerDictT extends RiducerDict 21 | >( 22 | leafState: LeafT, 23 | treeState: TreeT, 24 | action: ActionT, 25 | originalState: TreeT, 26 | riducerDict: RiducerDictT 27 | ): LeafT { 28 | if (isCustomAction(action)) { 29 | const { [action.leaf.creatorKey]: matchingDefinition } = riducerDict; 30 | 31 | if (matchingDefinition) { 32 | return isLonghandReducer(matchingDefinition) 33 | ? matchingDefinition.reducer(leafState, action, treeState) 34 | : matchingDefinition(leafState, action, treeState); 35 | } else { 36 | return leafState; 37 | } 38 | } 39 | 40 | if (Array.isArray(leafState)) { 41 | return arrayLeafReducer(leafState, treeState, action, originalState); 42 | } 43 | 44 | if (typeof leafState === "string") { 45 | return stringLeafReducer(leafState, treeState, action, originalState); 46 | } 47 | 48 | if (typeof leafState === "number") { 49 | return numberLeafReducer(leafState, treeState, action, originalState); 50 | } 51 | 52 | if (typeof leafState === "boolean") { 53 | return booleanLeafReducer(leafState, treeState, action, originalState); 54 | } 55 | 56 | if (isPlainObject(leafState)) { 57 | return objectLeafReducer(leafState, treeState, action, originalState); 58 | } 59 | 60 | if (typeof leafState === "undefined") { 61 | return undefinedLeafReducer(leafState, treeState, action, originalState); 62 | } 63 | 64 | return universalLeafReducer(leafState, treeState, action, originalState); 65 | } 66 | 67 | export default leafReducer; 68 | -------------------------------------------------------------------------------- /docs/old/defaults/do.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: do 3 | title: do 4 | hide_title: true 5 | sidebar_label: do 6 | --- 7 | 8 | # `do(callback)` 9 | **`create.do`** 10 | **`create(actionType).do`** 11 | *Appropriate leaf state: any* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively update the leaf's state to the return value of `callback(leafState, treeState)`. 14 | 15 | *Note: creating an action using `do(callback)` does not follow Redux's non-enforced recommendation that [actions should always be serializable](https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants), since the resultant action will have the function `callback` as its `payload`.* 16 | 17 | ## Parameters 18 | - `callback` *(function)*: invoked by the leaf's reducer with two arguments, `leafState` and `entireState` 19 | 20 | ## Returns 21 | `action` *(object)*: an object to dispatch to the `store` 22 | 23 | ## Example 24 | ```js 25 | import { createStore } from 'redux' 26 | import riduce from 'riduce' 27 | 28 | const initialState = { 29 | bool: false, 30 | num: 2, 31 | str: 'foo', 32 | arr: [1, 2, 3] 33 | } 34 | 35 | const [reducer, actions] = riduce(initialState) 36 | const store = createStore(reducer) 37 | ``` 38 | 39 | ### Calling `create.do` on a leaf: 40 | 41 | ```js 42 | const doToString = actions.str.create.do 43 | store.dispatch(doToString(state => state.toUpperCase())) 44 | console.log(store.getState().str) // 'FOO' 45 | ``` 46 | 47 | ### Calling `create(actionType).do` on a leaf: 48 | 49 | ```js 50 | const doToBoolean = actions.bool.create('APPLY_TO_BOOLEAN').do 51 | store.dispatch(doToBoolean(state => !state)) 52 | console.log(store.getState().bool) // true 53 | ``` 54 | 55 | ### Calling `create.do` on a branch: 56 | 57 | ```js 58 | const doToState = actions.create.do 59 | store.dispatch(doToState(state => ({ num: state.num, arr: state.arr })) 60 | console.log(store.getState()) // { num: 2, arr: [1, 2, 3] } 61 | ``` 62 | 63 | ### Calling `create.do` with two arguments: 64 | 65 | ```js 66 | const doToArray = actions.arr.create.do 67 | store.dispatch(doToArray( 68 | (leafState, treeState) => leafState.map(element => element * treeState.num) 69 | )) 70 | console.log(store.getState()) // { num: 2, arr: [2, 4, 6] } 71 | ``` -------------------------------------------------------------------------------- /docs/old/examples/basicExample.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: basic-example 3 | title: Basic example 4 | hide_title: true 5 | sidebar_label: Basic example 6 | --- 7 | 8 | # Basic example: 30 second demo 9 | 10 | [Play around with this code on Runkit](https://runkit.com/richardcrng/redux-leaves-basic-example) 11 | 12 | **Situation**: I want to be able to increment two different counters in Redux state, `counterOne` and `counterTwo`. 13 | 14 | **Complication**: I want to do this as quickly, painlessly and intuitively as possible. 15 | 16 | **Question**: Do I really have to define reducers, action types and creators to do this? 17 | 18 | Answer: no! Just provide Redux-Leaves with your state shape, i.e. the two counters, and it'll do the rest for you! 19 | 20 | ## Demonstration 21 | 22 | ### Set up the store's state 23 | ```js 24 | // Imports for Redux and Redux-Leaves 25 | import { createStore } from 'redux' 26 | import riduce from 'redux-leaves' 27 | 28 | // Your job: provide some initial state 29 | const initialState = { 30 | counterOne: 0, 31 | counterTwo: 0 32 | } 33 | 34 | // Redux-Leaves's job: to write your reducer and actions for you 35 | const [reducer, actions] = riduce(initialState) 36 | 37 | // Create your Redux store using the given reducer 38 | const store = createStore(reducer) 39 | ``` 40 | 41 | ### Update the store's state 42 | ```js 43 | console.log(store.getState()) // { counterOne: 0, counterTwo: 0 } 44 | 45 | // Let's create an action to increment counterOne by 3 46 | const actionToIncrementCounterOneByThree = actions.counterOne.create.increment(3) 47 | 48 | // Dispatch our created action to the store 49 | store.dispatch(actionToIncrementCounterOneByThree) 50 | 51 | // The store's state will be updated! 52 | console.log(store.getState()) // { counterOne: 3, counterTwo: 0 } 53 | 54 | // Now let's increment counterTwo by 10 55 | store.dispatch(actions.counterTwo.create.increment(10)) 56 | console.log(store.getState()) // { counterOne: 3, counterTwo: 10 } 57 | ``` 58 | 59 | Redux-Leaves has done all the hard work in providing you with a `reducer` and appropriate `actions` to `.create`! 60 | 61 | `increment` is one of many [default action creators](../defaults/README.md) that Redux-Leaves writes for you, which cover most basic needs. 62 | 63 | If you want to add some custom action creators, look at the [intermediate example](intermediateExample.md). -------------------------------------------------------------------------------- /docs/old/defaults/pushedSet.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: pushed-set 3 | title: pushed-set 4 | hide_title: true 5 | sidebar_label: pushed-set 6 | --- 7 | 8 | # `pushedSet(valueOrCallback)` 9 | **`create.pushedSet`** 10 | **`create(actionType).pushedSet`** 11 | *Appropriate leaf type: object* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to non-mutatively update the leaf's state at an auto-generated key (that orders chronologically after previous keys) with `value`. 14 | 15 | (This is inspired by the [Firebase Real-Time Database .push](https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data) and uses [their method for auto-generating keys](https://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html).) 16 | 17 | ## Parameters 18 | - `valueOrCallback` *(any)*: 19 | - the value to set; or 20 | - a callback function that uses the autogenerated key and returns the value to set 21 | 22 | ## Returns 23 | `action` *(object)*: an object to dispatch to the store 24 | 25 | ## Example 26 | ```js 27 | import { createStore } from 'redux' 28 | import riduce from 'riduce' 29 | 30 | const initialState = { 31 | foo: {}, 32 | bar: {} 33 | } 34 | 35 | const [reducer, actions] = riduce(initialState) 36 | const store = createStore(reducer) 37 | ``` 38 | 39 | ### Passing a value 40 | ```js 41 | const pushedSetInFoo = actions.foo.create.pushedSet 42 | store.dispatch(pushedSetInFoo('my first item')) 43 | store.dispatch(pushedSetInFoo('my second item')) 44 | console.log(store.getState().foo) 45 | /* 46 | will look something like: 47 | { 48 | 5bzqUkZnXzQIpIbg5dq8pzIrFVT2: 'my first item', 49 | B4y3IRRyR6hbFlu7s1HheCPPv5x1: 'my second item' 50 | } 51 | */ 52 | 53 | // Auto-generated keys guarantee expected order iteration 54 | const orderedValues = Object.values(store.getState().foo) 55 | console.log(orderedValues) // ['my first item', 'my second item'] 56 | ``` 57 | 58 | ### Passing a callback 59 | ```js 60 | const pushedSetInBar = actions.foo.create.pushedSet 61 | store.dispatch(pushedSetInBar(autoGeneratedKey => ({ id: autogeneratedKey, text: 'my first item' }))) 62 | console.log(store.getState().bar) 63 | /* 64 | will look something like: 65 | { 66 | 5bzqUkZnXzQIpIbg5dq8pzIrFVT2: { 67 | id: '5bzqUkZnXzQIpIbg5dq8pzIrFVT2', 68 | text: 'my first item' 69 | } 70 | */ 71 | 72 | ``` -------------------------------------------------------------------------------- /docs/old/defaults/clear.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: clear 3 | title: clear 4 | hide_title: true 5 | sidebar_label: clear 6 | --- 7 | 8 | # `clear(toNull = false)` 9 | **`create.clear`** 10 | **`create(actionType).clear`** 11 | *Appropriate leaf state: any* 12 | 13 | Returns an (action) object that the [riduce](../README.md) reducer uses to clears the leaf's state. 14 | 15 | If `toNull === true`, then it updates it to `null`, otherwise it follows the type of the leaf's state: 16 | - *number* clears to `0` 17 | - *string* clears to `''` 18 | - *boolean* clears to `false` 19 | - *array* clears to `[]` 20 | - *object* clears to `{}` 21 | 22 | ## Parameters 23 | - `toNull` *(boolean, optional)*: defaults to `false` 24 | 25 | ## Returns 26 | `action` *(object)*: an object to dispatch to the `store` 27 | 28 | ## Example 29 | ```js 30 | import { createStore } from 'redux' 31 | import riduce from 'riduce' 32 | 33 | const initialState = { 34 | bool: true, 35 | num: 2, 36 | str: 'foo', 37 | arr: [1, 2, 3] 38 | } 39 | 40 | const [reducer, actions] = riduce(initialState) 41 | const store = createStore(reducer) 42 | ``` 43 | ### Boolean state 44 | ```js 45 | const clearBool = actions.bool.create.clear 46 | 47 | store.dispatch(clearBool()) 48 | console.log(store.getState().bool) // false 49 | 50 | store.dispatch(clearBool(true)) 51 | console.log(store.getState().bool) // null 52 | ``` 53 | ### Number state 54 | ```js 55 | const clearNum = actions.num.create.clear 56 | 57 | store.dispatch(clearNum()) 58 | console.log(store.getState().num) // 0 59 | 60 | store.dispatch(clearNum(true)) 61 | console.log(store.getState().num) // null 62 | ``` 63 | ### String state 64 | ```js 65 | const clearStr = actions.str.create.clear 66 | 67 | store.dispatch(clearStr()) 68 | console.log(store.getState().str) // '' 69 | 70 | store.dispatch(clearStr(true)) 71 | console.log(store.getState().str) // null 72 | ``` 73 | ### Array state 74 | ```js 75 | const clearArr = actions.arr.create.clear 76 | 77 | store.dispatch(clearArr()) 78 | console.log(store.getState().arr) // [] 79 | 80 | store.dispatch(clearArr(true)) 81 | console.log(store.getState().arr) // null 82 | ``` 83 | ### Object state 84 | ```js 85 | const clearState = actions.create.clear 86 | 87 | store.dispatch(clearState()) 88 | console.log(store.getState()) // {} 89 | 90 | store.dispatch(clearState(true)) 91 | console.log(store.getState()) // null 92 | ``` -------------------------------------------------------------------------------- /docs/old/api/create.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | import riduce from '../../../src'; 3 | 4 | describe('create has action creators keyed by default and custom creatorKeys', () => { 5 | const initialState = { 6 | counter: 0, 7 | arbitrary: { 8 | nested: { 9 | path: ['hi!'] 10 | } 11 | }, 12 | str: 'hello world' 13 | } 14 | 15 | const riducerDict = { 16 | convertToFoobar: (_: string) => 'foobar' 17 | } 18 | 19 | const [reducer, actions] = riduce(initialState, riducerDict) 20 | 21 | test('All creates have default creatorKeys like update, set and push', () => { 22 | expect(typeof actions.counter.create.update).toBe('function') 23 | expect(typeof actions.arbitrary.nested.create.set).toBe('function') 24 | expect(typeof actions.arbitrary.nested.path.create.push).toBe('function') 25 | }) 26 | 27 | test('All creates also have supplied custom creatorKeys', () => { 28 | expect(typeof actions.str.create.convertToFoobar).toBe('function') 29 | expect(typeof actions.arbitrary.nested.path[0].create.convertToFoobar).toBe('function') 30 | }) 31 | 32 | const store = createStore(reducer) 33 | 34 | test("Executing an action creator returns an action to dispatch to the Redux store", () => { 35 | const updateCounter = actions.counter.create.update 36 | 37 | store.dispatch(updateCounter(5)) 38 | expect(store.getState().counter).toBe(5) 39 | 40 | store.dispatch(updateCounter(3)) 41 | expect(store.getState().counter).toBe(3) 42 | }) 43 | 44 | describe('create can take a string argument as actionType', () => { 45 | test('If given this argument, it still has properties corresponding to default and custom provided creators', () => { 46 | expect(typeof actions.counter.create('UPDATE_COUNTER').update).toBe('function') 47 | expect(typeof actions.str.create('CONVERT_TO_FOOBAR').convertToFoobar).toBe('function') 48 | }) 49 | 50 | test('The created actions have the supplied actionType as type', () => { 51 | const createNamedIncrement = actions.counter.create('NAMED_INCREMENT').increment 52 | const namedIncrement = createNamedIncrement() 53 | 54 | expect(namedIncrement.type).toBe('NAMED_INCREMENT') 55 | const currVal = store.getState().counter 56 | store.dispatch(namedIncrement) 57 | expect(store.getState().counter).toBe(currVal + 1) 58 | }) 59 | }) 60 | }) -------------------------------------------------------------------------------- /docs/old/typescript/actions.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from "../../../src" 2 | 3 | 4 | // @dts-jest:group Actions shape mirrors state 5 | { 6 | const initialState = { 7 | shallow: true, 8 | nested: { 9 | counter: 0, 10 | state: { 11 | deep: 'somewhat' 12 | } 13 | }, 14 | list: [1, 2, 3] 15 | } 16 | 17 | const [reducer, actions] = riduce(initialState) 18 | 19 | // @dts-jest:pass 20 | actions.shallow 21 | 22 | // @dts-jest:fail 23 | actions.foobar 24 | 25 | // @dts-jest:pass 26 | actions.nested.counter 27 | 28 | // @dts-jest:fail 29 | actions.nested.string 30 | } 31 | 32 | // @dts-jest:group Action keys are not possibly undefined but is sensitive to passing undefined 33 | { 34 | interface State { 35 | num: number; 36 | nested?: { 37 | deep?: boolean; 38 | }; 39 | } 40 | 41 | const initialState: State = { 42 | num: 4, 43 | nested: { 44 | deep: true, 45 | }, 46 | }; 47 | 48 | const [reducer, actions] = riduce(initialState); 49 | 50 | // @dts-jest:pass 51 | actions.nested.deep; 52 | 53 | // @dts-jest:pass 54 | actions.nested.deep.create.update(undefined); 55 | 56 | // @dts-jest:fail 57 | actions.num.create.update(undefined); 58 | } 59 | 60 | // @dts-jest:group Creator update is sensitive to the leaf type 61 | { 62 | const initialState = { 63 | boolState: true, 64 | nested: { 65 | num: 0, 66 | state: { 67 | str: 'somewhat' 68 | } 69 | }, 70 | numList: [1, 2, 3] 71 | } 72 | 73 | const [reducer, actions] = riduce(initialState) 74 | 75 | // @dts-jest:pass 76 | actions.boolState.create.update(true) 77 | 78 | // @dts-jest:pass 79 | actions.boolState.create.update(false) 80 | 81 | // @dts-jest:fail 82 | actions.boolState.create.update('true') 83 | 84 | // @dts-jest:pass 85 | actions.nested.num.create.update(5) 86 | 87 | // @dts-jest:fail 88 | actions.nested.num.create.update('5') 89 | 90 | // @dts-jest:pass 91 | actions.nested.state.create.update({ 92 | str: 'foobar' 93 | }) 94 | 95 | // @dts-jest:fail 96 | actions.nested.state.create.update({ randomKey: 'foobar' }) 97 | 98 | // @dts-jest:pass 99 | actions.numList.create.update([2, 4, 8]) 100 | 101 | // @dts-jest:fail 102 | actions.numList.create.update(['2']) 103 | } -------------------------------------------------------------------------------- /docs/old/defaults/set.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | // @dts-jest:group Inferred state 4 | { 5 | const initialState = { 6 | bool: false, 7 | num: 2, 8 | str: 'foo', 9 | arr: [1, 2, 3], 10 | nested: { deep: true }, 11 | obj: { num: 5, names: [['a', 'e'], ['b, c']] } 12 | } 13 | 14 | const [_, actions] = riduce(initialState) 15 | 16 | // @dts-jest:fail does not exist on boolean state 17 | actions.bool.create.set 18 | 19 | // @dts-jest:pass exists on object state 20 | actions.obj.create.set 21 | 22 | // @dts-jest:pass exists on root if object 23 | actions.create.set 24 | 25 | // @dts-jest:fail needs an argument 26 | actions.obj.create.set() 27 | 28 | // @dts-jest:fail requires more than one argument 29 | actions.obj.create.set('num') 30 | 31 | // @dts-jest:fail rejects inconsistent second argument 32 | actions.obj.create.set('num', '2') 33 | 34 | // @dts-jest:fail rejects inconsistent property 35 | actions.obj.create.set('notHere', 2) 36 | 37 | // @dts-jest:pass accepts correct match 38 | actions.obj.create.set('num', 2) 39 | } 40 | 41 | // @dts-jest:group Explicitly typed state 42 | { 43 | interface State { 44 | bool: boolean, 45 | num: number, 46 | str: string, 47 | arr: number[], 48 | nested: { deep: boolean }, 49 | obj: { num: number, names: string[][], here?: string } 50 | } 51 | 52 | const initialState: State = { 53 | bool: false, 54 | num: 2, 55 | str: 'foo', 56 | arr: [1, 2, 3], 57 | nested: { deep: true }, 58 | obj: { num: 5, names: [['a', 'e'], ['b, c']] } 59 | } 60 | 61 | const [_, actions] = riduce(initialState) 62 | 63 | // @dts-jest:fail does not exist on boolean state 64 | actions.bool.create.set 65 | 66 | // @dts-jest:pass exists on object state 67 | actions.obj.create.set 68 | 69 | // @dts-jest:pass exists on root if object 70 | actions.create.set 71 | 72 | // @dts-jest:fail needs an argument 73 | actions.obj.create.set() 74 | 75 | // @dts-jest:fail requires more than one argument 76 | actions.obj.create.set('num') 77 | 78 | // @dts-jest:fail rejects inconsistent second argument 79 | actions.obj.create.set('num', '2') 80 | 81 | // @dts-jest:fail rejects inconsistent property 82 | actions.obj.create.set('notHere', 2) 83 | 84 | // @dts-jest:pass accepts optional property 85 | actions.obj.create.set('here', 'you bet') 86 | 87 | // @dts-jest:pass accepts correct match 88 | actions.obj.create.set('num', 2) 89 | } -------------------------------------------------------------------------------- /src/create/makeTypedCreators.ts: -------------------------------------------------------------------------------- 1 | import { CreateFn } from "../types"; 2 | import makeArrayCreators, { 3 | madeArrayCreators, 4 | } from "../array/makeArrayCreators"; 5 | import makeStringCreators, { 6 | madeStringCreators, 7 | } from "../string/makeStringCreators"; 8 | import { isPlainObject } from "ramda-adjunct"; 9 | import makeObjectCreators, { 10 | madeObjectCreators, 11 | } from "../object/makeObjectCreators"; 12 | import makeBooleanCreators, { 13 | madeBooleanCreators, 14 | } from "../boolean/makeBooleanCreators"; 15 | import makeNumberCreators, { 16 | madeNumberCreators, 17 | } from "../number/makeNumberCreators"; 18 | import makeCreatorOfTypeFromPath from "./makeCreatorOfTypeFromPath"; 19 | 20 | function makeTypedCreators( 21 | leafState: L, 22 | path: (string | number)[] 23 | ): CreateFn { 24 | // Array creators 25 | if (Array.isArray(leafState)) { 26 | return makeArrayCreators(leafState, path); 27 | } 28 | 29 | // String creators 30 | if (typeof leafState === "string") { 31 | return makeStringCreators(leafState, path); 32 | } 33 | 34 | if (typeof leafState === "boolean") { 35 | return makeBooleanCreators(leafState, path); 36 | } 37 | 38 | if (typeof leafState === "number") { 39 | return makeNumberCreators(leafState, path); 40 | } 41 | 42 | // Object creators 43 | if (isPlainObject(leafState)) { 44 | return makeObjectCreators(leafState, path); 45 | } 46 | 47 | if (typeof leafState === "undefined") { 48 | const makeCreatorOfType = makeCreatorOfTypeFromPath(path); 49 | 50 | return (passedType?: string) => { 51 | // const asArray = Array.isArray(leafState) ? leafState : []; 52 | // const asBoolean = 53 | // typeof leafState === "boolean" ? leafState : Boolean(leafState); 54 | // const asNumber = 55 | // typeof leafState === "number" ? leafState : Number(leafState); 56 | // const asString = 57 | // typeof leafState === "string" ? leafState : String(leafState); 58 | // const asObject = isPlainObject(leafState) ? leafState : Object(leafState); 59 | 60 | return { 61 | ...madeArrayCreators([], path, makeCreatorOfType, passedType), 62 | ...madeBooleanCreators(false, path, makeCreatorOfType, passedType), 63 | ...madeNumberCreators(0, path, makeCreatorOfType, passedType), 64 | ...madeObjectCreators({}, path, makeCreatorOfType, passedType), 65 | ...madeStringCreators("", path, makeCreatorOfType, passedType), 66 | }; 67 | }; 68 | } 69 | 70 | return (_?: string) => ({}); 71 | } 72 | 73 | export default makeTypedCreators; 74 | -------------------------------------------------------------------------------- /docs/old/defaults/assign.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../../../src'; 2 | 3 | // @dts-jest:group Inferred state 4 | { 5 | const initialState = { 6 | bool: false, 7 | num: 2, 8 | str: 'foo', 9 | arr: [1, 2, 3], 10 | nested: { deep: true }, 11 | obj: { num: 5, names: [['a', 'e'], ['b, c']] } 12 | } 13 | 14 | const [_, actions] = riduce(initialState) 15 | 16 | // @dts-jest:fail does not exist on boolean state 17 | actions.bool.create.assign 18 | 19 | // @dts-jest:pass exists on object state 20 | actions.obj.create.assign 21 | 22 | // @dts-jest:pass exists on root if object 23 | actions.create.assign 24 | 25 | // @dts-jest:fail needs an argument 26 | actions.obj.create.assign() 27 | 28 | // @dts-jest:pass accepts partial match 29 | actions.obj.create.assign({ num: 2 }) 30 | 31 | // @dts-jest:fail rejects inconsistent match 32 | actions.obj.create.assign({ num: '2' }) 33 | 34 | // @dts-jest:fail rejects inconsistent property 35 | actions.nested.create.assign({ banana: true }) 36 | 37 | // @dts-jest:pass accepts empty object 38 | actions.obj.create.assign({}) 39 | 40 | // @dts-jest:pass accepts full match 41 | actions.obj.create.assign({ num: 101, names: [['c', 'a', 't']] }) 42 | } 43 | 44 | // @dts-jest:group Explicitly typed state 45 | { 46 | interface State { 47 | bool: boolean, 48 | num: number, 49 | str: string, 50 | arr: number[], 51 | nested: { deep: boolean, here?: string }, 52 | obj: { num: number, names: string[][] } 53 | } 54 | 55 | const initialState: State = { 56 | bool: false, 57 | num: 2, 58 | str: 'foo', 59 | arr: [1, 2, 3], 60 | nested: { deep: true }, 61 | obj: { num: 5, names: [['a', 'e'], ['b, c']] } 62 | } 63 | 64 | const [_, actions] = riduce(initialState) 65 | 66 | // @dts-jest:fail does not exist on boolean state 67 | actions.bool.create.assign 68 | 69 | // @dts-jest:pass exists on object state 70 | actions.obj.create.assign 71 | 72 | // @dts-jest:pass exists on root if object 73 | actions.create.assign 74 | 75 | // @dts-jest:fail needs an argument 76 | actions.obj.create.assign() 77 | 78 | // @dts-jest:pass accepts partial match 79 | actions.obj.create.assign({ num: 2 }) 80 | 81 | // @dts-jest:fail rejects inconsistent match 82 | actions.obj.create.assign({ num: '2' }) 83 | 84 | // @dts-jest:fail rejects inconsistent property 85 | actions.nested.create.assign({ banana: true }) 86 | 87 | // @dts-jest:pass accepts optional property 88 | actions.nested.create.assign({ here: 'you bet' }) 89 | 90 | // @dts-jest:pass accepts empty object 91 | actions.obj.create.assign({}) 92 | 93 | // @dts-jest:pass accepts full match 94 | actions.obj.create.assign({ num: 101, names: [['c', 'a', 't']] }) 95 | } -------------------------------------------------------------------------------- /src/object/object-types.ts: -------------------------------------------------------------------------------- 1 | import { ActionWithPayload, Action } from "../types"; 2 | 3 | export enum ObjectCreatorKeys { 4 | ASSIGN = "ASSIGN", 5 | PATH = "PATH", 6 | PUSHED_SET = "PUSHED_SET", 7 | SET = "SET", 8 | } 9 | 10 | export type ObjectCreators = { 11 | assign(props: Partial): ActionWithPayload; 12 | path( 13 | route: (string | number)[], 14 | value: V 15 | ): ActionWithPayload<{ path: (string | number)[]; value: V }>; 16 | pushedSet: PushedSet; 17 | set( 18 | key: KeyT, 19 | value: LeafT[KeyT] 20 | ): ActionWithPayload<{ key: KeyT; value: LeafT[KeyT] }>; 21 | }; 22 | 23 | type PushedSet = ( 24 | arg: L[keyof L] | PushedSetCallback 25 | ) => ActionWithPayload>; 26 | 27 | type PushedSetCallback = (id: string) => L[keyof L]; 28 | type PushedSetWithValue = (val: L[keyof L]) => ActionWithPayload; 29 | type PushedSetWithCallback = ( 30 | cb: PushedSetCallback 31 | ) => ActionWithPayload>; 32 | 33 | export type ObjectActions< 34 | KeyT extends keyof ObjectCreators, 35 | LeafT = unknown, 36 | TreeT = unknown 37 | > = ReturnType[KeyT]>; 38 | 39 | export function isObjectAction(action: Action): boolean { 40 | return ( 41 | isAssignAction(action) || 42 | isPathAction(action) || 43 | isPushedSetAction(action) || 44 | isPushedSetValueAction(action) || 45 | isPushedSetCallbackAction(action) || 46 | isSetAction(action) 47 | ); 48 | } 49 | 50 | export function isAssignAction( 51 | action: Action 52 | ): action is ObjectActions<"assign", L> { 53 | return action.leaf.CREATOR_KEY === ObjectCreatorKeys.ASSIGN; 54 | } 55 | 56 | export function isPathAction( 57 | action: Action 58 | ): action is ObjectActions<"path", L> { 59 | return action.leaf.CREATOR_KEY === ObjectCreatorKeys.PATH; 60 | } 61 | 62 | export function isPushedSetAction( 63 | action: Action 64 | ): action is ObjectActions<"pushedSet", L> { 65 | return action.leaf.CREATOR_KEY === ObjectCreatorKeys.PUSHED_SET; 66 | } 67 | 68 | export function isPushedSetValueAction( 69 | action: Action 70 | ): action is ReturnType> { 71 | return isPushedSetAction(action) && typeof action.payload !== "function"; 72 | } 73 | 74 | export function isPushedSetCallbackAction( 75 | action: Action 76 | ): action is ReturnType> { 77 | return isPushedSetAction(action) && typeof action.payload === "function"; 78 | } 79 | 80 | export function isSetAction( 81 | action: Action 82 | ): action is ObjectActions<"set", L> { 83 | return action.leaf.CREATOR_KEY === ObjectCreatorKeys.SET; 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/generatePushID.ts: -------------------------------------------------------------------------------- 1 | // Public gist: https://gist.github.com/mikelehen/3596a30bd69384624c11 2 | 3 | /** 4 | * Fancy ID generator that creates 20-character string identifiers with the following properties: 5 | * 6 | * 1. They're based on timestamp so that they sort *after* any existing ids. 7 | * 2. They contain 72-bits of random data after the timestamp so that IDs won't collide with other clients' IDs. 8 | * 3. They sort *lexicographically* (so the timestamp is converted to characters that will sort properly). 9 | * 4. They're monotonically increasing. Even if you generate more than one in the same timestamp, the 10 | * latter ones will sort after the former ones. We do this by using the previous random bits 11 | * but "incrementing" them by 1 (only in the case of a timestamp collision). 12 | */ 13 | const generatePushID: () => string = (function () { 14 | // Modeled after base64 web-safe chars, but ordered by ASCII. 15 | var PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'; 16 | 17 | // Timestamp of last push, used to prevent local collisions if you push twice in one ms. 18 | var lastPushTime = 0; 19 | 20 | // We generate 72-bits of randomness which get turned into 12 characters and appended to the 21 | // timestamp to prevent collisions with other clients. We store the last characters we 22 | // generated because in the event of a collision, we'll use those same characters except 23 | // "incremented" by one. 24 | var lastRandChars: any[] = []; 25 | 26 | return function () { 27 | var now = new Date().getTime(); 28 | var duplicateTime = (now === lastPushTime); 29 | lastPushTime = now; 30 | 31 | var timeStampChars = new Array(8); 32 | for (var i = 7; i >= 0; i--) { 33 | timeStampChars[i] = PUSH_CHARS.charAt(now % 64); 34 | // NOTE: Can't use << here because javascript will convert to int and lose the upper bits. 35 | now = Math.floor(now / 64); 36 | } 37 | if (now !== 0) throw new Error('We should have converted the entire timestamp.'); 38 | 39 | var id = timeStampChars.join(''); 40 | 41 | if (!duplicateTime) { 42 | for (i = 0; i < 12; i++) { 43 | lastRandChars[i] = Math.floor(Math.random() * 64); 44 | } 45 | } else { 46 | // If the timestamp hasn't changed since last push, use the same random number, except incremented by 1. 47 | for (i = 11; i >= 0 && lastRandChars[i] === 63; i--) { 48 | lastRandChars[i] = 0; 49 | } 50 | lastRandChars[i]++; 51 | } 52 | for (i = 0; i < 12; i++) { 53 | id += PUSH_CHARS.charAt(lastRandChars[i]); 54 | } 55 | if (id.length != 20) throw new Error('Length should be 20.'); 56 | 57 | return id; 58 | }; 59 | })(); 60 | 61 | export default generatePushID -------------------------------------------------------------------------------- /docs/old/examples/intermediateExample.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intermediate-example 3 | title: Intermediate example 4 | hide_title: true 5 | sidebar_label: Intermediate example 6 | --- 7 | 8 | # Intermediate example: custom logic 9 | 10 | [Play around with this code on Runkit](https://runkit.com/richardcrng/redux-leaves-custom-logic) 11 | 12 | **Situation**: I want to define a general type of reducer logic that can be reused on any arbitrary slice of state. 13 | 14 | **Complication**: I want to do this as quickly, painlessly and intuitively as possible. 15 | 16 | **Question**: Do I really have to create sub-reducers with the same underlying logic? 17 | 18 | Answer: no! Just provide Redux-Leaves once with your custom reducer logic, and you can automatically use it at any leaf of your state tree. 19 | 20 | ## Demonstration 21 | 22 | ### Set up with your custom reducer logic 23 | ```js 24 | import { createStore } from 'redux' 25 | import riduce from 'redux-leaves' 26 | 27 | const initialState = { 28 | counter: 2, 29 | list: ['first', 'second'], 30 | nested: { arbitrarily: { deep: 0 } } 31 | } 32 | 33 | // Key your reducer logic by a descriptive verb 34 | const riducerDict = { 35 | double: leafState => leafState * 2, 36 | appendToEach: (leafState, action) => leafState.map(str => str.concat(action.payload)), 37 | countTreeKeys: (leafState, action, treeState) => Object.keys(treeState).length 38 | } 39 | 40 | // Provide the dictionary of your reducer logic to riduce 41 | const [reducer, actions] = riduce(initialState, riducerDict) 42 | const store = createStore(reducer) 43 | ``` 44 | 45 | ### Dispatch actions at any leaf with the corresponding keys 46 | ```js 47 | store.dispatch(actions.counter.create.double()) 48 | console.log(store.getState().counter) // 4 49 | 50 | store.dispatch(actions.list.create.appendToEach(' item')) // ' item' will be the action payload 51 | console.log(store.getState().list) // ['first item', 'second item'] 52 | 53 | store.dispatch(actions.nested.arbitrarily.deep.create.countTreeKeys()) 54 | console.log(store.getState().nested.arbitrarily.deep) // 3 55 | 56 | // And to demonstrate reusing logic at an arbitrary leaf: 57 | store.dispatch(actions.nested.arbitrarily.deep.create.double()) 58 | console.log(store.getState().nested.arbitrarily.deep) // 6 59 | ``` 60 | 61 | ## Default handling of arguments 62 | When you supply `riduce` with custom reducer logic, it provides the corresponding action creators, e.g. `actions.list.create.appendToEach` used above. 63 | 64 | The *default behaviour* of these action creators is that, if they receive any arguments, *only the first argument* is passed to the created action as a payload: 65 | 66 | ```js 67 | const actionToAppend = actions.list.create.appendToEach('foo', 'bar') 68 | console.log(actionToAppend.payload) // 'foo' 69 | ``` 70 | 71 | If you would like to customise this behaviour, look at the [advanced example](advancedExample.md). -------------------------------------------------------------------------------- /docs/old/examples/typescriptExample.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: typescript-example 3 | title: TypeScript example 4 | hide_title: true 5 | sidebar_label: TypeScript example 6 | --- 7 | 8 | # TypeScript example 9 | 10 | Redux-Leaves is written in TypeScript and gives most typings by default. 11 | 12 | You can help it out by being explicit in your custom reducer typings (if you have any): 13 | 14 | ```typescript 15 | import { createStore } from 'redux' 16 | import riduce, { bundle, LeafReducer } from 'redux-leaves' 17 | 18 | // Declare an interface or type for your state shape 19 | interface State { 20 | list: string[], 21 | nested: { 22 | counter: number, 23 | state: { 24 | deep: string 25 | } 26 | } 27 | } 28 | 29 | const initialState: State = { 30 | list: ['a', 'b'], 31 | nested: { 32 | counter: 0, 33 | state: { 34 | deep: 'somewhat' 35 | } 36 | } 37 | } 38 | 39 | // Declare your custom ReducerSchemas 40 | interface ReducerSchemas { 41 | /* 42 | addAll: 43 | 1. reducer acts on number leaf state 44 | 2. action creator arguments is an array of numbers 45 | 3. action payload is an array of numbers 46 | */ 47 | addAll: LeafReducer.Schema, 48 | 49 | /* 50 | insertEarliest: 51 | 1. reducer acts on an array of strings 52 | 2. action creator takes a single argument, a number 53 | 3. action payload is a single number 54 | */ 55 | duplicateIndex: LeafReducer.Schema 56 | 57 | /* 58 | exponentiate: 59 | 1. reducer acts on 60 | */ 61 | exponentiate: LeafReducer.Schema, 62 | } 63 | 64 | const reducerDict: ReducerSchemas = { 65 | addAll: { 66 | reducer: (leafState, action) => { 67 | // TS picks up that: 68 | // 1. leafState is a number 69 | // 2. action.payload is an array of numbers 70 | action.payload.reducer((acc, val) => acc + val, leafState) 71 | }, 72 | // TS picks up that the return value of argsToPayload 73 | // should be an array of numbers 74 | argsToPayload: (...args) => args 75 | }, 76 | duplicatePayload: { 77 | // TS picks up that: 78 | // 1. leafState is an array of strings 79 | // 2. action.payload is a number 80 | reducer: (leafState, action) => [...leafState, leafState[action.payload]], 81 | // TS picks up that argsToPayload should 82 | // take one argument, a number, and 83 | // return a number 84 | argsToPayload: (index) => index 85 | }, 86 | exponentiate: { 87 | // TS picks up that: 88 | // 1. leafState is a number 89 | // 2. action.payload is a number 90 | reducer: (leafState, action) => Math.pow(leafState, action.payload), 91 | // TS picks up that the return value of argsToPayload 92 | // should be a number 93 | argsToPayload: (power) => power 94 | } 95 | } 96 | 97 | 98 | const [reducer, actions] = riduce(initialState, riducerDict) 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/old/intro/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: overview 3 | title: Overview 4 | hide_title: true 5 | --- 6 | 7 | # Overview 8 | 9 | The guiding philosophy of Redux-Leaves is *"write once, reduce anywhere"*. 10 | 11 | This page explains more about the motivation of Redux-Leaves and how its design philosophy is put into practice. 12 | 13 | > **Just want to see some code? Check out the basic [30 second demo](examples/basicExample.md).** 14 | 15 | ## Motivation 16 | 17 | ### Why? 18 | 19 | Redux is useful, powerful and great! 20 | 21 | But some developers complain about the boilerplate being [tedious, cumbersome and convoluted](https://medium.com/@Charles_Stover/no-boilerplate-global-state-management-in-react-41e905944eb7). 22 | 23 | Can we make it easier for developers to get the use, power and great developer experience from Redux? 24 | 25 | > **Redux-Leaves aims to make Redux easier to learn *and* quicker to scale**. 26 | 27 | ### How? 28 | 29 | Let's consider some of the developer complaints against Redux. 30 | 31 | * "Redux requires a ton of tedious code to do the most basic things" 32 | * "It often feels tedious to do trivial state changes" 33 | * "Redux has too much boilerplate, and I have to maintain all of it" 34 | * "New developers have a problem with flux architecture and functional concepts" 35 | * "Redux requires a ton of files to establish a new reducer" 36 | 37 | Maybe that developer experience would be better if we could: 38 | 39 | > 1. **Quickly set up** Redux for basic state changes; 40 | > 2. **Intuitively create actions** for arbitrary needs; and 41 | > 3. **Cut down boilerplate** of reducers drastically? 42 | 43 | ### What? 44 | 45 | Redux-Leaves lets you *write once, reduce anywhere* with: 46 | - [Quick setup](features.md#quick-setup); 47 | - [Intuitive API](features.md#intuitive-api); and 48 | - [Minimal boilerplate](features.md#minimal-boilerplate). 49 | 50 | #### Example 51 | ```js 52 | import { createStore } from 'redux' 53 | import riduce, { bundle } = from 'redux-leaves' 54 | 55 | // set up with initial state 56 | const initialState = { 57 | counter: 0, 58 | list: [], 59 | props: {} 60 | } 61 | 62 | const [reducer, actions] = riduce(initialState) 63 | const store = createStore(reducer) 64 | 65 | // setup complete! Now dispatch actions to your heart's content 66 | 67 | console.log(store.getState()) 68 | // => { counter: 0, list: [], props: {} } 69 | 70 | store.dispatch(actions.counter.create.increment(10)) 71 | console.log(store.getState()) 72 | // => { counter: 10, list: [], props: {} } 73 | 74 | store.dispatch(actions.list.create.push('foo')) 75 | console.log(store.getState()) 76 | // => { counter: 10, list: ['foo'], props: {} } 77 | 78 | const bundleAction = bundle([ 79 | actions.counter.create.reset(), 80 | actions.list[0].create.concat('bar'), 81 | actions.props.at.arbitrary.path.create.update('here I am!') 82 | ]) 83 | 84 | store.dispatch(bundleAction) 85 | console.log(store.getState()) 86 | /* 87 | => { 88 | counter: 0, 89 | list: ['foobar'], 90 | props: { at: { arbitrary: { path: 'here I am!' } } } 91 | } 92 | */ 93 | ``` 94 | 95 | ### Bonus: `useReducer` usage 96 | 97 | Although Redux-Leaves was written to make it easier to work with Redux, [it also works great with `useReducer`](../examples/useReducerExample.md)! -------------------------------------------------------------------------------- /docs/riduce-advanced.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import riduce from '../src/riduce' 2 | import { Riducer } from '../src/types' 3 | 4 | // @dts-jest:group shorthand examples 5 | { 6 | const restaurantState = { 7 | tables: [ 8 | { persons: 4, hasOrdered: false, hasPaid: false }, 9 | { persons: 3, hasOrdered: true, hasPaid: false } 10 | ], 11 | stock: { 12 | ramen: { 13 | beef: 5, 14 | veg: 2 15 | }, 16 | sushi: { 17 | nigiri: 10, 18 | sashimi: 4 19 | } 20 | } 21 | } 22 | 23 | type Table = typeof restaurantState['tables'][0] 24 | 25 | const finishTable = (tableState: Table) => ({ 26 | ...tableState, 27 | hasOrdered: true, 28 | hasPaid: true 29 | }) 30 | 31 | const decreaseValuesBy = (leafState: Record, action: any) => { 32 | const keys = Object.keys(leafState) 33 | return keys.reduce((acc, key) => ({ 34 | ...acc, 35 | [key]: leafState[key] - action.payload 36 | }), {}) 37 | } 38 | 39 | const [reducer, actions] = riduce(restaurantState, { 40 | finishTable, 41 | decreaseValuesBy 42 | }) 43 | 44 | // @dts-jest:pass 45 | actions.tables[0].create.finishTable() 46 | 47 | // @dts-jest:fail 48 | actions.stock.ramen.create.finishTable() 49 | 50 | // @dts-jest:pass 51 | actions.stock.ramen.create.decreaseValuesBy(1) 52 | 53 | // @dts-jest:pass 54 | actions.stock.sushi.create.decreaseValuesBy(4) 55 | 56 | // @dts-jest:fail 57 | actions.tables.create.decreaseValuesBy() 58 | } 59 | 60 | // @dts-jest:group longhand examples 61 | { 62 | const bookstoreState = { 63 | books: { 64 | 9780007925568: { 65 | title: 'Moby Dick', 66 | authorName: 'Herman Melville', 67 | stock: 7 68 | }, 69 | 9780486280615: { 70 | title: 'The Adventures of Huckleberry Finn', 71 | authorName: 'Mark Twain', 72 | stock: 10 73 | }, 74 | 9780764502231: { 75 | title: 'JavaScript for Dummies', 76 | authorName: 'Emily A. Vander Veer', 77 | stock: 5 78 | } 79 | }, 80 | visitor: { 81 | count: 2, 82 | guestbook: [] 83 | } 84 | } 85 | 86 | type BookstoreState = typeof bookstoreState 87 | 88 | interface BookReview { 89 | id: keyof BookstoreState['books'], 90 | stars: number, 91 | comment?: string 92 | } 93 | 94 | const addBookReviews: Riducer<{ 95 | treeState: BookstoreState, 96 | leafState: string[], 97 | args: BookReview[], 98 | payload: BookReview[] 99 | }> = { 100 | argsToPayload: (...reviews) => reviews, 101 | reducer: (leafState, { payload: reviews = [] }, treeState) => { 102 | return reviews.reduce((acc, { stars, id, comment }) => ([ 103 | ...acc, 104 | `${stars} stars for ${treeState.books[id].title}! ${comment}` 105 | ]), leafState) 106 | } 107 | } 108 | 109 | const [reducer, actions] = riduce(bookstoreState, { addBookReviews }) 110 | 111 | // @dts-jest:fail 112 | actions.create.addBookReviews([]) 113 | 114 | // @dts-jest:fail 115 | actions.visitor.guestbook.create.addBookReviews({ id: '9780007925568', stars: 4.5 }) 116 | 117 | actions.visitor.guestbook.create.addBookReviews( 118 | { id: 9780007925568, stars: 4.5 }, 119 | { id: 9780764502231, stars: 5, comment: 'so great!!' } 120 | ) 121 | } 122 | 123 | -------------------------------------------------------------------------------- /docs/old/api/bundle.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | import riduce, { bundle } from '../../../src'; 3 | 4 | describe('bundle bundles together actions into a single one', () => { 5 | describe("Actions array, no type", () => { 6 | const initialState = { 7 | counter: 0, 8 | list: ['a'] 9 | } 10 | 11 | const [reducer, actions] = riduce(initialState) 12 | const store = createStore(reducer) 13 | 14 | test('Group bundles actions together into a single update with default type provided', () => { 15 | const incrementAndPush = bundle([ 16 | actions.counter.create.increment(), 17 | actions.list.create.push('b') 18 | ]) 19 | 20 | expect(incrementAndPush.type).toBe('counter/INCREMENT; list/PUSH') 21 | 22 | store.dispatch(incrementAndPush) 23 | expect(store.getState()).toEqual({ counter: 1, list: ['a', 'b'] }) 24 | }) 25 | }) 26 | 27 | describe("Actions array, type provided", () => { 28 | const initialState = { 29 | counter: 0, 30 | list: ['a'] 31 | } 32 | 33 | const [reducer, actions] = riduce(initialState) 34 | const store = createStore(reducer) 35 | 36 | test('Returns an action of appropriate type and effect in reducer', () => { 37 | const incrementAndPush = bundle([ 38 | actions.counter.create.increment(), 39 | actions.list.create.push('b') 40 | ], 'INCREMENT_AND_PUSH') 41 | 42 | expect(incrementAndPush.type).toBe('INCREMENT_AND_PUSH') 43 | expect(incrementAndPush.leaf.bundled).toEqual(['counter/INCREMENT', 'list/PUSH']) 44 | 45 | store.dispatch(incrementAndPush) 46 | expect(store.getState()).toEqual({ counter: 1, list: ['a', 'b'] }) 47 | }) 48 | }) 49 | 50 | describe("Order matters", () => { 51 | const initialState = { 52 | counter: 0, 53 | list: [5] 54 | } 55 | 56 | const [reducer, actions] = riduce(initialState) 57 | const store = createStore(reducer) 58 | 59 | test('Processes actions in the order passed into the array', () => { 60 | const incrementThenPush = bundle([ 61 | actions.counter.create.increment(), 62 | actions.list.create.do((leafState, treeState) => [...leafState, treeState.counter]) 63 | ]) 64 | 65 | const pushThenIncrement = bundle([ 66 | actions.list.create.do((leafState, treeState) => [...leafState, treeState.counter]), actions.counter.create.increment() 67 | ]) 68 | 69 | store.dispatch(incrementThenPush) 70 | expect(store.getState()).toEqual({ counter: 1, list: [5, 1] }) 71 | 72 | store.dispatch(pushThenIncrement) 73 | expect(store.getState()).toEqual({ counter: 2, list: [5, 1, 1] }) 74 | }) 75 | }) 76 | 77 | describe("Compound bundling", () => { 78 | const initialState = { 79 | counter: 0, 80 | list: ['a'] 81 | } 82 | 83 | const [reducer, actions] = riduce(initialState) 84 | const store = createStore(reducer) 85 | 86 | test('Bundle bundles actions together into a single update', () => { 87 | const incrementAndPush = bundle([ 88 | actions.counter.create.increment(), 89 | actions.list.create.push('b') 90 | ]) 91 | 92 | const incrementAndPushAndIncrement = bundle([ 93 | incrementAndPush, 94 | actions.counter.create.increment() 95 | ]) 96 | 97 | store.dispatch(incrementAndPushAndIncrement) 98 | expect(store.getState()).toEqual({ counter: 2, list: ['a', 'b'] }) 99 | }) 100 | }) 101 | }) -------------------------------------------------------------------------------- /docs/old/intro/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: features 3 | title: Features 4 | hide_title: true 5 | --- 6 | 7 | # Features 8 | 9 | ## Quick setup 10 | It takes just 30 seconds to set up your reducer, actions and store with Redux-Leaves! 11 | 12 | 1. Pass some initial state to `riduce` 13 | 2. Grab the returned `reducer` and `actions` 14 | 3. Create your store and dispatch away! 15 | 16 | ```js 17 | import { createStore } from 'redux' 18 | import riduce from 'redux-leaves' 19 | 20 | // 1. Pass some initial state to riduce; and 21 | // 2. Grab the returned reducer and actions 22 | const [reducer, actions] = riduce({ 23 | counter: 0, 24 | list: [], 25 | arbitrary: { nested: { property: false } } 26 | }) 27 | 28 | // 3. Create your store and dispatch away! 29 | const store = createStore(reducer) 30 | store.dispatch(actions.counter.create.increment()) 31 | store.dispatch(actions.list.create.push('foobar')) 32 | store.dispatch(actions.arbitrary.nested.property.create.toggle()) 33 | 34 | console.log(store.getState()) 35 | 36 | /* 37 | { 38 | counter: 0, 39 | list: ['foobar'], 40 | arbitrary: { nested: { property: true } } 41 | } 42 | */ 43 | ``` 44 | 45 | Here's the [full list of action creators](../defaults/README.md) that can be accessed from `actions`. 46 | 47 | ## Intuitive API 48 | 49 | Continuing on from the example above: suppose that I want to set a new property at `arbitrary.nested` state, with the key `deep` and the value `true`. 50 | 51 | Here are the steps to update our store's state: 52 | 53 | 1. Navigate to the appropriate path from `actions` 54 | 2. Access action creators through `create` 55 | 3. Key into the appropriate action creator 56 | 4. Execute to create the action, and dispatch it! 57 | 58 | ```js 59 | // 1. Navigate to the appropriate path from actions 60 | const arbitaryNestedPathFromActions = actions.arbitrary.nested 61 | 62 | // 2. Access action creators through create 63 | const arbitraryNestedActionCreators = arbitraryNestedPathFromActions.create 64 | 65 | // 3. Key into the appropriate action creator 66 | const setArbitraryNestedState = arbitraryNestedActionCreators.set 67 | 68 | // 4. Execute to create the action, and dispatch it! 69 | store.dispatch(setArbitraryNestedState('deep', true)) 70 | console.log(store.getState().arbitrary.nested.deep) // true 71 | 72 | // or, in one line: 73 | store.dispatch(actions.arbitrary.nested.create.set('deep', false)) 74 | console.log(store.getState().arbitrary.nested.deep) // false 75 | ``` 76 | 77 | ## Minimal boilerplate 78 | 79 | If you want to extend the action creators available, you can define some reducer logic and access it at any arbitrary slice of state by passing it into `riduce` in a [`riducerDict`](../README.md#reducersdict). 80 | 81 | ```js 82 | import { createStore } from 'redux' 83 | import riduce from 'redux-leaves' 84 | 85 | const initialState = { 86 | title: 'foobar', 87 | some: { long: { description: 'pretty great' } } 88 | } 89 | 90 | // Reducer logic: capitalise some leaf state 91 | const capitalise = (leafState) => leafState.toUpperCase() 92 | 93 | // Second optional argument of riduce is a riducerDict 94 | const [reducer, actions] = riduce(initialState, { capitalise }) 95 | const store = createStore(reducer) 96 | 97 | // Access the action creator by the same key 98 | store.dispatch(actions.title.create.capitalise()) 99 | console.log(store.getState().title) // 'FOOBAR' 100 | 101 | // Dispatch it to any arbitrary slice of state 102 | store.dispatch(actions.some.long.description.capitalise()) 103 | console.log(store.getState().some.long.description) // 'PRETTY GREAT' 104 | ``` -------------------------------------------------------------------------------- /src/use-riducer/useRiducer.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from "@testing-library/react-hooks"; 2 | import useRiducer from "./useRiducer"; 3 | import bundle from "../bundle"; 4 | 5 | describe("useRiducer", () => { 6 | it("provides state, dispatch and actions", () => { 7 | const initialState = { 8 | counter: 0, 9 | messages: ["hello world!"], 10 | nested: { 11 | state: { 12 | isHard: true, 13 | }, 14 | }, 15 | }; 16 | 17 | const { result } = renderHook(() => useRiducer(initialState)); 18 | 19 | expect(result.current.state).toEqual(initialState); 20 | expect(typeof result.current.dispatch).toBe("function"); 21 | expect(result.current.actions).toHaveProperty("create"); 22 | }); 23 | 24 | it("lets you dispatch actions that then update state as expected", () => { 25 | const initialState = { 26 | counter: 0, 27 | messages: ["hello world!"], 28 | nested: { 29 | state: { 30 | isHard: true, 31 | }, 32 | }, 33 | }; 34 | 35 | const { result } = renderHook(() => useRiducer(initialState)); 36 | const newMessage = "this is a new thing!"; 37 | 38 | act(() => { 39 | const action = result.current.actions.messages.create.push(newMessage); 40 | result.current.dispatch(action); 41 | }); 42 | 43 | expect(result.current.state.messages).toHaveLength(2); 44 | expect(result.current.state.messages).toEqual([ 45 | ...initialState.messages, 46 | newMessage, 47 | ]); 48 | }); 49 | 50 | it("can process sequential dispatches", () => { 51 | const initialState = { 52 | counter: 0, 53 | messages: ["hello world!"], 54 | nested: { 55 | state: { 56 | isHard: true, 57 | }, 58 | }, 59 | }; 60 | 61 | const { result } = renderHook(() => useRiducer(initialState)); 62 | const newMessage = "second in my bundle"; 63 | 64 | act(() => { 65 | result.current.dispatch( 66 | result.current.actions.messages.create.push(newMessage) 67 | ); 68 | result.current.dispatch( 69 | result.current.actions.counter.create.increment(1000) 70 | ); 71 | result.current.dispatch( 72 | result.current.actions.nested.state.isHard.create.toggle() 73 | ); 74 | }); 75 | 76 | expect(result.current.state).toEqual({ 77 | counter: 1000, 78 | messages: [...initialState.messages, newMessage], 79 | nested: { 80 | state: { 81 | isHard: false, 82 | }, 83 | }, 84 | }); 85 | }); 86 | 87 | it("can process bundled updates", () => { 88 | const initialState = { 89 | counter: 0, 90 | messages: ["hello world!"], 91 | nested: { 92 | state: { 93 | isHard: true, 94 | }, 95 | }, 96 | }; 97 | 98 | const { result } = renderHook(() => useRiducer(initialState)); 99 | const newMessage = "second in my bundle"; 100 | 101 | act(() => { 102 | result.current.dispatch( 103 | bundle([ 104 | result.current.actions.messages.create.push(newMessage), 105 | result.current.actions.counter.create.increment(1000), 106 | result.current.actions.nested.state.isHard.create.toggle(), 107 | ]) 108 | ); 109 | }); 110 | 111 | expect(result.current.state).toEqual({ 112 | counter: 1000, 113 | messages: [...initialState.messages, newMessage], 114 | nested: { 115 | state: { 116 | isHard: false, 117 | }, 118 | }, 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /src/custom/custom-types.ts: -------------------------------------------------------------------------------- 1 | import { OmitByValue } from 'utility-types'; 2 | import { Action, LeafData } from "../types" 3 | 4 | export type CustomAction = Action & { 5 | leaf: LeafData & { 6 | custom: true 7 | }, 8 | payload: PayloadT 9 | } 10 | 11 | export type RiducerArgsToPayoad = (...args: ArgsT) => PayloadT 12 | 13 | export type RiducerReducer = (leafState: LeafT, action: CustomAction, treeState: TreeT) => LeafT 14 | 15 | export type PermissiveRiducer = ShorthandPermissiveRiducer | LonghandPermissiveRiducer 16 | 17 | export type Riducer = LonghandRiducer | ShorthandRiducer 20 | 21 | export type RiducerDict }> = { 22 | [K in keyof DefinitionsT]: PermissiveRiducer 23 | } 24 | 25 | export type LonghandPermissiveRiducer = 26 | LonghandRiducer<{ 27 | treeState: TreeT, leafState: any, payload: any, args: [any] 28 | }> 29 | 30 | export type LonghandRiducer = { 33 | 34 | argsToPayload: RiducerArgsToPayoad< 35 | T['payload'], 36 | T['args'] extends unknown[] ? T['args'] : unknown[] 37 | >, 38 | 39 | reducer: RiducerReducer< 40 | T['treeState'], 41 | T['leafState'], 42 | T['payload'] 43 | >, 44 | 45 | type?: string 46 | } 47 | 48 | export type ShorthandPermissiveRiducer = 49 | ShorthandRiducer<{ 50 | treeState: TreeT, leafState: any, payload: any, args: [any] 51 | }> 52 | 53 | export type ShorthandRiducer = LonghandRiducer['reducer'] 56 | 57 | export type LonghandCreator = (...args: Parameters) => CustomAction> 58 | 59 | export type ShorthandCreator = (payload?: PayloadT) => CustomAction 60 | 61 | export interface RiducerGeneric< 62 | TreeT = unknown, 63 | LeafT = unknown, 64 | PayloadT = unknown, 65 | ArgsT extends unknown[] = unknown[] 66 | > { 67 | treeState?: TreeT, 68 | leafState?: LeafT, 69 | payload?: PayloadT, 70 | args?: ArgsT 71 | } 72 | 73 | export type CustomCreators< 74 | LeafT, 75 | TreeT, 76 | RiducerDictT extends RiducerDict 77 | > = OmitByValue, never> 78 | 79 | export type CustomCreatorsAll< 80 | LeafT, 81 | TreeT, 82 | RiducerDictT extends RiducerDict 83 | > = { 84 | [K in keyof RiducerDictT]: 85 | RiducerDictT[K] extends LonghandPermissiveRiducer 86 | ? LeafT extends Parameters[0] 87 | ? LonghandCreator 88 | : never : 89 | RiducerDictT[K] extends LonghandPermissiveRiducer['reducer'] 90 | ? LeafT extends Parameters[0] 91 | ? ShorthandCreator 92 | : never 93 | : never 94 | } 95 | 96 | export function isCustomAction(action: Action): action is CustomAction { 97 | return !!action.leaf.custom 98 | } 99 | 100 | export function isShorthandReducer(definition: Riducer): definition is ShorthandRiducer { 101 | return typeof definition === 'function' 102 | } 103 | 104 | export function isLonghandReducer(definition: Riducer): definition is LonghandRiducer { 105 | return !isShorthandReducer(definition) 106 | } -------------------------------------------------------------------------------- /docs/old/api/leafReducers.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: leaf-reducers 3 | title: Leaf Reducers 4 | hide_title: true 5 | sidebar_label: Leaf reducers 6 | --- 7 | 8 | # Leaf reducers 9 | 10 | A leaf reducer is a function or configuration object that updates the state of an arbitrary leaf in your state tree. 11 | 12 | They are: 13 | - passed into [`riduce`](../README.md) with a unique [`creatorKey`](creatorKeys.md) as part of [`riducerDict`](README.md#reducersdict); and 14 | - triggered at an arbitrary leaf only by dispatching an action created by the leaf's [`create[creatorKey]`](create.md#createcreatorkey) method. 15 | 16 | ## Syntax 17 | 18 | ### Function (shorthand) 19 | ```js 20 | const shorthandFunction = (leafState, action, treeState) => { 21 | // some logic here 22 | // return the new leafState 23 | } 24 | ``` 25 | 26 | ### Configuration object (longhand) 27 | The above leafReducer function is shorthand for a configuration object with presets: 28 | ```js 29 | const longhandConfig = { 30 | reducer: shorthandFunction, 31 | 32 | // below are the configuration keys and their default values 33 | 34 | argsToPayload: firstArgOnly => firstArgOnly, 35 | // by default, if the action creator is invoked with arguments, 36 | // the first argument only becomes the action's payload property. 37 | } 38 | ``` 39 | 40 | Using the configuration object longhand allows greater customisation, through additional [configuration keys](#configuration-keys). 41 | 42 | ## Configuration keys 43 | 44 | The list of configuration keys that can be provided are below: 45 | 46 | | Key | Value (type) | Description | Optional? | 47 | | --- | --- | --- | --- | 48 | | [`reducer`](#reducer) | function | Updates the leaf's state | | 49 | | [`argsToPayload`](#argstopayload) | function | Converts action creator arguments to an action payload | Optional | 50 | 51 | ### `reducer` 52 | *(function)*: Updates the leaf's state. 53 | 54 | #### Arguments 55 | - `leafState` *(any)*: the current state of the given leaf 56 | - `action` *(object)*: the action created 57 | - `treeState` *(object)*: the current state of the entire Redux store 58 | 59 | #### Returns 60 | The new state value for the leaf. 61 | 62 | ### `argsToPayload` 63 | *(function, optional)*: Converts action creator arguments to an action payload. 64 | 65 | **Default behaviour:** if a first argument is provided, it is supplied as the action's payload. All other arguments are discarded. 66 | 67 | #### Arguments 68 | - `...args`: the arguments supplied to an action creator that triggers [`reducer`](#reducer) 69 | 70 | #### Returns 71 | A `payload` used by the action creator. 72 | 73 | #### Examples 74 | ```js 75 | // Action payload is the first argument only (default behaviour) 76 | const firstArgToPayload = firstArgOnly => firstArgOnly 77 | 78 | // Action payload as an array of the first 5 arguments 79 | const firstFiveArgsToPayload = (...args) => args.slice(0, 5) 80 | 81 | // Action payload as an object 82 | const spreadArgsToObjectPayload = (first, second, ...rest) => ({ first, second, rest }) 83 | ``` 84 | 85 | We can check that these are behaving as expected: 86 | ```js 87 | // Test them out by creating actions using riduce 88 | const returnPayload = (leafState, { payload }) => payload 89 | [ 90 | firstArgToPayload, 91 | firstFiveArgsToPayload, 92 | spreadArgsToObjectPayload 93 | ].forEach(argsToPayload => { 94 | // Use each as an argsToPayload 95 | const returnPayload = { 96 | reducer: (leafState, { payload }) => payload, 97 | argsToPayload 98 | } 99 | const [reducer, actions] = riduce({}, { returnPayload }) 100 | // log out the payload for an action passed seven arguments 101 | console.log(actions.create.returnPayload(1, 2, 3, 4, 5, 6, 7).payload) 102 | }) 103 | 104 | // 1 105 | // [1, 2, 3, 4, 5] 106 | // { first: 1, second: 2, rest: [3, 4, 5, 6, 7] } 107 | ``` -------------------------------------------------------------------------------- /docs/old/api/create.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: create 3 | title: create 4 | hide_title: true 5 | sidebar_label: create 6 | --- 7 | 8 | # `create` 9 | 10 | When you access the `create` property from any arbitrary path from the [`actions`](actions.md) object, you access the Redux-Leaves action creators API. 11 | 12 | Consider the `actions` object returned below. 13 | ```js 14 | import { createStore } from 'redux' 15 | import riduce from 'redux-leaves' 16 | 17 | const initialState = { 18 | counter: 0, 19 | arbitrary: { 20 | nested: { 21 | path: ['hi!'] 22 | } 23 | } 24 | } 25 | 26 | const riducerDict = { 27 | convertToFoobar: () => 'foobar' 28 | } 29 | 30 | const [reducer, actions] = riduce(initialState, riducerDict) 31 | ``` 32 | 33 | * `actions.counter.create` corresponds to creating actions at `state.counter`; 34 | * `actions.arbitrary.nested.path.create` corresponds to creating actions at `state.arbitrary.nested.path`; 35 | * `actions.create` corresponds to creating actions at the `state`'s root. 36 | 37 | ## `create[creatorKey]` 38 | ### Action creators 39 | You can access action creators (functions) from the `create` API at any leaf by using their [`creatorKey`](creatorKeys.md) as a property, both for the default ones and any custom ones you've defined. 40 | 41 | ```js 42 | // Example defaults: update, set, push 43 | console.log(typeof actions.counter.create.update) // 'function' 44 | console.log(typeof actions.arbitrary.nested.create.set) // 'function' 45 | console.log(typeof actions.arbitrary.nested.path.create.push) // 'function' 46 | 47 | // Custom creatorKey of 'convertToFoobar' 48 | console.log(typeof actions.create.convertToFoobar) // 'function' 49 | console.log(typeof actions.arbitrary.nested.path.create.convertToFoobar) // 'function' 50 | ``` 51 | 52 | ### Actions 53 | Executing these functions then create the actions that you should dispatch to your Redux store. 54 | 55 | ```js 56 | const store = createStore(reducer) // using reducer from riduce 57 | console.log(store.getState().counter) // 0 58 | 59 | const updateCounter = actions.counter.create.update 60 | 61 | store.dispatch(updateCounter(5)) 62 | console.log(store.getState().counter) // 5 63 | 64 | store.dispatch(updateCounter(3)) 65 | console.log(store.getState().counter) // 3 66 | ``` 67 | 68 | ### Optional `actionType` argument 69 | Rather than directly accessing action creators from `create`, you can optionally provide an `actionType` string as an argument to `create` before accessing the action creator functions: 70 | 71 | ```js 72 | // Defaults, e.g. update creatorKey 73 | console.log(typeof actions.counter.create.update) // 'function' 74 | console.log(typeof actions.counter.create('UPDATE_COUNTER').update) // 'function' 75 | 76 | // Custom creatorKey of 'convertToFoobar' 77 | console.log(typeof actions.create.convertToFoobar) // 'function' 78 | console.log(typeof actions.create('CONVERT_TO_FOOBAR').convertToFoobar) // 'function' 79 | ``` 80 | 81 | Providing an `actionType` string in this way does not change the way that the reducer will respond to actions; it merely overrides the created action's `type` property to be the string passed in (which might be desirable for a debugging perspective for Redux DevTools, for example): 82 | 83 | ```js 84 | const createDefaultIncrement = actions.counter.create.increment 85 | const createNamedIncrement = actions.counter.create('NAMED_INCREMENT').increment 86 | 87 | console.log(store.getState().counter) // 3 88 | 89 | const defaultIncrement = createDefaultIncrement() 90 | console.log(defaultIncrement.type) // 'counter/INCREMENT' 91 | dispatch(defaultIncrement) 92 | console.log(store.getState().counter) // 4 93 | 94 | const namedIncrement = createNamedIncrement() 95 | console.log(namedIncrement.type) // 'NAMED_INCREMENT' 96 | dispatch(namedIncrement) 97 | console.log(store.getState().counter) // 5 98 | ``` 99 | 100 | (It is safe to override name in this way because the Redux-Leaves `reducer` does not switch over the action's `type`.) -------------------------------------------------------------------------------- /docs/old/examples/advancedExample.spec.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import riduce, { bundle } from '../../../src'; 3 | 4 | 5 | describe('Advanced example', () => { 6 | describe("Bundling actions", () => { 7 | const initialState = { 8 | list: ['a', 'b'], 9 | nested: { 10 | counter: 0, 11 | state: { 12 | deep: 'somewhat', 13 | arbitrary: false 14 | } 15 | } 16 | } 17 | 18 | const [reducer, actions] = riduce(initialState) 19 | const store = createStore(reducer) 20 | 21 | test("Actions get bundled into a single action which updates multiple leaves of state", () => { 22 | const actionBundle = bundle([ 23 | actions.list.create.push('c'), 24 | actions.nested.counter.create.increment(5), 25 | actions.nested.state.create.set('arbitrary', true) 26 | ]) 27 | 28 | store.dispatch(actionBundle) 29 | expect(store.getState()).toEqual({ 30 | list: ['a', 'b', 'c'], 31 | nested: { 32 | counter: 5, 33 | state: { 34 | arbitrary: true, 35 | deep: 'somewhat' 36 | } 37 | } 38 | }) 39 | }) 40 | }) 41 | 42 | describe('Custom types', () => { 43 | const initialState = { 44 | list: ['a', 'b'], 45 | nested: { 46 | counter: 0, 47 | state: { 48 | deep: 'somewhat' 49 | } 50 | } 51 | } 52 | 53 | const riducerDict = { 54 | duplicate: (leafState: any[]) => leafState.concat(leafState) 55 | } 56 | 57 | const [reducer, actions] = riduce(initialState, riducerDict) 58 | 59 | it('Creates informative action types by default', () => { 60 | const actionToPushToList = actions.list.create.push('c') 61 | expect(actionToPushToList.type).toBe('list/PUSH') 62 | 63 | const actionToDuplicateList = actions.list.create.duplicate() 64 | expect(actionToDuplicateList.type).toBe('list/DUPLICATE') 65 | 66 | const actionToUpdateDeepState = actions.nested.state.deep.create.update('could go deeper') 67 | expect(actionToUpdateDeepState.type).toBe('nested/state/deep/UPDATE') 68 | }) 69 | 70 | test('You can override the default action type', () => { 71 | const appendLetter = actions.list.create('APPEND_LETTER').push 72 | expect(appendLetter('c').type).toBe('APPEND_LETTER') 73 | 74 | const duplicateList = actions.list.create('DUPLICATE_LIST').duplicate 75 | expect(duplicateList().type).toBe('DUPLICATE_LIST') 76 | }) 77 | 78 | test("Overriding action type doesn't change how the reducer responds", () => { 79 | const store = createStore(reducer) 80 | expect(store.getState().list).toEqual(['a', 'b']) 81 | 82 | store.dispatch(actions.list.create('APPEND_LETTER').push('c')) 83 | expect(store.getState().list).toEqual(['a', 'b', 'c']) 84 | 85 | store.dispatch(actions.list.create('DUPLICATE_LIST').duplicate()) 86 | expect(store.getState().list).toEqual(['a', 'b', 'c', 'a', 'b', 'c']) 87 | }) 88 | }) 89 | 90 | describe('Controlling payloads', () => { 91 | const initialState = { 92 | counter: 0 93 | } 94 | 95 | const riducerDict = { 96 | addMultiple: { 97 | argsToPayload: (...args: number[]) => args, 98 | reducer: (leafState: number, { payload }: { payload: number[] }) => payload.reduce((acc, val) => acc + val, leafState) 99 | }, 100 | addFirstThing: (leafState: number, { payload } : { payload: number }) => leafState + payload 101 | } 102 | 103 | const [reducer, actions] = riduce(initialState, riducerDict) 104 | const store = createStore(reducer) 105 | 106 | test('We can configure to use custom argsToPayload', () => { 107 | expect(store.getState().counter).toBe(0) 108 | 109 | store.dispatch(actions.counter.create.addMultiple(4, 2, 10)) 110 | expect(store.getState().counter).toBe(16) 111 | }) 112 | 113 | test("If we don't configure, it uses only the first argument as payload", () => { 114 | expect(store.getState().counter).toBe(16) 115 | // @ts-ignore 116 | store.dispatch(actions.counter.create.addFirstThing(1, 100)) 117 | expect(store.getState().counter).toBe(17) 118 | }) 119 | }) 120 | }) -------------------------------------------------------------------------------- /docs/old/api/bundle.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: bundle 3 | title: bundle 4 | hide_title: true 5 | sidebar_label: bundle 6 | --- 7 | 8 | # `bundle(actions[, type])` 9 | 10 | Returns an (action) object that the [riduce](../README.md) reducer uses to process the individual actions in the `actions` array sequentially (but, through the store, one dispatch). 11 | 12 | **See the [30 second demo](../examples/basicExample.md)** for usage. 13 | 14 | ## Parameters 15 | - `actions` *(object[])*: an array where each element should be an action created through the Redux-Leaves API (either [`create`](create.md) or `bundle` itself) 16 | - `type` *(string, optional)*: a string that will be the type of the returned action 17 | 18 | ## Returns 19 | `action` *(object)*: a single object to dispatch to the `store` 20 | 21 | ## Examples 22 | 23 | ### Actions array, no type provided 24 | When provided a single argument, an array of actions created through the [`create`](create.md) API, `bundle` will return a single action which is equivalent to all of those actions run sequentially. 25 | 26 | ```js 27 | import { createStore } from 'redux' 28 | import riduce, { bundle } from 'riduce' 29 | 30 | const initialState = { 31 | counter: 0, 32 | list: ['a'] 33 | } 34 | 35 | const [reducer, actions] = riduce(initialState) 36 | const store = createStore(reducer) 37 | 38 | const incrementAndPush = bundle([ 39 | actions.counter.create.increment(), 40 | actions.list.create.push('b') 41 | ]) 42 | 43 | // Action has a default type based on bundled action types: 44 | console.log(incrementAndPush.type) // 'counter/INCREMENT; list/PUSH' 45 | 46 | store.dispatch(incrementAndPush) 47 | console.log(store.getState()) // { counter: 1, list: ['a', 'b'] } 48 | ``` 49 | 50 | ### Actions array, type provided 51 | When provided a second argument of a string, `bundle` returns an action with that exact `type` property. 52 | 53 | ```js 54 | import { createStore } from 'redux' 55 | import riduce, { bundle } from 'riduce' 56 | 57 | const initialState = { 58 | counter: 0, 59 | list: ['a'] 60 | } 61 | 62 | const [reducer, actions] = riduce(initialState) 63 | const store = createStore(reducer) 64 | 65 | const incrementAndPush = bundle([ 66 | actions.counter.create.increment(), 67 | actions.list.create.push('b'), 68 | ], 'INCREMENT_AND_PUSH') 69 | 70 | // Action has the provided type 71 | console.log(incrementAndPush.type) // 'INCREMENT_AND_PUSH' 72 | 73 | // But you can still see the action types bundled if you wish 74 | console.log(incrementAndPush.leaf.bundled) // ['counter/INCREMENT', 'list/PUSH'] 75 | 76 | store.dispatch(incrementAndPush) 77 | console.log(store.getState()) // { counter: 1, list: ['a', 'b'] } 78 | ``` 79 | 80 | ### Order matters 81 | Since `bundle` effectively runs through actions in the ordered provided, the order of elements in the array can make a difference to the overall effect. 82 | 83 | ```js 84 | import { createStore } from 'redux' 85 | import riduce, { bundle } from 'riduce' 86 | 87 | const initialState = { 88 | counter: 0, 89 | list: ['a'] 90 | } 91 | 92 | const [reducer, actions] = riduce(initialState) 93 | const store = createStore(reducer) 94 | 95 | const incrementThenPush = bundle([ 96 | actions.counter.create.increment(), 97 | actions.list.create.do((leafState, treeState) => [...leafState, treeState.counter]) 98 | ]) 99 | 100 | const pushThenIncrement = bundle([ 101 | actions.list.create.do((leafState, treeState) => [...leafState, treeState.counter]), actions.counter.create.increment() 102 | ]) 103 | 104 | store.dispatch(incrementThenPush) 105 | console.log(store.getState()) // { counter: 1, list: ['a', 1] } 106 | 107 | store.dispatch(pushThenIncrement) 108 | console.log(store.getState()) // { counter: 2, list: ['a', 1, 1] } 109 | ``` 110 | 111 | ### Compound bundling 112 | You can `bundle` together actions that have already been bundled (a 'bundle bundling'): 113 | 114 | ```js 115 | import { createStore } from 'redux' 116 | import riduce, { bundle } from 'riduce' 117 | 118 | const initialState = { 119 | counter: 0, 120 | list: ['a'] 121 | } 122 | 123 | const [reducer, actions] = riduce(initialState) 124 | const store = createStore(reducer) 125 | 126 | const incrementAndPush = bundle([ 127 | actions.counter.create.increment(), 128 | actions.list.create.push('b') 129 | ]) 130 | 131 | const incrementAndPushAndIncrement = bundle([ 132 | incrementAndPush, 133 | actions.counter.create.increment() 134 | ]) 135 | 136 | store.dispatch(incrementAndPushAndIncrement) 137 | console.log(store.getState()) // { counter: 2, list: ['a', 'b'] } 138 | ``` -------------------------------------------------------------------------------- /docs/old/examples/advancedExample.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: advanced-example 3 | title: Advanced example 4 | hide_title: true 5 | sidebar_label: Advanced example 6 | --- 7 | 8 | # Advanced example: bundling actions, custom types and controlling payloads 9 | 10 | ## Bundling actions 11 | Perhaps you're worried that the atomic actions you're creating at each leaf will cause too much rerendering or clog up your Redux DevTools inspector. 12 | 13 | You can bundle together actions with [`bundle`](../api/bundle.md), to produce a new bundle action that will update your store's state in a single `dispatch`. 14 | 15 | [Bundling example on Runkit](https://runkit.com/richardcrng/redux-leaves-bundling-actions) 16 | 17 | ```js 18 | import { createStore } from 'redux' 19 | import riduce, { bundle } from 'redux-leaves' 20 | 21 | const initialState = { 22 | list: ['a', 'b'], 23 | nested: { 24 | counter: 0, 25 | state: { 26 | deep: 'somewhat' 27 | } 28 | } 29 | } 30 | 31 | const [reducer, actions] = riduce(initialState) 32 | const store = createStore(reducer) 33 | 34 | const actionBundle = bundle([ 35 | actions.list.create.push('c'), 36 | actions.nested.counter.create.increment(5), 37 | actions.nested.state.create.set('arbitrary', true) 38 | ]) 39 | 40 | store.dispatch(actionBundle) 41 | console.log(store.getState()) 42 | /* 43 | { 44 | list: ['a', 'b', 'c'], 45 | nested: { 46 | counter: 5, 47 | state: { 48 | arbitrary: true, 49 | deep: 'somewhat' 50 | } 51 | } 52 | } 53 | */ 54 | ``` 55 | 56 | ## Custom action types 57 | 58 | ### Default action types 59 | When you create an action through Redux-Leaves - whether using a default creator or some custom reducer logic you've supplied - it gives the action an informative `type` property: 60 | 61 | ```js 62 | import { createStore } from 'redux' 63 | import riduce from 'redux-leaves' 64 | 65 | const initialState = { 66 | list: ['a', 'b'], 67 | nested: { 68 | counter: 0, 69 | state: { 70 | deep: 'somewhat' 71 | } 72 | } 73 | } 74 | 75 | const riducerDict = { 76 | duplicate: leafState => leafState.concat(leafState) 77 | } 78 | 79 | const [reducer, actions] = riduce(initialState, riducerDict) 80 | 81 | const actionToPushToList = actions.list.create.push('c') 82 | console.log(actionToPushToList.type) // 'list/PUSH' 83 | 84 | const actionToDuplicateList = actions.list.create.duplicate() 85 | console.log(actionToDuplicateList.type) // 'list/DUPLICATE' 86 | 87 | const actionToUpdateDeepState = actions.nested.state.deep.create.update('could go deeper') 88 | console.log(actionToUpdateDeepState.payload) 89 | // 'nested/state/deep/UPDATE' 90 | ``` 91 | 92 | ### Overriding the default action type 93 | You may find benefits, e.g. with Redux DevTools, to overriding the default action type. 94 | 95 | You can do this by providing a string argument to `create`: 96 | 97 | ```js 98 | const appendLetter = actions.list.create('APPEND_LETTER').push 99 | console.log(appendLetter('c').type) // 'APPEND_LETTER' 100 | 101 | const duplicateList = actions.list.create('DUPLICATE_LIST').duplicate 102 | console.log(duplicateList().type) // 'DUPLICATE LIST' 103 | ``` 104 | 105 | Overriding the default action type won't change how the Redux-Leaves `reducer` responds to the action: 106 | ```js 107 | const store = createStore(reducer) 108 | console.log(store.getState().list) // ['a', 'b'] 109 | 110 | store.dispatch(appendLetter('c')) 111 | console.log(store.getState().list) // ['a', 'b', 'c'] 112 | 113 | store.dispatch(duplicateList()) 114 | console.log(store.getState().list) 115 | // ['a', 'b', 'c', 'a', 'b', 'c'] 116 | ``` 117 | 118 | ### Usage pattern 119 | An expected pattern that this facilitates is the defining of action creators in one file, e.g. `actions.js`: 120 | ```js 121 | // import the actions object created by Redux-Leaves 122 | import { actions } from './some/location' 123 | 124 | export const incrementCounter = actions.counter.create('INCREMENT_COUNTER').increment 125 | export const updateDeepState = actions.nested.state.deep.create('UPDATE_DEEP_STATE').update 126 | ``` 127 | and then import these action creators into whichever file needs access to them. 128 | 129 | ## Controlling payloads 130 | Suppose I want to create a custom creator, `addMultiple`, such that I can pass multiple numbers as arguments and have them all added to a given leaf's state. 131 | 132 | The default behaviour of a custom action creator is that only the first argument is passed as an action's payload, but we can configure that: 133 | 134 | ```js 135 | import { createStore } from 'redux' 136 | import riduce from 'redux-leaves' 137 | 138 | const initialState = { 139 | counter: 0 140 | } 141 | 142 | const riducerDict = { 143 | // object configuration longhand 144 | addMultiple: { 145 | // Capture all arguments and pass them to the reducer: 146 | argsToPayload: (...args) => args, 147 | reducer: (leafState, { payload }) => payload.reduce((acc, val) => acc + val, leafState) 148 | }, 149 | 150 | // function shorthand 151 | // uses default payload behaviour 152 | addFirstThing: (leafState, { payload }) => leafState + payload 153 | } 154 | 155 | const [reducer, actions] = riduce(initialState, riducerDict) 156 | const store = createStore(reducer) 157 | 158 | console.log(store.getState().counter) // 0 159 | 160 | store.dispatch(actions.counter.create.addMultiple(4, 2, 10)) 161 | console.log(store.getState().counter) // 16 162 | 163 | store.dispatch(actions.counter.create.addFirstThing(1, 100)) 164 | console.log(store.getState().counter) // 17 165 | ``` -------------------------------------------------------------------------------- /src/riduce.dts-jest.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react' 2 | import { createStore } from 'redux' 3 | import riduce, { Riducer, ActionWithPayload } from "." 4 | 5 | // @dts-jest:group Library consistency 6 | { 7 | const initialState = { 8 | name: 'Richard', 9 | coder: true 10 | } 11 | 12 | const [reducer, actions] = riduce(initialState) 13 | 14 | function Empty() { 15 | // @dts-jest:pass Works with useReducer 16 | const [state, dispatch] = useReducer(reducer, initialState) 17 | 18 | return null 19 | } 20 | 21 | // @dts-jest:pass Works with createStore 22 | const store = createStore(reducer) 23 | } 24 | 25 | // @dts-jest:group Actions shape mirrors state 26 | { 27 | const initialState = { 28 | shallow: true, 29 | nested: { 30 | counter: 0, 31 | state: { 32 | deep: 'somewhat' 33 | } 34 | }, 35 | list: [1, 2, 3] 36 | } 37 | 38 | const [reducer, actions] = riduce(initialState) 39 | 40 | // @dts-jest:pass Root actions has a create 41 | actions.create 42 | 43 | // @dts-jest:pass 44 | actions.create.update 45 | 46 | // @dts-jest:pass Root actions create.update takes state shape 47 | const updateAction = actions.create.update({ 48 | shallow: false, 49 | nested: { 50 | counter: 5, 51 | state: { 52 | deep: 'foobar' 53 | } 54 | }, 55 | list: [4, 10, 2] 56 | }) 57 | 58 | // @dts-jest:pass Reducer can take this created action 59 | reducer(initialState, updateAction) 60 | 61 | // @dts-jest:fail Root actions create.update requires argument 62 | actions.create.update() 63 | 64 | // @dts-jest:fail Root actions create.update requires conforming argument 65 | actions.create.update({ shallow: 'false' }) 66 | 67 | // @dts-jest:pass 68 | actions.shallow 69 | 70 | // @dts-jest:fail 71 | actions.foobar 72 | 73 | // @dts-jest:pass 74 | actions.nested.counter 75 | 76 | // @dts-jest:fail 77 | actions.nested.string 78 | } 79 | 80 | // @dts-jest:group Creators are sensitive to the leaf type 81 | { 82 | const initialState = { 83 | boolState: true, 84 | nested: { 85 | num: 0, 86 | state: { 87 | str: 'somewhat' 88 | } 89 | }, 90 | numList: [1, 2, 3] 91 | } 92 | 93 | const [reducer, actions] = riduce(initialState) 94 | 95 | // @dts-jest:pass Update can be passed boolean for boolState 96 | actions.boolState.create.update(true) 97 | 98 | // @dts-jest:fail Update cannot be passed string for boolState 99 | actions.boolState.create.update('true') 100 | 101 | // @dts-jest:pass Update can be passed number for number state 102 | actions.nested.num.create.update(5) 103 | 104 | // @dts-jest:fail Update cannot be passed string for number state 105 | actions.nested.num.create.update('5') 106 | 107 | // @dts-jest:pass Update can be passed object for object state 108 | actions.nested.state.create.update({ 109 | str: 'foobar' 110 | }) 111 | 112 | // @dts-jest:fail Update cannot be passed bad object for object state 113 | actions.nested.state.create.update({ randomKey: 'foobar' }) 114 | 115 | // @dts-jest:pass Update can be passed number[] for number[] state 116 | actions.numList.create.update([2, 4, 8]) 117 | 118 | // @dts-jest:fail Update cannot be passed string[] for number[] 119 | actions.numList.create.update(['2']) 120 | } 121 | 122 | // @dts-jest:group Custom reducer, explicitly typed 123 | { 124 | const initialState = { 125 | shallow: true, 126 | nested: { 127 | counter: 0, 128 | state: { 129 | deep: 'somewhat' 130 | } 131 | }, 132 | list: [1, 2, 3] 133 | } 134 | 135 | const multiplyBy: Riducer<{ 136 | leafState: number, 137 | payload: number, 138 | args: [number] 139 | }> = { 140 | argsToPayload: (num) => num, 141 | reducer: (leafState, action) => leafState * action.payload 142 | } 143 | 144 | const [reducer, actions] = riduce(initialState, { multiplyBy }) 145 | 146 | // @dts-jest:fail does not exist on boolean state 147 | actions.shallow.create.multiplyBy 148 | 149 | // @dts-jest:pass exists on number state 150 | actions.nested.counter.create.multiplyBy 151 | 152 | // @dts-jest:fail needs an argument 153 | actions.nested.counter.create.multiplyBy() 154 | 155 | // @dts-jest:pass accepts numerical argument 156 | actions.nested.counter.create.multiplyBy(2) 157 | 158 | // @dts-jest:fail rejects string argument 159 | actions.nested.counter.create.multiplyBy('2') 160 | } 161 | 162 | // @dts-jest:group Custom reducer, implicitly typed 163 | { 164 | const initialState = { 165 | shallow: true, 166 | nested: { 167 | counter: 0, 168 | state: { 169 | deep: 'somewhat' 170 | } 171 | }, 172 | list: [1, 2, 3] 173 | } 174 | 175 | const multiplyBy = { 176 | argsToPayload: (num: number) => num, 177 | reducer: (leafState: number, action: ActionWithPayload ) => leafState * action.payload 178 | } 179 | 180 | const [reducer, actions] = riduce(initialState, { multiplyBy }) 181 | 182 | // @dts-jest:fail does not exist on boolean state 183 | actions.shallow.create.multiplyBy 184 | 185 | // @dts-jest:pass exists on number state 186 | actions.nested.counter.create.multiplyBy 187 | 188 | // @dts-jest:fail needs an argument 189 | actions.nested.counter.create.multiplyBy() 190 | 191 | // @dts-jest:pass accepts numerical argument 192 | actions.nested.counter.create.multiplyBy(2) 193 | 194 | // @dts-jest:fail rejects string argument 195 | actions.nested.counter.create.multiplyBy('2') 196 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": ["ESNext", "ES2018", "ES2019"], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [] /* Type declaration files to be included in compilation. */, 48 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | "include": ["src"], 64 | "exclude": [ 65 | "node_modules", 66 | "website", 67 | "**/*.spec.ts", 68 | "**/*.test.ts", 69 | "**/*.dts-jest.ts" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/tmp/jest_rt", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | globals: { 62 | _dts_jest_: { 63 | "compiler_options": { 64 | "module": 'commonjs', 65 | "strict": true, 66 | // "strictNullChecks": true, 67 | "target": "es6", 68 | "esModuleInterop": true 69 | } 70 | } 71 | }, 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "json", 82 | // "jsx", 83 | // "ts", 84 | // "tsx", 85 | // "node" 86 | // ], 87 | 88 | // A map from regular expressions to module names that allow to stub out resources with a single module 89 | // moduleNameMapper: {}, 90 | 91 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 92 | modulePathIgnorePatterns: ["/dist/"], 93 | 94 | // Activates notifications for test results 95 | // notify: false, 96 | 97 | // An enum that specifies notification mode. Requires { notify: true } 98 | // notifyMode: "failure-change", 99 | 100 | // A preset that is used as a base for Jest's configuration 101 | // preset: null, 102 | 103 | // Run tests from one or more projects 104 | // projects: null, 105 | 106 | // Use this configuration option to add custom reporters to Jest 107 | // reporters: undefined, 108 | 109 | // Automatically reset mock state between every test 110 | // resetMocks: false, 111 | 112 | // Reset the module registry before running each individual test 113 | // resetModules: false, 114 | 115 | // A path to a custom resolver 116 | // resolver: null, 117 | 118 | // Automatically restore mock state between every test 119 | // restoreMocks: false, 120 | 121 | // The root directory that Jest should scan for tests and modules within 122 | // rootDir: null, 123 | 124 | // A list of paths to directories that Jest should use to search for files in 125 | // roots: [ 126 | // "" 127 | // ], 128 | 129 | // Allows you to use a custom runner instead of Jest's default test runner 130 | // runner: "jest-runner", 131 | 132 | // The paths to modules that run some code to configure or set up the testing environment before each test 133 | // setupFiles: [], 134 | 135 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 136 | // setupFilesAfterEnv: [], 137 | 138 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 139 | // snapshotSerializers: [], 140 | 141 | // The test environment that will be used for testing 142 | testEnvironment: "node", 143 | 144 | // Options that will be passed to the testEnvironment 145 | // testEnvironmentOptions: {}, 146 | 147 | // Adds a location field to test results 148 | // testLocationInResults: false, 149 | 150 | // The glob patterns Jest uses to detect test files 151 | // testMatch: [ 152 | // "**/__tests__/**/*.[jt]s?(x)", 153 | // "**/?(*.)+(spec|test).[tj]s?(x)" 154 | // ], 155 | 156 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 157 | // testPathIgnorePatterns: [ 158 | // "/node_modules/" 159 | // ], 160 | 161 | // The regexp pattern or array of patterns that Jest uses to detect test files 162 | testRegex: [ 163 | "^.+\\.dts-jest.ts$", 164 | "^.+\\.spec.ts$", 165 | "^.+\\.test.ts$" 166 | ], 167 | 168 | // This option allows the use of a custom results processor 169 | // testResultsProcessor: null, 170 | 171 | // This option allows use of a custom test runner 172 | // testRunner: "jasmine2", 173 | 174 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 175 | // testURL: "http://localhost", 176 | 177 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 178 | // timers: "real", 179 | 180 | // A map from regular expressions to paths to transformers 181 | transform: { 182 | "^.+\\.dts-jest.ts$": "dts-jest/transform", 183 | '^.+\\.ts?$': 'ts-jest' 184 | }, 185 | 186 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 187 | // transformIgnorePatterns: [ 188 | // "/node_modules/" 189 | // ], 190 | 191 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 192 | // unmockedModulePathPatterns: undefined, 193 | 194 | // Indicates whether each individual test should be reported during the run 195 | // verbose: null, 196 | 197 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 198 | // watchPathIgnorePatterns: [], 199 | 200 | // Whether to use watchman for file crawling 201 | // watchman: true, 202 | }; 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Riduce 👻 2 | 3 | **Get *rid* of your reducer boilerplate!** 4 | 5 | *Zero hassle state management that's typed, flexible and scalable.* 6 | 7 | ```bash 8 | npm install riduce 9 | ``` 10 | 11 | ![Travis (.org)](https://img.shields.io/travis/richardcrng/riduce.svg) 12 | [![bundle size](https://badgen.net/bundlephobia/min/riduce)](https://badgen.net/bundlephobia/min/riduce) 13 | [![npm version](https://badge.fury.io/js/riduce.svg)](https://badge.fury.io/js/riduce) 14 | 15 | [![Edit Riduce example - MadLibs for Developers](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/riduce-example-madlibs-for-developers-njo9t?fontsize=14&hidenavigation=1&theme=dark&view=preview) 16 | 17 | Whether you're using `useReducer` or `redux`, reducer boilerplate is tedious to learn, setup and maintain. 18 | 19 | What if type-safe state management was quicker, easier and simpler? 20 | 21 | Riduce is a library written to be: 22 | - **Strongly-typed**, so your state stays predictable 23 | - **Trivial to scale** as your state grows more complex 24 | - **Zero hassle**, with *just two lines of code...* 25 | 26 | *... and one of the 2 lines to setup is an `import`.* 27 | 28 | ```ts 29 | import riduce from 'riduce' 30 | 31 | const [reducer, actions] = riduce(initialState) 32 | ``` 33 | 34 | That's it! Now you've got a type-safe `reducer` and arbitrary `actions`, with zero hassle. 35 | 36 | Let's see it in use! 37 | 38 | > 🚧 Full documentation for Riduce is under construction - but the API is essentially the same as [Redux-Leaves](https://redux-leaves.js.org), except `riduce` replaces the `reduxLeaves` default export. 39 | > Currently documented here are indicative examples on setup, usage and customisation. These give quite a lot of information about how the library is used. 40 | > For more specifics, please consult the Redux-Leaves documentation to see, e.g., the [default action creators](https://redux-leaves.js.org/docs/defaults/overview) which [`create`](https://redux-leaves.js.org/docs/api/create) gives access to. 41 | 42 | # Introductory Example 43 | For a `useReducer` example, [see this CodeSandbox](https://codesandbox.io/s/riduce-example-madlibs-for-developers-njo9t). 44 | 45 | For a `redux` example, you can run this [Repl.it](https://repl.it/@richardcrng/Riduce-with-Redux). 46 | 47 | For more advanced usage of Riduce, see [this example](./docs/riduce-advanced.md). 48 | 49 | Below, we'll walk through the introductory Redux example, showing: 50 | 1. [Zero hassle setup](#zero-hassle-setup) with 2 lines of code; 51 | 2. [Scalable state management](#scalable-state-management) with arbitrary actions; and 52 | 3. [Typesafe action creators](#typesafe-action-creators) to mirror your state's shape. 53 | 54 | ## Zero hassle setup 55 | Let's imagine we're controlling the state for a museum. 56 | ```ts 57 | import { createStore } from 'redux' 58 | import riduce from 'riduce' // 1st line: import 59 | 60 | const museumState = { 61 | isOpen: false, 62 | visitor: { 63 | counter: 0, 64 | guestbook: ['richard woz here'] 65 | } 66 | } 67 | 68 | const [reducer, actions] = riduce(museumState) // 2nd line: setup 69 | const { getState, dispatch } = createStore(reducer) 70 | ``` 71 | **And that's it.** Those two lines replace *all* of our reducer boilerplate. 72 | 73 | ## Scalable state management 74 | Continuing on from [above](#zero-hassle-setup), let's: 75 | 1. Open our museum; 76 | 2. Add to the visitor counter; 77 | 3. Sign the guestbook; and 78 | 4. Amend a guestbook entry. 79 | 80 | Previously, you might create 4 x reducer branches, action types and action creators. 81 | 82 | **Riducer gets rid of all that boilerplate.** 83 | 84 | Now, it's as simple as describing the changes we want to see! 85 | 86 | ```ts 87 | // at `state.isOpen`, create an action to toggle the boolean 88 | dispatch(actions.isOpen.create.toggle()) 89 | 90 | // at `state.visitor.counter`, create an action to add 5 91 | dispatch(actions.visitor.counter.create.increment(5)) 92 | 93 | // at `state.visitor.guestbook`, create an action to push a string 94 | dispatch(actions.visitor.guestbook.create.push('LOL from js fan')) 95 | 96 | // at `state.visitor.guestbook[0]`, create an action to concat a string 97 | dispatch(actions.visitor.guestbook[0].create.concat('!!!')) 98 | 99 | getState() 100 | /* 101 | { 102 | isOpen: true, 103 | visitor: { 104 | counter: 5, 105 | guestbook: [ 106 | 'richard woz here!!!', 107 | 'LOL from js fan' 108 | ] 109 | } 110 | } 111 | */ 112 | ``` 113 | All this is possible because Riduce's `actions` gives you **loads of convenient action creators out of the box**, which you can *use liberally throughout your state tree:* `update`, `set`, `filter`, `reset`, and many more... 114 | 115 | It's also possible to add your own in, as documented in [advanced Riduce usage](./docs/riduce-advanced.md). 116 | 117 | ## Typesafe action creators 118 | Now we've seen that Riduce is [zero-hassle setup](#zero-hassle-setup) for [arbitrary action creators without the reducer boilerplate](#scalable-state-management). 119 | 120 | It's written in TypeScript, so it's helpfully typed right out of the box as well! 121 | 122 | ```ts 123 | // can we push to a boolean? no! 124 | // ❌ TypeError: (ts 2339) Property 'push' does not exist on type... 125 | actions.isOpen.create.push() 126 | 127 | // can we push to an array without an argument? no! 128 | // ❌ TypeError: (ts 2554) Expected 1-3 arguments, but got 0. 129 | actions.visitor.guestbook.create.push() 130 | 131 | // can we push a number to an inferred string[]? no! 132 | // ❌ TypeError: (ts 2345) Argument of type '10' is not assignable to parameter of type 'string'. 133 | actions.visitor.guestbook.create.push(10) 134 | 135 | // can we push a string to an inferred string[]? yeah, okay then. 136 | // ✅ compiles! 137 | actions.visitor.guestbook.create.push('10') 138 | ``` 139 | 140 | # Get started 141 | You may wish to check out the following: 142 | - [Riduce: advanced usage](./docs/riduce-advanced.md) 143 | - [Riduce with `useReducer`: CodeSandbox demo](https://codesandbox.io/s/riduce-example-madlibs-for-developers-njo9t) 144 | - [Riduce with Redux: Repl.it demo](https://repl.it/@richardcrng/Riduce-with-Redux) 145 | 146 | Advanced Riduce usage includes: 147 | 1. [Bundle multiple actions](./docs/riduce-advanced.md#bundle-multiple-actions) into a single dispatch; 148 | 2. [Execute arbitrary reducer logic](./docs/riduce-advanced.md#execute-arbitrary-reducer-logic) for extendability; 149 | 3. [Add custom reducers](./docs/riduce-advanced.md#add-custom-reducers) for reusability; and 150 | 4. [Control action types](./docs/riduce-advanced.md#control-action-types) for debugging (e.g. Redux DevTools). 151 | 152 | Have fun adding it to your project! 153 | 154 | ```bash 155 | npm install riduce 156 | ``` 157 | 158 | > 🚧 Full documentation for Riduce is under construction - but the API is essentially the same as [Redux-Leaves](https://redux-leaves.js.org), except `riduce` replaces the `reduxLeaves` default export. 159 | > Currently documented here are indicative examples on setup, usage and customisation. These give quite a lot of information about how the library is used. 160 | > For more specifics, please consult the Redux-Leaves documentation to see, e.g., the [default action creators](https://redux-leaves.js.org/docs/defaults/overview) which [`create`](https://redux-leaves.js.org/docs/api/create) gives access to. 161 | --------------------------------------------------------------------------------