gnar-edge
v2.0.8
Published
A sharp set of utilities: base64, drain, handleChange, jwt, notifications
Downloads
27
Maintainers
Keywords
Readme
Gnar Edge: Precision edging for JS apps
Part of Project Gnar: base • gear • piste • off-piste • edge • powder • genesis • patrol
Get started with Project Gnar on
Gnar Edge is a sharp set of JS utilities: base64 • drain • handleChange • JWT • notifications • redux
Installation
npm install gnar-edge
or
yarn add gnar-edge
Packages
Gnar Edge ships with three distinct types of packages:
- Single optimized (45.0 KB) ES5 main package
- Discreet ES5 tree-shakable packages (sizes listed below)
- ES6 tree-shakable modules (recommended)
Choose only one type of package to use in your application; mixing package types will needlessly increase the size of your production build.
The ES5 packages are transpiled via Babel using the following browserslist setting:
"browserslist": [ ">0.25%", "ie >= 11" ]
This is a fairly conservative setting, largely due to the inclusion of IE 11 (scroll down), and may result in build that is larger than necessary for your specific needs. The overall browser market share of that setting can be found at browserl.ist, however the global coverage listed there is a bit misleading. Babel uses the lowest common denominator of all the ES6 features in your code vs. the target browsers' ES6 support to determine which shims to include in the build. The global market share of browsers that support your code with the bundled shims is likely much higher. If you prefer to transpile Gnar Edge using a different set of target browsers, use Gnar Edge's ES6 modules.
Gnar Edge is written in ES6+. See the ES6 Tree-Shakable Modules section for more info.
Main Package
Chose the main package when want ES5 code and you plan to use all the Gnar Edge utilities in your app or when the combined size of the utilities (listed below) you choose to use exceeds 45.0 KB.
The main package is smaller than the combined total of the tree-shakable packages (45.0 KB vs. 54.7 KB) due to the webpack module overhead, module overlap (i.e. base64
and jwt
) and overlap of the babel shims between modules.
The main package may grow over time. There is a high probability that new utilities will be added to Gnar Edge in the future and all new utilities will be added to the main package. Whenever a new utility is added, Gnar Edge's major semver will be incremented (e.g. 1.x.x
-> 2.0.0
).
Usage
The main package can be used as an ES6 import or a Node require:
import { base64, drain, handleChange, jwt, notifications } from 'gnar-edge';
or
const { base64, drain, handleChange, jwt, notifications } = require('gnar-edge');
In the module docs and code examples, we'll be using the ES6 format.
ES5 Tree-Shakable Packages
If you want ES5 code and you only need some of Gnar Edge's utilities, we can take advantage of Webpack's tree shaking to reduce the size of your production build.
The tree-shakable packages are:
Package | Size -- | --: gnar-edge/base64 | 2.3 KB gnar-edge/drain | 3.5 KB gnar-edge/handleChange | 2.8 KB gnar-edge/jwt | 4.2 KB gnar-edge/notifications | 35.6 KB gnar-edge/redux | 6.3 KB
Usage
Each tree-shakable package can be used as an ES6 import or a Node require, for example:
import base64 from 'gnar-edge/base64';
or
const base64 = require('gnar-edge/base64').default;
In the module docs and code examples, we'll be using the ES6 format.
ES6 Tree-Shakable Modules
Gnar Edge is written in ES6+, i.e. ES6 mixed with a few features that are in the TC39 process, namely the bind operator (::
) and decorators. Using Gnar Edge's ES6 modules will significantly reduce your production build size vs. using the ES5 packages, but it does require extra setup work. The ES6 modules that ship with Gnar Edge are not minified (minification / uglification should be part of your build process).
The ES6 modules are:
Module | Size -- | --: gnar-edge/es/base64 | 0.7 KB gnar-edge/es/drain | 2.1 KB gnar-edge/es/handleChange | 0.6 KB gnar-edge/es/jwt | 2.1 KB gnar-edge/es/notifications | 19.6 KB gnar-edge/es/redux | 3.3 KB
The minified module sizes reported above are produced in the Gnar Powder build. YMMV.
The total gzipped size of gnar-edge (excluding drain) is 4.6 KB. This is really the most noteworthy number since it is the number of bytes that will be transmitted over the wire if your server is properly configured.
To use the ES6 modules with Babel 7, follow these steps:
Install the following babel plugins:
npm i -D \ @babel/plugin-syntax-dynamic-import \ @babel/plugin-proposal-function-bind \ @babel/plugin-proposal-export-default-from \ @babel/plugin-proposal-decorators \ @babel/plugin-proposal-class-properties
Add these plugins to your
babel.config.js
file, e.g.{ presets: [ '@babel/preset-env', '@babel/react' ], env: { production: { presets: [ 'react-optimize' ] } }, plugins: [ 'react-hot-loader/babel', '@babel/transform-runtime', '@babel/plugin-syntax-dynamic-import', '@babel/plugin-proposal-function-bind', '@babel/plugin-proposal-export-default-from', [ '@babel/plugin-proposal-decorators', { legacy: true } ], [ '@babel/plugin-proposal-class-properties', { loose: true } ] ] }
Check the Edge and Powder repos for complete examples of
package.json
andbabel.config.js
.Update your webpack module rule for javascript to not exclude
gnar-edge
. You will likely have a js rule that looks like:{ test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' }
Change the rule to:
{ test: /\.jsx?$/, exclude: /node_modules\/(?!gnar-edge)/, use: 'babel-loader' }
This tells babel to transpile the gnar-edge code along with your application code.
If you're linting with eslint, add
"legacyDecorators": true
toparserOptions.ecmaFeatures
in.eslintrc
.If you're testing with Jest, add or update
transformIgnorePatterns
inpackage.json
to excludegnar-edge
, e.g."transformIgnorePatterns": [ "<rootDir>/node_modules/(?!gnar-edge)" ],
This tells Jest to transpile the gnar-edge code.
Use the Gnar Edge
es
modules in your app, e.g.:import base64 from 'gnar-edge/es/base64';
Base64
Usage:
Using the ES5 main package:
import { base64 } from 'gnar-edge';
or, using the ES5 tree-shakable package:
import base64 from 'gnar-edge/base64';
or, using the ES6 tree-shakable module:
import base64 from 'gnar-edge/es/base64';
Read the package usage section if you're unsure of which format to use.
The base64
package provides three simple functions for encoding and decoding base64 content:
base64.decode
handles both traditional and web-safe base64 content, outputs a UTF-8 stringbase64.encode
encodes a UTF-8 string to web-safe base64base64.encodeNonWebSafe
encode a UTF-8 string to traditional base64
Examples:
base64.encode('✓ à la mode'); // '4pyTIMOgIGxhIG1vZGU='
base64.decode('4pyTIMOgIGxhIG1vZGU='); // '✓ à la mode'
base64.encode(' >'); // 'ICA-'
base64.encode(' ?'); // 'ICA_'
base64.encodeNonWebSafe(' >'); // 'ICA+'
base64.encodeNonWebSafe(' ?'); // 'ICA/'
Acknowledgements
- MDN offers two alternative solutions for transcoding Unicode to Base64.
Drain
Usage:
Using the ES5 main package:
import { drain } from 'gnar-edge';
or, using the ES5 tree-shakable package:
import drain from 'gnar-edge/drain';
or, using the ES6 tree-shakable module:
import drain from 'gnar-edge/es/drain';
Read the package usage section if you're unsure of which format to use.
Drain converts a generator function to a promise. It supports yields of all JS types, i.e.:
- Functions / Thunks
- Promises
- Generator Functions
- Generators
- Async Functions
- Arrays (recursively)
- Plain (i.e. Literal) Objects (recursively)
- Basic JS Types (Number, String, Boolean, Date, etc.)
Example:
drain(function* () {
let result = 1;
result *= yield 2;
const array = yield [3];
result *= array[0];
const object = yield { x: 4 };
result *= object.x;
result *= yield new Promise(resolve => { setTimeout(() => { resolve(5); }, 10); });
result *= yield () => 6;
result *= yield () => new Promise(resolve => { setTimeout(() => { resolve(7); }, 10); });
const mixedArray = yield [
8,
new Promise(resolve => { setTimeout(() => { resolve(9); }, 10); }),
() => new Promise(resolve => { setTimeout(() => { resolve(10); }, 10); })
];
mixedArray.forEach(x => { result *= x; });
const mixedObject = yield {
a: 11,
b: new Promise(resolve => { setTimeout(() => { resolve(12); }, 10); }),
c: () => new Promise(resolve => { setTimeout(() => { resolve(13); }, 10); })
};
Object.values(mixedObject).forEach(x => { result *= x; });
function* generatorFunction1() {
return yield 14;
}
result *= yield generatorFunction1;
function* generatorFunction2(x) {
return yield x;
}
const generator = generatorFunction2(15);
result *= yield generator;
result *= yield async () => {
try {
return await new Promise(resolve => { setTimeout(() => { resolve(16); }, 10); });
} catch (e) {
throw e;
}
};
return result;
})
.then(result => { console.log(result); /* 20922789888000, i.e. 16! */ });
Implementation Note
Drain returns a function which returns a promise. The returned function includes three convenience methods, then
, catch
, and finally
which invoke the function and chain onto the resulting promise.
The six basic methods of utilizing drain
are:
as a function:
const fn = drain(function* () {});
as a promise:
const promise = drain(function* () {})();
then
chained:drain(function* () { return yield 'oh, yeah!'; }) .then(result => { console.log(result); });
>> 'oh, yeah!'
then
chained witherror
support:drain(function* () { throw new Error('oops'); yield 'unreachable'; }).then( result => { console.log(result); }, error => { console.log(error.message); });
>> 'oops'
catch
chained:drain(function* () { throw new Error('oops, I did it again'); yield 'unreachable'; }) .catch(error => { console.log(error.message); });
>> 'oops, I did it again'
finally
chained:drain(function* () { throw new Error('oops'); yield 'unreachable'; }) .finally(() => { console.log('always called'); });
>> 'always called'
Usage with Jest
Testing generator functions in Jest is simple with drain
.
Example:
describe('Testing a generator function', drain(function* () {
const theAnswerToLifeTheUniverseAndEverything = yield 42;
expect(theAnswerToLifeTheUniverseAndEverything).toBe(42);
}));
Acknowledgements
co by @tj provides similar functionality to drain
.
I initially used co
in Project Gnar. I wrote drain
as an enhancement to co
to add these features:
- handle basic JS types (numbers, strings, booleans, dates, etc)
- handle functions and thunks without a callback (i.e. co's
done
) - fix an issue with
co.wrap
- I had to overrideco.wrap
to get it to pass along the generator function in my Jest tests - provide a single dual-purpose interface, i.e.
drain
replaces bothco
andco.wrap
- ES6 implementation -
co
is written in ES5 whereasdrain
is written in ES6 and transpiled via Babel - Simplified implementation: 43 SLOC vs. 101
HandleChange
Usage:
Using the ES5 main package:
import { handleChange } from 'gnar-edge';
or, using the ES5 tree-shakable package:
import handleChange from 'gnar-edge/handleChange';
or, using the ES6 tree-shakable module:
import handleChange from 'gnar-edge/es/handleChange';
Read the package usage section if you're unsure of which format to use.
The handleChange
package provides an onChange
event handler which updates the state of a bound React element. It accepts an optional callback and an optional set of options.
handleChange(<< stateKeyName: String >>, << ?callback: Function >>, << ?options >>)
options:
beforeSetState
[Function]: Function to call before updating the state.
It works with:
<input>
<input type='checkbox'>
<input type='radio'>
<select>
<textarea>
Example:
import React, { Component } from 'react';
import handleChange from 'gnar-edge/handleChange';
export default class MyView extends Component {
state = {
firstName: '',
lastName: ''
};
handleChange = this::handleChange; // when using the ES7 stage 0 bind operator, or
handleChange = handleChange.bind(this); // when using the ES5 bind function
beforeLastNameChange = () => {
console.log('Before change', this.state.lastName);
}
handleLastNameChange = () => {
console.log('After change', this.state.lastName);
}
render() {
const { firstName, lastName } = this.state;
const beforeSetState = this.beforeLastNameChange;
return (
<div>
<input onChange={this.handleChange('firstName')} />
<input onChange={this.handleChange('lastName', this.handleLastNameChange, { beforeSetState })} />
</div>
<div>`Hello, ${firstName} ${lastName}!`</div>
);
}
}
JWT
The jwt
package provides a set of utilities to simplify the handling of jwt tokens.
Usage:
Using the ES5 main package:
import { jwt } from 'gnar-edge';
const { base64, getJwt, isLoggedIn, jwtDecode } = jwt; // or use `jwt.base64`, etc.
or, using the ES5 tree-shakable package:
import { base64, getJwt, isLoggedIn, jwtDecode } from 'gnar-edge/jwt';
or, using the ES6 tree-shakable module:
import { base64, getJwt, isLoggedIn, jwtDecode } from 'gnar-edge/es/jwt';
Read the package usage section if you're unsure of which format to use.
Base64
The base64 package is included in the jwt
package.
GetJwt
Retrieves a jwt token from localStorage and decodes it:
getJwt(<< keyName: String >>)
.keyName
defaults to'jwt'
.
Example:
import { getJwt } from 'gnar-edge/jwt';
const myJwt = getJwt();
const myCustomKeyJwt = getJwt('J-W-T');
IsLoggedIn
Retrieves a jwt token from localStorage (using getJwt) and returns a Boolean indicating whether or not the jwt token has expired:
isLoggedIn(<< keyName: String >>)
.keyName
defaults to'jwt'
.
Example:
import { isLoggedIn } from 'gnar-edge/jwt';
...
<Route render={() => <Redirect to={isLoggedIn() ? '/account' : '/login'} />} />
JwtDecode
Decodes a JWT token.
Example:
import { jwtDecode } from 'gnar-edge/jwt';
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiRmxhaXIsIEduYXIgRmxhaXIifQ.-gYkrEvtdghFzzKecKdu_gITvJFwEdOHPYXdp643-2w';
console.log(jwtDecode(jwtToken).name);
>> 'Edge, Gnar Edge'
Notifications
Usage:
Using the ES5 main package:
import { notifications } from 'gnar-edge';
const {
ADD_NOTIFICATION,
DISMISS_NOTIFICATION,
Notifications,
dismissNotifications,
notificationActions,
notifications,
notifyError,
notifyInfo,
notifySuccess,
notifyWarning
} = notifications; // or use `notifications.ADD_NOTIFICATION`, etc.
or, using the ES5 tree-shakable package:
import {
ADD_NOTIFICATION,
DISMISS_NOTIFICATION,
Notifications,
dismissNotifications,
notificationActions,
notifications,
notifyError,
notifyInfo,
notifySuccess,
notifyWarning
} from 'gnar-edge/notifications'
or, using the ES6 tree-shakable module:
import {
ADD_NOTIFICATION,
DISMISS_NOTIFICATION,
Notifications,
dismissNotifications,
notificationActions,
notifications,
notifyError,
notifyInfo,
notifySuccess,
notifyWarning
} from 'gnar-edge/es/notifications'
Read the package usage section if you're unsure of which format to use.
The notifications package requires the following npm packages to be installed in your app (i.e. in the dependencies
section of package.json
):
- @material-ui/core
- @material-ui/icons
- animate.css
- classnames
- immutable
- prop-types
- react
- react-dom
- react-redux
- redux
- redux-actions
- redux-saga
If your app is based on Gnar Powder, these packages are already installed. Otherwise, these notifications will work with any Redux Saga-based app that includes the packages listed above. The following command will install any packages you may be missing:
npm i @material-ui/core @material-ui/icons animate.css classnames immutable prop-types react react-dom react-redux redux redux-actions redux-saga
In addition to installing the dependencies, you must add the Notifications
component to your DOM and add the notifications
reducer to your root reducer.
Component
The Notifications
component should be placed at the root of the application, for example:
import { Notifications } from 'gnar-edge/notifications';
...
<Grid container>
<Grid item xs={12}>
<Switch>
...
</Switch>
<Notifications />
</Grid>
</Grid>
The component accepts one property, position
, in the form '<< vertical position >> << horizontal position >>'
with default 'top right'
. The acceptable values are:
- vertical: 'top' or 'bottom'
- horizontal: 'left', 'center', or 'right'
Reducer
The notifications
reducer must be added to your root reducer, for example:
import { combineReducers } from 'redux';
import { notifications } from 'gnar-edge/notifications';
export default combineReducers({
...
notifications,
...
});
Action Types
ADD_NOTIFICATION
and DISMISS_NOTIFICATION
are provided for use with an action watcher (optional).
Actions
The notificationActions
can be used in any view, for example:
import { connect } from 'react-redux';
import { notificationActions } from 'gnar-edge/notifications';
import Button from '@material-ui/core/Button';
import React, { Component } from 'react';
const mapStateToProps = () => ({});
const mapDispatchToProps = notificationActions;
@connect(mapStateToProps, mapDispatchToProps)
export default class MyView extends Component {
newSuccess = () => { this.props.notifySuccess('More Success!', { autoDismissMillis: 2000, onDismiss: this.newSuccess }); }
render() {
const { dismissNotification, notifyError, notifyInfo, notifySuccess, notifyWarning } = this.props;
return (
<div>
<Button onClick={() => notifySuccess('Such Success!')}>Success</Button>
<Button onClick={() => notifyError('You shall not pass.')}>Error</Button>
<Button onClick={() => notifyInfo('Gnarly info, dude.')}>Info</Button>
<Button onClick={() => notifyWarning('Danger, Will Robinson!')}>Warning</Button>
<Button onClick={() => notifyInfo("I'm sticking around", { key: 'sticky', autoDismissMillis: 0 })}>Sticky</Button>
<Button onClick={() => dismissNotification('sticky')}>Dismiss Sticky</Button>
<Button onClick={this.newSuccess}>Perpetual Success</Button>
</div>
);
}
}
Each notify method accepts an optional set of options. The available options are:
- autoDismissMillis: Number of milliseconds to wait before auto-dismissing the notification; specify 0 for no auto-dismiss (i.e. the user must click the close icon).
- key: String to override the autogenerated notification key; for use with an action watcher - when a notification is dismissed, the
DISMISS_NOTIFICATION
action is dispatched with a payload containing the notification key. - onDismiss: Callback to execute when the notification is dismissed; the callback receives a single Boolean parameter which indicates whether or not the notification was dismissed by the user (i.e. the user clicked the close icon).
Sagas
The notifications utility generator functions can be used in any Redux Saga, for example:
import { takeEvery } from 'redux-saga/effects';
import { notifySuccess } from 'gnar-edge/notifications';
function* successAction() {
yield notifySuccess('Such Saga Success!');
}
export default function* watchSuccessAction() {
yield takeEvery('SUCCESS_ACTION', successAction);
}
The available sagas are dismissNotification
, notifyError
, notifyInfo
, notifySuccess
, notifyWarning
.
The notify sagas accept the same options (autoDismissMillis
, key
, and onDismiss
) as the notify actions.
Redux
Boilerplate-nixing convenience functions for creating actions and reducers.
Usage:
Using the ES5 main package:
import { redux } from 'gnar-edge';
const { gnarActions, gnarReducers } = redux; // or use `redux.gnarActions`, etc.
or, using the ES5 tree-shakable package:
import { gnarActions, gnarReducers } from 'gnar-edge/redux'
or, using the ES6 tree-shakable module:
import { gnarActions, gnarReducers } from 'gnar-edge/es/redux'
Read the package usage section if you're unsure of which format to use.
gnarActions
A common pattern when creating Redux actions looks like this:
import { createAction } from 'redux-actions';
export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const REALLY_BASIC_ACTION = 'REALLY_BASIC_ACTION';
export const ACTION_WITH_CUSTOM_PAYLOAD_CREATOR = 'ACTION_WITH_CUSTOM_PAYLOAD_CREATOR';
export default {
groupOfActions: {
someAction: createAction(SOME_ACTION, (param1, param2, param3) => ({ param1, param2, param3 })),
someOtherAction: createAction(SOME_OTHER_ACTION, param1 => ({ param1 }))
},
someOtherGroupOfActions: {
reallyBasicAction: createAction(REALLY_BASIC_ACTION, () => ({})),
actionWithCustomPayloadCreator: createAction(ACTION_WITH_CUSTOM_PAYLOAD_CREATOR, cost => ({ cost: 2 * cost }))
}
};
The actions are often split out into a bunch of small files, like I did with Gnar Powder before I wrote gnarActions
.
It would be nice if we could reduce this code a bit. Using gnarActions
, the code above becomes:
import { gnarActions } from 'gnar-edge/es/redux';
export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const REALLY_BASIC_ACTION = 'REALLY_BASIC_ACTION';
export const ACTION_WITH_CUSTOM_PAYLOAD_CREATOR = 'ACTION_WITH_CUSTOM_PAYLOAD_CREATOR';
export default gnarActions({
groupOfActions: {
[SOME_ACTION]: [ 'param1', 'param2', 'param3' ],
[SOME_OTHER_ACTION]: 'param1'
},
someOtherGroupOfActions: {
[REALLY_BASIC_ACTION]: [],
[ACTION_WITH_CUSTOM_PAYLOAD_CREATOR]: cost => ({ cost: 2 * cost })
}
});
Removing all the boilerplate has a nice impact on our code's readability. It also becomes clear that consolidating actions into fewer files improves maintainability. Check out the difference in Gnar Powder after incorporating Gnar Edge Redux.
Tip: You might be tempted to use SOME_ACTION
instead of [SOME_ACTION]
in the actions object - don't. The interpolated version binds the object key to the action constant.
How does gnarActions
work?
It recursively iterates through the input object looking for the following pattern:
- key: All uppercase letters, digits and underscores, i.e. matches
/^([A-Z\d]+_)*[A-Z\d]+$/
- value: String, empty array, array of strings, or function.
For every match, it performs the following transformation:
key: Converts to camelcase
value: Converts to a Redux action following the pseudocode template:
createAction(<< key >>, (param1, param2, param3, ...) => ({ param1, param2, param3, ... }))
or with a predefined
payloadCreator
:createAction(<< key >>, payloadCreator)
If a node in the input object doesn't match the key, value pattern outline above, the node is retained unchanged in the output actions (unless the nodes is a literal object, then we recurse through it). This allows us to include actions that don't match the pattern that is transformed using the template. For example:
groupOfActions: {
[SOME_ACTION]: [ 'param1', 'param2', 'param3' ],
anotherAction: createAction(SOME_OTHER_ACTION, (param1, param2, ...) => { return someFancyObject; })
}
gnarReducers
A common pattern when creating Redux reducers looks like:
import { handleActions } from 'redux-actions';
export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const YET_ANOTHER_ACTION = 'YET_ANOTHER_ACTION';
export default {
someStoreNode: handleActions({
[SOME_ACTION]: (state, { payload }) => ({ ...state, ...payload }),
[SOME_OTHER_ACTION]: (state, { payload }) => ({ ...state, ...payload }),
}, {} /* <- initial state */),
someParentStoreNode: {
someChildStoreNode: handleActions({
[YET_ANOTHER_ACTION]: (state, { payload }) => { return someFancyObject; }
}, { fruit: 'apple' })
}
};
The reducers are often split out into a bunch of small files, like I did with Gnar Powder before I wrote gnarReducers
.
It would be nice if we could also, ahem, reduce this code a bit. Using gnarReducers
, the code above becomes:
import { gnarReducers } from 'gnar-edge/es/redux';
export const SOME_ACTION = 'SOME_ACTION';
export const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
export const YET_ANOTHER_ACTION = 'YET_ANOTHER_ACTION';
export default gnarReducers({
someStoreNode: {
basicReducers: [ SOME_ACTION, SOME_OTHER_ACTION ]
},
someParentStoreNode: {
someChildStoreNode: {
initialState: { fruit: 'apple' },
customReducers: {
[YET_ANOTHER_ACTION]: (state, { payload }) => { return someFancyObject; }
}
}
}
});
Again, removing all the boilerplate has a nice impact on our code's readability and it becomes clear that consolidating reducers into fewer files improves maintainability. Check out the difference in Gnar Powder after incorporating Gnar Edge Redux.
But wait, there's more! If a node is a string or an array, it's interpreted as basicReducers. That means our example can be further simplified to:
export default gnarReducers({
someStoreNode: [ SOME_ACTION, SOME_OTHER_ACTION ],
someParentStoreNode: { ... }
});
How does gnarReducers
work?
It recursively iterates through the input object looking for the following pattern:
- key: The key is not checked
- value: String, array, or an object containing either
basicReducers
orcustomReducers
(or both)
For every match, it performs the following transformation:
key: No change
value: Converts to a redux reducer following the pseudocode template:
const node = << node value is String || Array >> ? { basicReducers: << node value >> } : << node value >>; const { initialState, basicReducers, customReducers } = node; const parsedReducers = { ...(typeof basicReducers === 'string' ? [ basicReducers ] : basicReducers || []).reduce((memo, key) => { memo[key] = (state, { payload }) => ({ ...state, ...payload }) return memo; }, {}), ...(customReducers || {}) }; return handleActions(parsedReducers, initialState || {});
If a node in the input object isn't a string, an array, or an object that includes basicReducers
or customReducers
in its value, the node is retained unchanged in the output reducer (unless the nodes is a literal object, then we recurse through it). This allows us to include predefined reducers in the input object.
Optional Parameters
gnarReducers
accepts two optional parameters, defaultInitialState
, and defaultReducer
.
defaultInitialState
: Function which returns the desireddefaultState
parameter forhandleActions
.defaultReducer
: Reducer function to use in place of the reducer used for the basic reducers, i.e.(state, { payload }) => ({ ...state, ...payload })
Immutable Maps
gnarReducers
is also designed to work with Immutable Maps.
To activate the Immutable Map mode, either specify Map
as the defaultInitialState
or specify a function that returns a Map
, for example:
import { Map } from 'immutable';
import { gnarReducers } from 'gnar-edge/es/redux';
export default gnarReducers({ ... }, Map);
In Immutable Map mode, the defaultReducer
becomes:
(state, { payload }) => state.merge(payload)