UNPKG

react-sweet-state

Version:

Global + local state combining the best of Redux and Context API

284 lines (223 loc) 14.5 kB
"use strict"; 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; }