Never Panic Much

    gnar-edge

    2.0.8 • Public • Published

    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

    Install

    npm i gnar-edge

    DownloadsWeekly Downloads

    0

    Version

    2.0.8

    License

    MIT

    Unpacked Size

    155 kB

    Total Files

    25

    Last publish

    Collaborators

    • ic3b3rg