Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/aakashns/dextrous
Utilities for reducer composition in Redux
https://github.com/aakashns/dextrous
composable frontend javascript react react-native reducer redux
Last synced: 13 days ago
JSON representation
Utilities for reducer composition in Redux
- Host: GitHub
- URL: https://github.com/aakashns/dextrous
- Owner: aakashns
- License: mit
- Created: 2017-09-08T19:18:19.000Z (about 7 years ago)
- Default Branch: master
- Last Pushed: 2017-09-25T02:09:09.000Z (about 7 years ago)
- Last Synced: 2024-10-15T09:32:23.094Z (24 days ago)
- Topics: composable, frontend, javascript, react, react-native, reducer, redux
- Language: JavaScript
- Homepage: https://npmjs.com/dextrous
- Size: 497 KB
- Stars: 9
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
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)
# dextrous
A tiny library with utilities for reducing Redux boilerplate and reusing reducer logic.### Contents
* [Objectives](#objectives)
* [Installation](#installation)
* [Usage](#usage)
* [Live Examples](#live-examples)
* [Support](#support)## Objectives
- Reduce the amount of boilerplate involved defining reducers and action creators in Redux(using `makeReducer`, `makeObjectReducer`, `makeListReducer` etc.).
- 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.)
## Installation
Install using `npm` or `yarn`:
```bash
npm install dextrous --save
```
or
```bash
yarn add dextrous
```## Usage
### Quick Links
* [`makeMultiReducer`](#makeMultiReducer)
* [`nameReducer`](#nameReducer)
* [`nameAction`](#nameAction)
* [`nameActionCreator`](#nameActionCreator)
* [`makeReducer`](#makeReducer)
* [`makeObjectReducer`](#makeObjectReducer)
* [`objectReducer`](#objectReducer)
* [`makeListReducer`](#makeListReducer)
* [`listReducer`](#listReducer)
* [Other Functions](#other-functions)### makeMultiReducer(reducer, [keyExtractor])
Creates 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.`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.
#### Example ([Try Online](https://stackblitz.com/edit/react-gtd76c))
```javascript
import { makeMultiReducer, makeMultiGetter } from 'dextrous';// Reducer to manage state for one counter
const counter = (state = 10, { type }) => {
switch (type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
};// Reducer to manage state for multiple counters.
const counters = makeMultiReducer(counter);
const getCounter = makeMultiGetter(counter);let state = counters(undefined, { type: "@@INIT" });
console.log('State:', state); // {}
console.log('Counter a:', getCounter(state, 'a')); // 10
console.log('Counter b:', getCounter(state, 'b')); // 10
console.log('Counter c:', getCounter(state, 'c')); // 10state = counters(state, { type: "INCREMENT", key: "a" });
console.log('State:', state); // {a: 11}
console.log('Counter a:', getCounter(state, 'a')); // 11
console.log('Counter b:', getCounter(state, 'b')); // 10
console.log('Counter c:', getCounter(state, 'c')); // 10state = counters(state, { type: "DECREMENT", key: "c" });
console.log('State:', state); // {a: 11, c: 9}
console.log('Counter a:', getCounter(state, 'a')); // 11
console.log('Counter b:', getCounter(state, 'b')); // 10
console.log('Counter c:', getCounter(state, 'c')); // 9// Using a custom key extractor
const counters2 = makeMultiReducer(counter, action => action.id);
let state2 = counters2(undefined, { type: '@@INIT'});
const action = { type: 'INCREMENT', id: 'd' }; // Providing 'id' instead of 'key'console.log(getCounter(state2, 'd')); // 10
state2 = counters2(state2, action);
console.log(getCounter(state2, 'd')) // 11```
**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`.
### `nameReducer(reducer, name, whitelist = [])`
Wraps the given `reducer` and returns a new reducer that only responds to actions that actions that contain a `name` matching the given `name`.
#### Example ([Try online](https://stackblitz.com/edit/react-gdmtuu))
```javascript
import { nameReducer } from 'dextrous';// A simple counter reducer supporting increment and decrement.
const counter = (state = 0, { type }) => {
switch(type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
};// Create two named reducers using counter.
const counter1 = nameReducer(counter, 'counter1');
const counter2 = nameReducer(counter, 'counter2');// Test behavior by passing named actions
console.log(counter1(0, { type: 'INCREMENT' })); // 0
console.log(counter1(0, { type: 'INCREMENT', name: 'counter2' })); // 0
console.log(counter1(0, { type: 'INCREMENT', name: 'counter1' })); // 1
```Additionally, 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.
### `nameAction(action, name)`
Utility function to add a `name` property to an action.
#### Example
```javascript
import { nameAction } from 'dextrous';console.log(nameAction({ type: 'INCREMENT'}, 'counter2'));
// { type: 'INCREMENT', name: 'counter' }```
### `nameActionCreator(actionCreator, name)`
Given an `actionCreator` and a name, returns a new action creator which adds the `name` property to the resulting action.
#### Example
```javascript
import { nameActionCreator } from 'dextrous';const actionCreator = (email, password) => ({
type: 'EDIT_LOGIN_DATA',
payload: { email, password }
});const namedCreator = nameActionCreator(actionCreator, 'loginForm');
console.log(namedCreator('[email protected]', 'password123'));
/*
{
type: 'EDIT_LOGIN_DATA',
name: 'loginForm',
payload: {
email: '[email protected]',
password: 'password123'
}
}
*/
```### `makeReducer(initialState)`
Creates a reducer with the given initial state, supporting two actions:
1. `SET`: Change the state to a new value provided with the action.
2. `RESET`: Reset the state back to `initialState`.The action creators `setValue` and `resetValue` can be used to create `SET` and `RESET` actions respectively.
#### Example
```javascript
import { makeReducer, setValue, resetValue } from 'dextrous';const reducer = makeReducer('nothing');
console.log(reducer('Hello', setValue('world'))); // 'world'
console.log(reducer('Hello', resetValue())); // 'nothing'```
Use `makeReducer` in conjuction with `nameReducer` and `nameActionCreator` to easily create multiple reducers for handling different parts of the state.
#### Example
```javascript
import {
makeReducer,
setValue,
resetValue,
nameReducer,
nameActionCreator
} from 'dextrous';
import { combineReducers } from 'redux';const ReducerNames = {
age: 'age',
location: 'location'
};// Reducer and action creators for managing state.age
const age = nameReducer(makeReducer(18), ReducerNames.age);
const setAge = nameActionCreator(setValue, ReducerNames.age);
const resetAge = nameActionCreator(resetValue, ReducerNames.age);// Reducer and action creators for managing state.location
const location = nameReducer(makeReducer('London'), ReducerNames.location);
const setLocation = nameActionCreator(setValue, ReducerNames.location);
const resetLocation = nameActionCreator(resetValue, ReducerNames.location);// Create a combined reducer
const rootReducer = combineReducers({
age,
location
});// Check the initial state
const initialState = rootReducer(undefined, { type: 'IGNORED_ACTION'});
console.log(initialState);
// { age: 18, location: 'London' }// Dispatch an action to change the age (but not location)
console.log(rootReducer(initialState, setAge(23)));
// { age: 23, location: 'London' }// Dispatch an action to change the location (but not age)
console.log(rootReducer(initialState, setLocation('Paris')));
// { age: 18, location: 'Paris' }```
**NOTE**: Never use `makeReducer` without `nameReducer`, otherwise every action with the type `SET` or `RESET` will change the state managed by the reducer.
For 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.
### `makeObjectReducer(initialState = {})`
Create a reducer that allows setting and removing entries in a plain Javascript object. It supports the following actions:
1. `EDIT`: Change the values of one or more keys in the state object.
2. `REMOVE`: Clear one or more keys in the state object.
1. `SET`: Change the state to a new value provided with the action.
2. `RESET`: Reset the state back to `initialState`.The 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.
### Example
```javascript
import {
makeObjectReducer,
editObject,
removeKeys,
resetValue,
nameReducer,
nameActionCreator
} from 'dextrous';const reducerName = 'signupForm';
const initialState = { name: '', email: '', age: 18};// Define a named reducer
const signupForm = nameReducer(makeObjectReducer(initialState), reducerName);// Define some named action creators
const editSignupForm = nameActionCreator(editObject, reducerName);
const removeSignupFields = nameActionCreator(removeKeys, reducerName);
const clearSignupForm = nameActionCreator(resetValue, reducerName);console.log(signupForm(undefined, { type: 'IGNORED_ACTION' }));
// { name: '', email: '', age: 18}const editAction = editSignupForm({
email: '[email protected]',
age: 23
});
const newState1 = signupForm(initialState, editAction)
console.log(newState1);
// {name: "", email: "[email protected]", age: 23}const removeAction = removeSignupFields(['age', 'name'])
const newState2 = signupForm(newState1, removeAction);
console.log(newState2);
// {email: "[email protected]"}const resetAction = clearSignupForm();
const newState3 = signupForm(newState2, resetAction);
console.log(newState3);
// { name: '', email: '', age: 18}
```**NOTE**: As with `makeReducer`, never use `makeObjectReducer` without `nameReducer`.
### `objectReducer`
If the `initialState` of your reducer is the empty object `{}`, you can use `objectReducer` instead of `makeObjectReducer({})`. It supports all the actions that `makeObjectReducer` supports.#### Example
```javascript
import { objectReducer, nameReducer } from 'dextrous';const loginForm = nameReducer(objectReducer, 'loginForm');
/* Equivalent to:
const loginForm = nameReducer(makeObjectReducer({}), 'loginForm');
*/```
### `makeListReducer(initialState = [])`
Create a reducer that allows adding and removing items in a Javascript array. It supports the following actions:
1. `ADD`: Add one or more item at the end of the list.
2. `REMOVE`: Clear one or more items from the list.
1. `SET`: Change the state to a new value provided with the action.
2. `RESET`: Reset the state back to `initialState`.The 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.
#### Example
```javascript
import {
makeListReducer,
addItem,
removeItem,
resetValue,
nameReducer,
nameActionCreator
} from 'dextrous';const reducerName = 'locations';
const defaultLocations = ['London', 'Paris'];// Define the reducer and action creators
const locations = nameReducer(makeListReducer(defaultLocations), reducerName);
const addLocation = nameActionCreator(addItem, reducerName);
const removeLocation = nameActionCreator(removeItem, reducerName);
const resetLocations = nameActionCreator(resetValue, reducerName);// Check the initial state
const initialState = locations(undefined, { type: 'IGNORED_ACTION' });
console.log(initialState);
// ["London", "Paris"]// Add a location
const newState1 = locations(initialState, addLocation('San Francisco'));
console.log(newState1);
// ["London", "Paris", "San Francisco"]// Remove a location
const newState2 = locations(newState1, removeLocation('Paris'));
console.log(newState2);
// ["London", "San Francisco"]// Reset to the initial state
const newState3 = locations(newState2, resetLocations());
console.log(newState3);
// ["London", "Paris"]
```**NOTE**: As with `makeReducer`, never use `makeListReducer` without `nameReducer`.
### `listReducer`
If the `initialState` of your reducer is the empty list `[]`, you can use `listReducer` instead of `makeListReducer([])`. It supports all the actions that `makeListReducer` supports.#### Example
```javascript
import { listReducer, nameReducer } from 'dextrous';const locations = nameReducer(listReducer, 'locations');
/* Equivalent to:
const locations = nameReducer(makeListReducer([]), 'locations');
*/```
### Other Functions
There 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.
Here is a full list of exported functions:
* `makeMultiReducer` ([source](), [tests]())
* `makeMultiGetter` ([source](), [tests]())* `makeReducer` ([source](), [tests]())
* `setValue` ([source](), [tests]())
* `resetValue` ([source](), [tests]())
* `nameReducer` ([source](), [tests]())
* `nameAction` ([source](), [tests]())
* `nameActionCreator` ([source](), [tests]())
* `nameActionCreators` ([source](), [tests]())* `makeNamedReducers` ([source](), [tests]())
* `nameReducers` ([source](), [tests]())
* `nameAndCombineReducers` ([source](), [tests]())
* `nameAndBindActionCreators` ([source](), [tests]())
* `makeObjectReducer` ([source](), [tests]())
* `editObject` ([source](), [tests]())
* `removeKeys` ([source](), [tests]())
* `objectReducer` ([source](), [tests]())
* `makeListReducer` ([source](), [tests]())
* `listReducer` ([source](), [tests]())
* `addItem` ([source](), [tests]())
* `addItems` ([source](), [tests]())
* `removeItem` ([source](), [tests]())
* `removeItems` ([source](), [tests]())
* `nameReducerAndCreators` ([source](), [tests]())
* `makeNamedReducer` ([source](), [tests]())
* `makeNamedObjectReducer` ([source](), [tests]())
* `makeNamedListReducer` ([source](), [tests]())
* `makeNamedMultiReducer` ([source](), [tests]())If you are using any of the above functions, please consider opening a pull request adding some documentation and examples.
## Live Examples
Here are some live examples where edit you can edit the code online and play around with the APIs:
* Using `makeMultiReducer` to render several independent counters: [https://stackblitz.com/edit/dextrous-example](https://stackblitz.com/edit/dextrous-example)
* `makeMultiReducer` demo: [https://stackblitz.com/edit/react-gtd76c](https://stackblitz.com/edit/react-gtd76c)
* `nameReducer` demo: [https://stackblitz.com/edit/react-gdmtuu](https://stackblitz.com/edit/react-gdmtuu)
These examples powered by the [Stackblitz](https://stackblitz.com/) online IDE.
## Support
I 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.
`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)).