gnar-edge

A sharp set of utilities: base64, drain, handleChange, jwt, notifications

Usage no npm install needed!

<script type="module">
  import gnarEdge from 'https://cdn.skypack.dev/gnar-edge';
</script>

README

Gnar Edge: Precision edging for JS apps

MIT license codecov pipeline status npm version total downloads of gnar-edge

Part of Project Gnar:  base  •  gear  •  piste  •  off-piste  •  edge  •  powder  •  genesis  •  patrol

Get started with Project Gnar on  Project Gnar on Medium

Join Project Gnar on  Project Gnar on Slack

Support Project Gnar on  Project Gnar on Patreon

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:

  1. 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
    
  2. 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 and babel.config.js.

  3. 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.

  4. If you're linting with eslint, add "legacyDecorators": true to parserOptions.ecmaFeatures in .eslintrc.

  5. If you're testing with Jest, add or update transformIgnorePatterns in package.json to exclude gnar-edge, e.g.

    "transformIgnorePatterns": [
      "<rootDir>/node_modules/(?!gnar-edge)"
    ],
    

    This tells Jest to transpile the gnar-edge code.

  6. 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 string
  • base64.encode encodes a UTF-8 string to web-safe base64
  • base64.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 with error 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 override co.wrap to get it to pass along the generator function in my Jest tests
  • provide a single dual-purpose interface, i.e. drain replaces both co and co.wrap
  • ES6 implementation - co is written in ES5 whereas drain 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

Notifications Animation

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 or customReducers (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 desired defaultState parameter for handleActions.

  • 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)

Made with Love by Brien Givens