├── .gitignore ├── tsconfig.json ├── test └── index.test.ts ├── package.json ├── lib └── index.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .* 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "*.test.ts"], 3 | "include": ["lib/**/*.ts"], 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "outDir": "dist", 8 | "module": "es6", 9 | "target": "es6", 10 | "moduleResolution": "node" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import useReduction from "../lib"; 2 | 3 | const [state, actions] = useReduction(0, { 4 | reset() { 5 | return 0; 6 | }, 7 | increment: (state, { payload }: { payload: number }) => state + payload, 8 | incrementByOne: (state) => state + 1, 9 | }); 10 | 11 | actions.reset(); 12 | actions.increment(5); 13 | actions.incrementByOne(); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-reduction", 3 | "version": "2.1.2", 4 | "description": "Simplify your reducer hooks", 5 | "files": [ 6 | "dist" 7 | ], 8 | "keywords": [ 9 | "react", 10 | "hooks", 11 | "reducer", 12 | "redux", 13 | "flux" 14 | ], 15 | "author": "Andrew Nater", 16 | "license": "MIT", 17 | "peerDependencies": { 18 | "react": "^16.11.0 || ^17.0.0 || ^18.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.0", 22 | "react": "^16.11.0 || ^17.0.0 || ^18.0.0", 23 | "typescript": "^4.6.2" 24 | }, 25 | "main": "dist/index.js", 26 | "module": "dist/index.js", 27 | "scripts": { 28 | "build": "tsc", 29 | "dev": "npm run build -w", 30 | "prepublishOnly": "npm run build" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/anater/useReduction.git" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { useReducer, Reducer, Dispatch, useMemo } from "react"; 2 | 3 | type Action = { 4 | type: T; 5 | payload: P; 6 | }; 7 | 8 | type ActionMap = { 9 | [K in keyof A]: (payload?: A[K]) => void; 10 | }; 11 | 12 | type ReducerMap = { 13 | [K in keyof A]: Reducer>; 14 | }; 15 | 16 | export default function useReduction( 17 | initialState: S, 18 | reducerMap: ReducerMap, 19 | debug = false 20 | ): [S, ActionMap] { 21 | const [state, dispatch] = useReducer(makeReducer(reducerMap), initialState); 22 | const actions = useMemo( 23 | () => makeActions(reducerMap, dispatch, debug), 24 | [reducerMap] 25 | ); 26 | 27 | return [state, actions]; 28 | } 29 | 30 | function makeReducer(reducerMap: ReducerMap) { 31 | return (state: S, action: Action) => { 32 | // if the dispatched action is valid and there's a matching reducer, use it 33 | if (action && action.type && reducerMap[action.type]) { 34 | return reducerMap[action.type](state, action); 35 | } else { 36 | // always return state if the action has no reducer 37 | return state; 38 | } 39 | }; 40 | } 41 | 42 | function makeActions( 43 | reducerMap: ReducerMap, 44 | dispatch: Dispatch>, 45 | debug: boolean 46 | ): ActionMap { 47 | const types = Object.keys(reducerMap) as Array; 48 | 49 | return types.reduce((actions: ActionMap, type: keyof A) => { 50 | // if there isn't already an action with this type 51 | if (!actions[type]) { 52 | // dispatches action with type and payload when called 53 | actions[type] = (payload: any) => { 54 | const action = { type, payload }; 55 | dispatch(action); 56 | // optionally log actions 57 | if (debug) console.log(action); 58 | }; 59 | } 60 | return actions; 61 | }, {} as ActionMap); 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # useReduction 2 | 3 | ```javascript 4 | function App() { 5 | const [count, actions] = useReduction(0, { 6 | increment: (count, { payload }) => count + payload, 7 | decrement: (count, { payload }) => count - payload 8 | }); 9 | 10 | return ( 11 |
12 |

{count}

13 | 14 | 15 |
16 | ); 17 | } 18 | ``` 19 | 20 | useReduction minimizes reducer code by automatically creating action creators from your reducer object. 21 | 22 | Write less code by using objects instead of switch statements for defining reducers. 23 | 24 | Debugging is baked in. In debug mode, all dispatched actions are logged to the console. 25 | 26 | ## Installation 27 | 28 | `npm install use-reduction` 29 | 30 | _Note_: React is a peer dependency. Its assumed you already have this installed for your project. 31 | 32 | ## Usage 33 | 34 | ```javascript 35 | import useReduction from "use-reduction"; 36 | // Call the hook with an initial state and a reducer object: 37 | const initialState = 0; 38 | const reducer = { 39 | increment: (count, { payload }) => count + payload, 40 | decrement: (count, { payload }) => count - payload 41 | }; 42 | 43 | const [state, actions] = useReduction(initialState, reducer); 44 | ``` 45 | 46 | The hook returns the current state and an object with action creators. When an action creator is called, an action (`{ type, payload }`) will be dispatched to update your state. 47 | 48 | ### Actions 49 | 50 | Action names are derived from reducer names to minimize boilerplate. Providing `useReduction` with a reducer `increment()` will generate an action creator also named `increment()` that dispatches an action with a `type` “increment” and uses the first argument as `payload`. `increment(1)` would use `1` as the `payload` provided to the reducer. 51 | 52 | ```javascript 53 | // use actions to update count from previous example 54 | actions.increment(1) 55 | // count = 1 56 | actions.decrement(2) 57 | // count = -1 58 | ``` 59 | 60 | ### Debugging 61 | 62 | Pass `true` as the third argument in `useReduction` to enable debug mode. It will log the dispatched action to the console. 63 | 64 | ```javascript 65 | const [state, actions] = useReduction(initialState, reducerMap, true); 66 | ``` 67 | --------------------------------------------------------------------------------