envoc-hook-local-slice
Version:
envoc-table
129 lines (128 loc) • 6.39 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useLocalSlice = void 0;
var react_1 = require("react");
var immer_1 = __importDefault(require("immer"));
function useLocalSlice(_a) {
var initialState = _a.initialState, reducers = _a.reducers, _b = _a.slice, slice = _b === void 0 ? 'unnamed' : _b;
(0, react_1.useDebugValue)(slice);
var reducerKeys = Object.keys(reducers);
var stateRef = (0, react_1.useRef)(initialState);
var currentDraftRef = (0, react_1.useRef)();
var reducersRef = (0, react_1.useRef)(reducers);
reducersRef.current = reducers;
var abortControllersRef = (0, react_1.useRef)([]);
var _c = (0, react_1.useState)(initialState), visibleState = _c[0], setVisibleState = _c[1];
(0, react_1.useEffect)(function () {
// trigger all aborts due to an unmount
var ref = abortControllersRef;
return function cleanupAborts() {
var abortControllers = ref.current;
for (var i = 0; i < abortControllers.length; i++) {
var abortController = abortControllers[i];
abortController.abort();
}
};
}, []);
var dispatchAction = (0, react_1.useMemo)(function () {
// a map of actions available.
// should be one method per provided reducer name
var map = {};
var _loop_1 = function (actionType) {
map[actionType] = function (payload) {
// each time an action is called with some payload the correct method should be called with the current state
// this includes ensuring that if a reducer calls another action that the action called has the correct draft state
handledDispatchedAction(actionType, reducersRef, payload, stateRef, currentDraftRef, abortControllersRef, setVisibleState);
};
};
// each reducer key is also the name of an action
for (var _i = 0, reducerKeys_1 = reducerKeys; _i < reducerKeys_1.length; _i++) {
var actionType = reducerKeys_1[_i];
_loop_1(actionType);
}
return map;
},
// we only want new actions if the keys of the reducers change (which should be never to be honest)
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(reducerKeys)]);
return [visibleState, dispatchAction];
}
exports.useLocalSlice = useLocalSlice;
// handles each call to an action method in a consistent way
function handledDispatchedAction(actionType, reducersRef, payload, stateRef, currentDraftRef, abortControllersRef, setVisibleState) {
// this is the actual reducer - it could be a direct function or an object (checked by typescript)
var reducers = reducersRef.current;
var reducerDef = reducers[actionType];
if (typeof reducerDef === 'function') {
// standard reducer function
callFunctionWithDraft(function (draft) { return reducerDef(draft, payload); });
}
else {
// action with a promise - so let's make sure we can cancel
var controller_1 = new AbortController();
abortControllersRef.current.push(controller_1);
callFunctionWithDraft(function (draft) {
// pending method generate the promise
var promise = reducerDef.pending(draft, payload, controller_1.signal);
if (reducerDef.resolved) {
// only try 'resolved' if provided
var resolvedBlock_1 = reducerDef.resolved;
promise = promise.then(function (x) {
if (controller_1.signal.aborted) {
// never call anything if we have aborted
return;
}
callFunctionWithDraft(function (draft) { return resolvedBlock_1(draft, payload, x); });
});
}
var rejectedBlock = reducerDef.rejected;
// we always handle the catch to avoid needless cancel errors in some cases
// TODO: thoughts?
promise = promise.catch(function (e) {
if (controller_1.signal.aborted) {
return;
}
if (!rejectedBlock) {
throw e;
}
callFunctionWithDraft(function (draft) { return rejectedBlock(draft, payload, e); });
});
var finallyBlock = reducerDef.finally;
promise.finally(function () {
if (controller_1.signal.aborted) {
return;
}
abortControllersRef.current = abortControllersRef.current.filter(function (x) { return x !== controller_1; });
if (!finallyBlock) {
return;
}
callFunctionWithDraft(function (draft) { return finallyBlock(draft, payload); });
});
});
}
// calls to adjust the state through callback should be made consistent
// Note especially the usage of currentDraftRef
function callFunctionWithDraft(callback) {
if (currentDraftRef.current) {
//there is already a draft being built - keep using that.
// e.g. a reducer calling another action before returning will hit this
// failure to call with the ongoing draft results in inconsistent behavior
callback(currentDraftRef.current);
}
else {
// this is the initial call to the actions, we will produce the draft
stateRef.current = (0, immer_1.default)(stateRef.current, function (draftState) {
currentDraftRef.current = draftState;
// triggering the callback may result in other actions being called
// so we set the current working draft ref first
callback(currentDraftRef.current);
});
// as soon as the state is updated clear our working copy and update the visible state
currentDraftRef.current = undefined;
setVisibleState(stateRef.current);
}
}
}