react-sweet-state
Version:
Global + local state combining the best of Redux and Context API
284 lines (223 loc) • 14.5 kB
JavaScript
;
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createContainer = createContainer;
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _context = require("../context");
var _store = require("../store");
var _shallowEqual = _interopRequireDefault(require("../utils/shallow-equal"));
var _excluded = ["children"],
_excluded2 = ["scope", "isGlobal"];
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
var noop = function noop() {
return function () {};
};
function createContainer() {
var StoreOrOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$onInit = _ref.onInit,
_onInit = _ref$onInit === void 0 ? noop : _ref$onInit,
_ref$onUpdate = _ref.onUpdate,
onUpdate = _ref$onUpdate === void 0 ? noop : _ref$onUpdate,
_ref$onCleanup = _ref.onCleanup,
onCleanup = _ref$onCleanup === void 0 ? noop : _ref$onCleanup,
_ref$displayName = _ref.displayName,
displayName = _ref$displayName === void 0 ? '' : _ref$displayName;
if ('key' in StoreOrOptions) {
var Store = StoreOrOptions;
var dn = displayName || "Container(".concat(Store.key.split('__')[0], ")");
return createFunctionContainer({
displayName: dn,
// compat fields
override: {
Store: Store,
handlers: Object.assign({}, _onInit !== noop && {
onInit: function onInit() {
return _onInit();
}
}, onCleanup !== noop && {
onDestroy: function onDestroy() {
return onCleanup();
}
}, // TODO: on next major pass through next/prev props args
onUpdate !== noop && {
onContainerUpdate: function onContainerUpdate() {
return onUpdate();
}
})
}
});
}
return createFunctionContainer(StoreOrOptions);
}
function useRegistry(scope, isGlobal, _ref2) {
var globalRegistry = _ref2.globalRegistry;
return (0, _react.useMemo)(function () {
var isLocal = !scope && !isGlobal;
return isLocal ? new _store.StoreRegistry('__local__') : globalRegistry;
}, [scope, isGlobal, globalRegistry]);
}
function useContainedStore(scope, registry, propsRef, check, override) {
// Store contained scopes in a map, but throwing it away on scope change
// eslint-disable-next-line react-hooks/exhaustive-deps
var containedStores = (0, _react.useMemo)(function () {
return new Map();
}, [scope]);
var getContainedStore = (0, _react.useCallback)(function (Store) {
var containedStore = containedStores.get(Store); // first time it gets called we add store to contained map bound
// so we can provide props to actions (only triggered by children)
if (!containedStore) {
var _handlers$onInit;
var isExisting = registry.hasStore(Store, scope);
var config = {
props: function props() {
return propsRef.current.sub;
},
contained: check
};
var _registry$getStore = registry.getStore(Store, scope, config),
storeState = _registry$getStore.storeState;
var actions = (0, _store.bindActions)(Store.actions, storeState, config);
var handlers = (0, _store.bindActions)(Object.assign({}, Store.handlers, override === null || override === void 0 ? void 0 : override.handlers), storeState, config, actions);
containedStore = {
storeState: storeState,
actions: actions,
handlers: handlers,
unsubscribe: undefined
};
containedStores.set(Store, containedStore); // Signal store is contained and ready now, so by the time
// consumers subscribe we already have updated the store (if needed).
// Also if override maintain legacy behaviour, triggered on every mount
if (!isExisting || override) (_handlers$onInit = handlers.onInit) === null || _handlers$onInit === void 0 ? void 0 : _handlers$onInit.call(handlers);
}
return containedStore;
}, [containedStores, scope, registry, propsRef, check, override]);
return [containedStores, getContainedStore];
}
function useApi(check, getContainedStore, _ref3) {
var globalRegistry = _ref3.globalRegistry,
retrieveStore = _ref3.retrieveStore;
var retrieveRef = (0, _react.useRef)();
retrieveRef.current = function (Store) {
return check(Store) ? getContainedStore(Store) : retrieveStore(Store);
}; // This api is "frozen", as changing it will trigger re-render across all consumers
// so we link retrieveStore dynamically and manually call notify() on scope change
return (0, _react.useMemo)(function () {
return {
globalRegistry: globalRegistry,
retrieveStore: function retrieveStore(s) {
return retrieveRef.current(s);
}
};
}, [globalRegistry]);
}
function createFunctionContainer() {
var _ref4 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
displayName = _ref4.displayName,
override = _ref4.override;
var check = function check(store) {
return override ? store === override.Store : store.containedBy === FunctionContainer;
};
function FunctionContainer(props) {
var children = props.children,
restProps = _objectWithoutProperties(props, _excluded);
var scope = restProps.scope,
isGlobal = restProps.isGlobal,
subProps = _objectWithoutProperties(restProps, _excluded2);
var ctx = (0, _react.useContext)(_context.Context);
var registry = useRegistry(scope, isGlobal, ctx); // Store props in a ref to avoid re-binding actions when they change and re-rendering all
// consumers unnecessarily. The update is handled by an effect on the component instead
var propsRef = (0, _react.useRef)({
prev: null,
next: restProps,
sub: subProps
});
propsRef.current = {
prev: propsRef.current.next,
next: restProps,
sub: subProps // TODO remove on next major
};
var _useContainedStore = useContainedStore(scope, registry, propsRef, check, override),
_useContainedStore2 = _slicedToArray(_useContainedStore, 2),
containedStores = _useContainedStore2[0],
getContainedStore = _useContainedStore2[1]; // Use a stable object as is passed as value to context Provider
var api = useApi(check, getContainedStore, ctx); // This listens for custom props change, and so we trigger container update actions
// before the re-render gets to consumers (hence why side effect on render).
// We do not use React hooks because num of restProps might change and react will throws
if (!(0, _shallowEqual["default"])(propsRef.current.next, propsRef.current.prev)) {
containedStores.forEach(function (_ref5) {
var _handlers$onContainer;
var handlers = _ref5.handlers;
(_handlers$onContainer = handlers.onContainerUpdate) === null || _handlers$onContainer === void 0 ? void 0 : _handlers$onContainer.call(handlers, propsRef.current.next, propsRef.current.prev);
});
} // Every time we add/remove a contained store, we ensure we are subscribed to the updates
// as an effect to properly handle strict mode
(0, _react.useEffect)(function () {
containedStores.forEach(function (containedStore) {
if (!containedStore.unsubscribe) {
var unsub = containedStore.storeState.subscribe(function () {
var _containedStore$handl, _containedStore$handl2;
return (_containedStore$handl = (_containedStore$handl2 = containedStore.handlers).onUpdate) === null || _containedStore$handl === void 0 ? void 0 : _containedStore$handl.call(_containedStore$handl2);
});
containedStore.unsubscribe = function () {
unsub();
containedStore.unsubscribe = undefined;
};
}
});
}, [containedStores, containedStores.size]); // We support renderding "bootstrap" containers without children with override API
// so in this case we call getCS to initialize the store globally asap
if (override && !containedStores.size && (scope || isGlobal)) {
getContainedStore(override.Store);
} // This listens for scope change or component unmount, to notify all consumers
// so all work is done on cleanup
(0, _react.useEffect)(function () {
return function () {
containedStores.forEach(function (_ref6, Store) {
var storeState = _ref6.storeState,
handlers = _ref6.handlers,
unsubscribe = _ref6.unsubscribe;
// Detatch container from subscription
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe(); // Trigger a forced update on all subscribers as we opted out from context
// Some might have already re-rendered naturally, but we "force update" all anyway.
// This is sub-optimal as if there are other containers with the same
// old scope id we will re-render those too, but still better than context
storeState.notify(); // Given unsubscription is handled by useSyncExternalStore, we have no control on when
// React decides to do it. So we schedule on next tick to run last
Promise.resolve().then(function () {
var _registry$getStore2;
if (!storeState.listeners().size && // ensure registry has not already created a new store with same scope
storeState === ((_registry$getStore2 = registry.getStore(Store, scope, null)) === null || _registry$getStore2 === void 0 ? void 0 : _registry$getStore2.storeState)) {
var _handlers$onDestroy;
(_handlers$onDestroy = handlers.onDestroy) === null || _handlers$onDestroy === void 0 ? void 0 : _handlers$onDestroy.call(handlers); // We only delete scoped stores, as global shall persist and local are auto-deleted
if (!isGlobal) registry.deleteStore(Store, scope);
}
});
}); // no need to reset containedStores as the map is already bound to scope
};
}, [registry, scope, isGlobal, containedStores]);
return /*#__PURE__*/_react["default"].createElement(_context.Context.Provider, {
value: api
}, children);
}
FunctionContainer.displayName = displayName || "Container";
FunctionContainer.propTypes = {
children: _propTypes["default"].node,
scope: _propTypes["default"].string,
isGlobal: _propTypes["default"].bool
};
return FunctionContainer;
}