{"id":17741743,"url":"https://github.com/aakashns/dextrous","last_synced_at":"2025-04-21T07:32:54.640Z","repository":{"id":57204868,"uuid":"102896453","full_name":"aakashns/dextrous","owner":"aakashns","description":"Utilities for reducer composition in Redux","archived":false,"fork":false,"pushed_at":"2017-09-25T02:09:09.000Z","size":509,"stargazers_count":9,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-15T09:32:23.094Z","etag":null,"topics":["composable","frontend","javascript","react","react-native","reducer","redux"],"latest_commit_sha":null,"homepage":"https://npmjs.com/dextrous","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aakashns.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-09-08T19:18:19.000Z","updated_at":"2024-02-26T15:04:33.000Z","dependencies_parsed_at":"2022-09-18T01:32:24.690Z","dependency_job_id":null,"html_url":"https://github.com/aakashns/dextrous","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aakashns%2Fdextrous","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aakashns%2Fdextrous/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aakashns%2Fdextrous/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aakashns%2Fdextrous/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aakashns","download_url":"https://codeload.github.com/aakashns/dextrous/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223856777,"owners_count":17214938,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["composable","frontend","javascript","react","react-native","reducer","redux"],"created_at":"2024-10-26T04:23:11.529Z","updated_at":"2024-11-09T17:04:07.266Z","avatar_url":"https://github.com/aakashns.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![npm version](https://img.shields.io/npm/v/dextrous.svg?style=flat-square)](https://www.npmjs.com/package/dextrous) [![build status](https://img.shields.io/travis/aakashns/dextrous.svg?style=flat-square)](https://travis-ci.org/aakashns/dextrous)\n\n\u003c!--[![npm downloads](https://img.shields.io/npm/dm/dextrous.svg?style=flat-square)](https://www.npmjs.com/package/dextrous)--\u003e\n\n# dextrous\nA tiny library with utilities for reducing Redux boilerplate and reusing reducer logic.\n\n### Contents\n\n* [Objectives](#objectives)\n* [Installation](#installation)\n* [Usage](#usage)\n* [Live Examples](#live-examples)\n* [Support](#support)\n\n\u003ca name=\"objectives\"\u003e\u003c/a\u003e\n\n## Objectives\n\n- Reduce the amount of boilerplate involved defining reducers and action creators in Redux(using `makeReducer`, `makeObjectReducer`, `makeListReducer` etc.). \n\n- Reuse reducers to handle multiple parts of the state without defining a whole new set of action types and action creators. (using `makeMultiReducer`, `nameReducer`, `nameAction`, `nameActionCreators`, `nameAndCombineReducers` etc.)\n\n\u003ca name=\"installation\"\u003e\u003c/a\u003e\n\n## Installation\nInstall using `npm` or `yarn`:\n```bash\nnpm install dextrous --save\n```\nor \n```bash\nyarn add dextrous\n```\n\n\u003ca name=\"usage\"\u003e\u003c/a\u003e\n\n## Usage\n\n### Quick Links\n* [`makeMultiReducer`](#makeMultiReducer)\n* [`nameReducer`](#nameReducer)\n* [`nameAction`](#nameAction)\n* [`nameActionCreator`](#nameActionCreator)\n* [`makeReducer`](#makeReducer)\n* [`makeObjectReducer`](#makeObjectReducer)\n* [`objectReducer`](#objectReducer)\n* [`makeListReducer`](#makeListReducer)\n* [`listReducer`](#listReducer)\n* [Other Functions](#other-functions)\n\n\n\u003ca name=\"makeMultiReducer\"\u003e\u003c/a\u003e\n\n### makeMultiReducer(reducer, [keyExtractor])\nCreates a key-based reducer that can be used to manage different parts of the state using the same `reducer`. The key must be provided by setting the `key` property on actions. Alternatively, you can provide a custom `keyExtractor` function to extract the key from an action.\n\n`makeMultiReducer` is ideal for cases where you want to use the same reducer to manage the state for multiple components, especially when the number of components is not known beforehand e.g. showing 5 independent counters on a page, with an 'Add Counter' button to add new counters.\n\n#### Example ([Try Online](https://stackblitz.com/edit/react-gtd76c))\n\n```javascript\nimport { makeMultiReducer, makeMultiGetter } from 'dextrous';\n\n// Reducer to manage state for one counter\nconst counter = (state = 10, { type }) =\u003e {\n  switch (type) {\n    case \"INCREMENT\":\n      return state + 1;\n    case \"DECREMENT\":\n      return state - 1;\n    default:\n      return state;\n  }\n};\n\n// Reducer to manage state for multiple counters.\nconst counters = makeMultiReducer(counter);\nconst getCounter = makeMultiGetter(counter);\n\nlet state = counters(undefined, { type: \"@@INIT\" });\nconsole.log('State:', state); // {}\nconsole.log('Counter a:', getCounter(state, 'a')); // 10\nconsole.log('Counter b:', getCounter(state, 'b')); // 10\nconsole.log('Counter c:', getCounter(state, 'c')); // 10\n\nstate = counters(state, { type: \"INCREMENT\", key: \"a\" });\nconsole.log('State:', state); // {a: 11}\nconsole.log('Counter a:', getCounter(state, 'a')); // 11\nconsole.log('Counter b:', getCounter(state, 'b')); // 10\nconsole.log('Counter c:', getCounter(state, 'c')); // 10\n\nstate = counters(state, { type: \"DECREMENT\", key: \"c\" });\nconsole.log('State:', state); // {a: 11, c: 9}\nconsole.log('Counter a:', getCounter(state, 'a')); // 11\nconsole.log('Counter b:', getCounter(state, 'b')); // 10\nconsole.log('Counter c:', getCounter(state, 'c')); // 9\n\n// Using a custom key extractor\nconst counters2 = makeMultiReducer(counter, action =\u003e action.id);\nlet state2 = counters2(undefined, { type: '@@INIT'});\nconst action = { type: 'INCREMENT', id: 'd' }; // Providing 'id' instead of 'key'\n\nconsole.log(getCounter(state2, 'd')); // 10\nstate2 = counters2(state2, action);\nconsole.log(getCounter(state2, 'd')) // 11\n\n```\n\n**NOTE**: Always use `makeMultiReducer` in conjunction with `makeMultiGetter` to retrieve the state correctly (as shown in the example above). If you try to acess the state for a particular key directly, you may get `undefined`.\n\n\u003ca name=\"nameReducer\"\u003e\u003c/a\u003e\n\n### `nameReducer(reducer, name, whitelist = [])`\n\nWraps the given `reducer` and returns a new reducer that only responds to actions that actions that contain a `name` matching the given `name`. \n\n#### Example ([Try online](https://stackblitz.com/edit/react-gdmtuu))\n\n```javascript\nimport { nameReducer } from 'dextrous';\n\n// A simple counter reducer supporting increment and decrement.\nconst counter = (state = 0, { type }) =\u003e {\n  switch(type) {\n    case 'INCREMENT':\n      return state + 1;\n    case 'DECREMENT':\n      return state - 1;\n    default:\n      return state;\n  }\n};\n\n// Create two named reducers using counter.\nconst counter1 = nameReducer(counter, 'counter1');\nconst counter2 = nameReducer(counter, 'counter2');\n\n// Test behavior by passing named actions\nconsole.log(counter1(0, { type: 'INCREMENT' })); // 0\nconsole.log(counter1(0, { type: 'INCREMENT', name: 'counter2' })); // 0\nconsole.log(counter1(0, { type: 'INCREMENT', name: 'counter1' })); // 1\n```\n\nAdditionally, you can use the `whitelist` argument in `nameReducer` to provide a list of action types that should be handled even if they do not contain a `name` property.\n\n\u003ca name=\"nameAction\"\u003e\u003c/a\u003e\n\n### `nameAction(action, name)`\n\nUtility function to add a `name` property to an action.\n\n#### Example\n```javascript\nimport { nameAction } from 'dextrous';\n\nconsole.log(nameAction({ type: 'INCREMENT'}, 'counter2'));\n// { type: 'INCREMENT', name: 'counter' }\n\n```\n\n\u003ca name=\"nameActionCreator\"\u003e\u003c/a\u003e\n\n### `nameActionCreator(actionCreator, name)` \n\nGiven an `actionCreator` and a name, returns a new action creator which adds the `name` property to the resulting action. \n\n#### Example\n```javascript \nimport { nameActionCreator } from 'dextrous';\n\nconst actionCreator = (email, password) =\u003e ({\n  type: 'EDIT_LOGIN_DATA',\n  payload: { email, password }\n});\n\nconst namedCreator = nameActionCreator(actionCreator, 'loginForm');\n\nconsole.log(namedCreator('test@example.com', 'password123'));\n/*\n{\n  type: 'EDIT_LOGIN_DATA',\n  name: 'loginForm',\n  payload: {\n    email: 'test@example.com',\n    password: 'password123'\n  }\n}\n*/\n```\n\n\u003ca name=\"makeReducer\"\u003e\u003c/a\u003e\n\n### `makeReducer(initialState)`\n\nCreates a reducer with the given initial state, supporting two actions:\n1. `SET`: Change the state to a new value provided with the action.\n2. `RESET`: Reset the state back to `initialState`.\n\nThe action creators `setValue` and `resetValue` can be used to create `SET` and `RESET` actions respectively.\n\n#### Example\n```javascript\nimport { makeReducer, setValue, resetValue } from 'dextrous';\n\nconst reducer = makeReducer('nothing');\n\nconsole.log(reducer('Hello', setValue('world'))); // 'world'\nconsole.log(reducer('Hello', resetValue())); // 'nothing'\n\n```\n\nUse `makeReducer` in conjuction with `nameReducer` and `nameActionCreator` to easily create multiple reducers for handling different parts of the state.\n\n#### Example\n```javascript\nimport {\n  makeReducer,\n  setValue,\n  resetValue,\n  nameReducer,\n  nameActionCreator\n} from 'dextrous';\nimport { combineReducers } from 'redux';\n\nconst ReducerNames = {\n  age: 'age',\n  location: 'location'\n};\n\n// Reducer and action creators for managing state.age\nconst age = nameReducer(makeReducer(18), ReducerNames.age);\nconst setAge = nameActionCreator(setValue, ReducerNames.age);\nconst resetAge = nameActionCreator(resetValue, ReducerNames.age);\n\n// Reducer and action creators for managing state.location\nconst location = nameReducer(makeReducer('London'), ReducerNames.location);\nconst setLocation = nameActionCreator(setValue, ReducerNames.location);\nconst resetLocation = nameActionCreator(resetValue, ReducerNames.location);\n\n// Create a combined reducer\nconst rootReducer = combineReducers({\n  age,\n  location\n});\n\n// Check the initial state\nconst initialState = rootReducer(undefined, { type: 'IGNORED_ACTION'});\nconsole.log(initialState);\n// { age: 18, location: 'London' }\n\n// Dispatch an action to change the age (but not location)\nconsole.log(rootReducer(initialState, setAge(23)));\n// { age: 23, location: 'London' }\n\n// Dispatch an action to change the location (but not age)\nconsole.log(rootReducer(initialState, setLocation('Paris')));\n// { age: 18, location: 'Paris' }\n\n```\n\n**NOTE**: Never use `makeReducer` without `nameReducer`, otherwise every action with the type `SET` or `RESET` will change the state managed by the reducer. \n\nFor example, if we do not use `nameReducer` in the above example while defining `age` and `location`, then dispatching the action `setAge` will change both `state.age` and `state.location` to the given value, which is not the desired behavior.\n\n\u003ca name=\"makeObjectReducer\"\u003e\u003c/a\u003e\n\n### `makeObjectReducer(initialState = {})`\nCreate a reducer that allows setting and removing entries in a plain Javascript object. It supports the following actions:\n1. `EDIT`: Change the values of one or more keys in the state object.\n2. `REMOVE`: Clear one or more keys in the state object.\n1. `SET`: Change the state to a new value provided with the action.\n2. `RESET`: Reset the state back to `initialState`.\n\nThe action creators `editObject`, `removeKeys`, `setValue` and `resetValue` can be used to create the above actions. `makeObjectReducer` is ideal for managing the state of HTML forms.\n\n### Example\n```javascript\nimport { \n  makeObjectReducer, \n  editObject, \n  removeKeys,\n  resetValue,\n  nameReducer,\n  nameActionCreator\n} from 'dextrous';\n\nconst reducerName = 'signupForm';\nconst initialState = { name: '', email: '', age: 18};\n\n// Define a named reducer\nconst signupForm = nameReducer(makeObjectReducer(initialState), reducerName);\n\n// Define some named action creators\nconst editSignupForm = nameActionCreator(editObject, reducerName);\nconst removeSignupFields = nameActionCreator(removeKeys, reducerName);\nconst clearSignupForm = nameActionCreator(resetValue, reducerName);\n\nconsole.log(signupForm(undefined, { type: 'IGNORED_ACTION' }));\n// { name: '', email: '', age: 18}\n\nconst editAction = editSignupForm({\n  email: 'name@example.com',\n  age: 23\n});\nconst newState1 = signupForm(initialState, editAction)\nconsole.log(newState1);\n// {name: \"\", email: \"name@example.com\", age: 23}\n\nconst removeAction = removeSignupFields(['age', 'name'])\nconst newState2 = signupForm(newState1, removeAction);\nconsole.log(newState2);\n// {email: \"name@example.com\"}\n\nconst resetAction = clearSignupForm();\nconst newState3 = signupForm(newState2, resetAction);\nconsole.log(newState3);\n// { name: '', email: '', age: 18}\n```\n\n**NOTE**: As with `makeReducer`, never use `makeObjectReducer` without `nameReducer`.\n\n\u003ca name=\"objectReducer\"\u003e\u003c/a\u003e\n\n### `objectReducer`\nIf the `initialState` of your reducer is the empty object `{}`, you can use `objectReducer` instead of `makeObjectReducer({})`. It supports all the actions that `makeObjectReducer` supports.\n\n#### Example\n```javascript\nimport { objectReducer, nameReducer } from 'dextrous';\n\nconst loginForm = nameReducer(objectReducer, 'loginForm');\n/* Equivalent to:\nconst loginForm = nameReducer(makeObjectReducer({}), 'loginForm');\n*/\n\n```\n\n\u003ca name=\"makeListReducer\"\u003e\u003c/a\u003e\n\n### `makeListReducer(initialState = [])`\nCreate a reducer that allows adding and removing items in a Javascript array. It supports the following actions:\n1. `ADD`: Add one or more item at the end of the list.\n2. `REMOVE`: Clear one or more items from the list.\n1. `SET`: Change the state to a new value provided with the action.\n2. `RESET`: Reset the state back to `initialState`.\n\nThe action creators `addItem`, `addItems`, `removeItem`, `removeItems`, `setValue` and `resetValue` can be used to create the above actions. `makeListReducer` is ideal for cases where the state is variable list of items.\n\n#### Example\n```javascript\nimport { \n  makeListReducer, \n  addItem, \n  removeItem,\n  resetValue,\n  nameReducer,\n  nameActionCreator\n} from 'dextrous';\n\nconst reducerName = 'locations';\nconst defaultLocations = ['London', 'Paris'];\n\n// Define the reducer and action creators\nconst locations = nameReducer(makeListReducer(defaultLocations), reducerName);\nconst addLocation = nameActionCreator(addItem, reducerName);\nconst removeLocation = nameActionCreator(removeItem, reducerName);\nconst resetLocations = nameActionCreator(resetValue, reducerName);\n\n// Check the initial state\nconst initialState = locations(undefined, { type: 'IGNORED_ACTION' });\nconsole.log(initialState);\n// [\"London\", \"Paris\"]\n\n// Add a location\nconst newState1 = locations(initialState, addLocation('San Francisco'));\nconsole.log(newState1);\n// [\"London\", \"Paris\", \"San Francisco\"]\n\n// Remove a location\nconst newState2 = locations(newState1, removeLocation('Paris'));\nconsole.log(newState2);\n// [\"London\", \"San Francisco\"]\n\n// Reset to the initial state\nconst newState3 = locations(newState2, resetLocations());\nconsole.log(newState3);\n// [\"London\", \"Paris\"]\n```\n\n**NOTE**: As with `makeReducer`, never use `makeListReducer` without `nameReducer`.\n\n\u003ca name=\"listReducer\"\u003e\u003c/a\u003e\n\n### `listReducer`\nIf the `initialState` of your reducer is the empty list `[]`, you can use `listReducer` instead of `makeListReducer([])`. It supports all the actions that `makeListReducer` supports.\n\n#### Example\n```javascript\nimport { listReducer, nameReducer } from 'dextrous';\n\nconst locations = nameReducer(listReducer, 'locations');\n/* Equivalent to:\nconst locations = nameReducer(makeListReducer([]), 'locations');\n*/\n\n```\n\n\u003ca name=\"other-functions\"\u003e\u003c/a\u003e\n\n### Other Functions\n\nThere are many other utility functions that are currently not documented. You can go through their source code, comments and tests to see understand they do. \n\nHere is a full list of exported functions:\n\n* `makeMultiReducer` ([source](), [tests]())\n  \n* `makeMultiGetter` ([source](), [tests]())\n\n* `makeReducer` ([source](), [tests]())\n  \n* `setValue` ([source](), [tests]())\n  \n* `resetValue` ([source](), [tests]())\n  \n* `nameReducer` ([source](), [tests]())\n  \n* `nameAction` ([source](), [tests]())\n  \n* `nameActionCreator` ([source](), [tests]())\n  \n* `nameActionCreators` ([source](), [tests]())\n\n* `makeNamedReducers` ([source](), [tests]())\n  \n* `nameReducers` ([source](), [tests]())\n  \n* `nameAndCombineReducers` ([source](), [tests]())\n  \n* `nameAndBindActionCreators` ([source](), [tests]())\n  \n* `makeObjectReducer` ([source](), [tests]())\n  \n* `editObject` ([source](), [tests]())\n  \n* `removeKeys` ([source](), [tests]())\n  \n* `objectReducer` ([source](), [tests]())\n  \n* `makeListReducer` ([source](), [tests]())\n  \n* `listReducer` ([source](), [tests]())\n  \n* `addItem` ([source](), [tests]())\n  \n* `addItems` ([source](), [tests]())\n  \n* `removeItem` ([source](), [tests]())\n  \n* `removeItems` ([source](), [tests]())\n  \n* `nameReducerAndCreators` ([source](), [tests]())\n  \n* `makeNamedReducer` ([source](), [tests]())\n  \n* `makeNamedObjectReducer` ([source](), [tests]())\n  \n* `makeNamedListReducer` ([source](), [tests]())\n  \n* `makeNamedMultiReducer` ([source](), [tests]())\n\nIf you are using any of the above functions, please consider opening a pull request adding some documentation and examples.\n\n\u003ca name=\"live-examples\"\u003e\u003c/a\u003e\n\n## Live Examples\n\nHere are some live examples where edit you can edit the code online and play around with the APIs:\n\n* Using `makeMultiReducer` to render several independent counters: [https://stackblitz.com/edit/dextrous-example](https://stackblitz.com/edit/dextrous-example)\n\n* `makeMultiReducer` demo: [https://stackblitz.com/edit/react-gtd76c](https://stackblitz.com/edit/react-gtd76c)\n\n* `nameReducer` demo: [https://stackblitz.com/edit/react-gdmtuu](https://stackblitz.com/edit/react-gdmtuu)\n\nThese examples powered by the [Stackblitz](https://stackblitz.com/) online IDE.\n\n\u003ca name=\"support\"\u003e\u003c/a\u003e\n\n## Support\n\nI developed `dextrous` after facing the same problems (non-reusable reducer logic, too much boilerplate etc.) across several React + Redux projects. It's currently being used in over half a dozen projects running in production, so I fully intend to support, enhance, test and document the project for the forseeable future. This library has saved me from writing 1000s of lines of code, so I can't imagine not using for a future project. \n\n`dextrous` is released under the MIT Licence, so feel free to do whatever you want with it! For feedback, comments and suggestions, [open a pull request](https://github.com/aakashns/dextrous/pulls) or just tweet to me ([@aakashns](https://twitter.com/aakashns)).","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faakashns%2Fdextrous","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faakashns%2Fdextrous","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faakashns%2Fdextrous/lists"}