redux-tree
Version:
An alternative way to compose Redux reducers
226 lines (178 loc) • 10.6 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('immutable')) :
typeof define === 'function' && define.amd ? define(['exports', 'immutable'], factory) :
(factory((global.ReduxTree = global.ReduxTree || {}),global.Immutable));
}(this, (function (exports,immutable) { 'use strict';
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var createShell = function createShell(branches) {
var shell = branches.reduce(function (state, branch) {
return _extends({}, state, _defineProperty({}, branch, undefined));
}, {});
var Shell = immutable.Record(shell);
return new Shell();
};
var isReduxAction = function isReduxAction(type) {
return type === '@@INIT' || /^@@redux\/.*$/.test(type);
};
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var isPlainObject = function isPlainObject(subject) {
return (typeof subject === 'undefined' ? 'undefined' : _typeof(subject)) === 'object' && Object.prototype.toString.call(subject) === '[object Object]';
};
var REDUCERS_EXAMPLES = "\n Provide action handlers to the \"createLeaf\" function in one of the following ways:\n\n Option #1:\n { ACTION_TYPE: (state, action) => state }\n\n Option #2:\n {\n ACTION_TYPE: {\n leaf: ['path', 'to', 'leaf'],\n reduce: (state, action) => state,\n },\n }\n\n Option #3:\n {\n ACTION_TYPE: [\n (state, action) => state,\n\n {\n leaf: ['path', 'to', 'leaf'],\n reduce: (state, action) => state,\n },\n ],\n }\n";
var nonRecordState = function nonRecordState(state) {
var stringifiedState = JSON.stringify(state);
return "State must be Immutable Record. Received: " + stringifiedState;
};
var undefinedState = function undefinedState(keyPath, action) {
var stringifiedKeyPath = JSON.stringify(keyPath);
var stringifiedAction = JSON.stringify(action);
return "\n Reducer at keypath " + stringifiedKeyPath + " returned undefined.\n To ignore an action, you must explicitly return the previous state.\n If you want this reducer to hold no value, you can return null instead of undefined.\n\n Keypath: " + stringifiedKeyPath + "\n Action: " + stringifiedAction + "\n ";
};
var badKeyPath = function badKeyPath(keyPath, action) {
var stringifiedKeyPath = JSON.stringify(keyPath);
var stringifiedAction = JSON.stringify(action);
return "\n \"action." + action.type + "\" was dispatched.\n Trying to update state at keypath " + stringifiedKeyPath + " but resolved value is undefined.\n Most likely you misspelled one of the keys in the leaf keypath.\n Re-check your action handlers for this action.\n\n Keypath: " + stringifiedKeyPath + "\n Action: " + stringifiedAction + "\n ";
};
var badActionHandler = function badActionHandler(actionHandler, action) {
var stringifiedActionHandler = JSON.stringify(actionHandler);
var stringifiedAction = JSON.stringify(action);
return "\n Invalid action handler is passed for \"action." + action.type + "\".\n\n Received:\n Action: " + stringifiedAction + "\n Action handler: " + stringifiedActionHandler + "\n\n " + REDUCERS_EXAMPLES + "\n ";
};
var createTree = function createTree(children) {
var childrenNames = Object.keys(children);
return function () {
var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : createShell(childrenNames);
var action = arguments[1];
// Record.isRecord method is available only in immutable@4.x
// For now skipping this check if older version is used
if (state && immutable.Record.isRecord && !immutable.Record.isRecord(state)) {
throw new Error(nonRecordState(state));
}
return state.withMutations(function (nextState) {
childrenNames.forEach(function (child) {
// eslint-disable-next-line no-param-reassign
nextState = children[child](nextState, action, child);
});
});
};
};
var createBranch = function createBranch(children) {
var childrenNames = Object.keys(children);
var keyPath = void 0;
return function (state, action, parentKeyPath) {
if (!keyPath) {
keyPath = [].concat(parentKeyPath);
}
return state.withMutations(function (nextState) {
if (isReduxAction(action.type)) {
var branchState = nextState.getIn(keyPath);
if (!branchState) {
nextState.setIn(keyPath, createShell(childrenNames));
// Record.isRecord method is available only in immutable@4.x
// For now skipping this check if older version is used
} else if (immutable.Record.isRecord && !immutable.Record.isRecord(branchState)) {
throw new Error(nonRecordState(branchState));
}
}
childrenNames.forEach(function (child) {
var childKeyPath = keyPath.concat(child);
// eslint-disable-next-line no-param-reassign
nextState = children[child](nextState, action, childKeyPath);
});
});
};
};
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
var createLeaf = function createLeaf(initialState, reducers) {
if (typeof initialState === 'undefined') {
throw new Error('\n "createLeaf" recieved undefined on initialisation.\n The initial state may not be undefined.\n If you don\'t want to set a value for this reducer,\n you can use null instead of undefined.\n ');
}
if (reducers && !isPlainObject(reducers)) {
throw new Error('\n Invalid action handlers are passed to "createLeaf" function.\n\n Received:\n arg #1: ' + JSON.stringify(initialState) + '\n arg #2: ' + JSON.stringify(reducers) + '\n\n ' + REDUCERS_EXAMPLES + '\n ');
}
if (process.env.NODE_ENV !== 'production') {
// TODO: Validate reducers shape.
}
return function (state, action, keyPath) {
var leafKeyPath = Array.isArray(keyPath) ? keyPath : [keyPath];
var leafState = state.getIn(leafKeyPath);
return state.withMutations(function (nextState) {
// If reducer is a plain function w/ the switch statement
if (!reducers && typeof initialState === 'function') {
var reducer = initialState;
var nextLeafState = reducer(leafState, action);
if (typeof nextLeafState === 'undefined') {
throw new Error(undefinedState(leafKeyPath, action));
}
return nextState.setIn(leafKeyPath, nextLeafState);
}
// On redux action
if (isReduxAction(action.type)) {
// Do nothing if state exists (i.e. rehydrated)
if (typeof leafState !== 'undefined') return;
// Otherwise set initial state
return nextState.setIn(leafKeyPath, initialState);
}
// No reducers attached to the leaf,
// it's fine as the state might be changed from another leafs
if (!reducers) return;
var type = action.type,
payload = _objectWithoutProperties(action, ['type']);
// No reaction required, exiting
if (!reducers[type]) return;
// If value of action type key is function -> reducing local leaf state
if (typeof reducers[type] === 'function') {
var _nextLeafState = reducers[type](leafState, payload);
if (typeof _nextLeafState === 'undefined') {
throw new Error(undefinedState(leafKeyPath, action));
}
return nextState.setIn(leafKeyPath, _nextLeafState);
}
// If it's an object w/ keypath to leaf and reducer -> reducing leaf state at keypath
if (isPlainObject(reducers[type])) {
var externalLeafKeyPath = reducers[type].leaf;
var externalLeafState = nextState.getIn(externalLeafKeyPath);
if (typeof externalLeafState === 'undefined') {
// NOTE: Might worth to check if this is exactly leaf, not a branch, at provided keypath.
// But this data must be stored somewhere. Premature optimisation at this point.
throw new Error(badKeyPath(externalLeafKeyPath, action));
}
var nextExternalLeafState = reducers[type].reduce(externalLeafState, payload);
if (typeof nextExternalLeafState === 'undefined') {
throw new Error(undefinedState(externalLeafKeyPath, action));
}
return nextState.setIn(externalLeafKeyPath, nextExternalLeafState);
}
// If it's an array -> iterating
if (Array.isArray(reducers[type])) {
return reducers[type].forEach(function (reducer) {
// If array's item is a function -> reducing local leaf state
if (typeof reducer === 'function') {
var _nextLeafState2 = reducer(leafState, payload);
if (typeof _nextLeafState2 === 'undefined') {
throw new Error(undefinedState(leafKeyPath, action));
}
return nextState.setIn(leafKeyPath, _nextLeafState2);
}
// Otherwise reducing state of the leaf at provided keypath
var externalLeafState = nextState.getIn(reducer.leaf);
if (typeof externalLeafState === 'undefined') {
throw new Error(badKeyPath(reducer.leaf, action));
}
var nextExternalLeafState = reducer.reduce(externalLeafState, payload);
if (typeof nextExternalLeafState === 'undefined') {
throw new Error(undefinedState(reducer.leaf, action));
}
return nextState.setIn(reducer.leaf, nextExternalLeafState);
});
}
throw new Error(badActionHandler(reducers[type], action));
});
};
};
exports.createTree = createTree;
exports.createBranch = createBranch;
exports.createLeaf = createLeaf;
Object.defineProperty(exports, '__esModule', { value: true });
})));