UNPKG

mobx-react-lite

Version:

Lightweight React bindings for MobX based on React 16.8+ and Hooks

601 lines (493 loc) 19.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var mobx = require('mobx'); var React = require('react'); var React__default = _interopDefault(React); var reactDom = require('react-dom'); if (!React.useState) { throw new Error("mobx-react-lite requires React with Hooks support"); } if (!mobx.makeObservable) { throw new Error("mobx-react-lite@3 requires mobx at least version 6 to be available"); } function defaultNoopBatch(callback) { callback(); } function observerBatching(reactionScheduler) { if (!reactionScheduler) { reactionScheduler = defaultNoopBatch; { console.warn("[MobX] Failed to get unstable_batched updates from react-dom / react-native"); } } mobx.configure({ reactionScheduler: reactionScheduler }); } var isObserverBatched = function isObserverBatched() { { console.warn("[MobX] Deprecated"); } return true; }; var deprecatedMessages = []; function useDeprecated(msg) { if (!deprecatedMessages.includes(msg)) { deprecatedMessages.push(msg); console.warn(msg); } } function printDebugValue(v) { return mobx.getDependencyTree(v); } var FinalizationRegistryLocal = typeof FinalizationRegistry === "undefined" ? undefined : FinalizationRegistry; function createTrackingData(reaction) { var trackingData = { reaction: reaction, mounted: false, changedBeforeMount: false, cleanAt: Date.now() + CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS }; return trackingData; } /** * The minimum time before we'll clean up a Reaction created in a render * for a component that hasn't managed to run its effects. This needs to * be big enough to ensure that a component won't turn up and have its * effects run without being re-rendered. */ var CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS = 10000; /** * The frequency with which we'll check for leaked reactions. */ var CLEANUP_TIMER_LOOP_MILLIS = 10000; /** * FinalizationRegistry-based uncommitted reaction cleanup */ function createReactionCleanupTrackingUsingFinalizationRegister(FinalizationRegistry) { var cleanupTokenToReactionTrackingMap = new Map(); var globalCleanupTokensCounter = 1; var registry = new FinalizationRegistry(function cleanupFunction(token) { var trackedReaction = cleanupTokenToReactionTrackingMap.get(token); if (trackedReaction) { trackedReaction.reaction.dispose(); cleanupTokenToReactionTrackingMap["delete"](token); } }); return { addReactionToTrack: function addReactionToTrack(reactionTrackingRef, reaction, objectRetainedByReact) { var token = globalCleanupTokensCounter++; registry.register(objectRetainedByReact, token, reactionTrackingRef); reactionTrackingRef.current = createTrackingData(reaction); reactionTrackingRef.current.finalizationRegistryCleanupToken = token; cleanupTokenToReactionTrackingMap.set(token, reactionTrackingRef.current); return reactionTrackingRef.current; }, recordReactionAsCommitted: function recordReactionAsCommitted(reactionRef) { registry.unregister(reactionRef); if (reactionRef.current && reactionRef.current.finalizationRegistryCleanupToken) { cleanupTokenToReactionTrackingMap["delete"](reactionRef.current.finalizationRegistryCleanupToken); } }, forceCleanupTimerToRunNowForTests: function forceCleanupTimerToRunNowForTests() {// When FinalizationRegistry in use, this this is no-op }, resetCleanupScheduleForTests: function resetCleanupScheduleForTests() {// When FinalizationRegistry in use, this this is no-op } }; } function _extends() { _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; }; return _extends.apply(this, arguments); } 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 _createForOfIteratorHelperLoose(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (it) return (it = it.call(o)).next.bind(it); if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; return function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } /** * timers, gc-style, uncommitted reaction cleanup */ function createTimerBasedReactionCleanupTracking() { /** * Reactions created by components that have yet to be fully mounted. */ var uncommittedReactionRefs = new Set(); /** * Latest 'uncommitted reactions' cleanup timer handle. */ var reactionCleanupHandle; /* istanbul ignore next */ /** * Only to be used by test functions; do not export outside of mobx-react-lite */ function forceCleanupTimerToRunNowForTests() { // This allows us to control the execution of the cleanup timer // to force it to run at awkward times in unit tests. if (reactionCleanupHandle) { clearTimeout(reactionCleanupHandle); cleanUncommittedReactions(); } } /* istanbul ignore next */ function resetCleanupScheduleForTests() { if (uncommittedReactionRefs.size > 0) { for (var _iterator = _createForOfIteratorHelperLoose(uncommittedReactionRefs), _step; !(_step = _iterator()).done;) { var ref = _step.value; var tracking = ref.current; if (tracking) { tracking.reaction.dispose(); ref.current = null; } } uncommittedReactionRefs.clear(); } if (reactionCleanupHandle) { clearTimeout(reactionCleanupHandle); reactionCleanupHandle = undefined; } } function ensureCleanupTimerRunning() { if (reactionCleanupHandle === undefined) { reactionCleanupHandle = setTimeout(cleanUncommittedReactions, CLEANUP_TIMER_LOOP_MILLIS); } } function scheduleCleanupOfReactionIfLeaked(ref) { uncommittedReactionRefs.add(ref); ensureCleanupTimerRunning(); } function recordReactionAsCommitted(reactionRef) { uncommittedReactionRefs["delete"](reactionRef); } /** * Run by the cleanup timer to dispose any outstanding reactions */ function cleanUncommittedReactions() { reactionCleanupHandle = undefined; // Loop through all the candidate leaked reactions; those older // than CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS get tidied. var now = Date.now(); uncommittedReactionRefs.forEach(function (ref) { var tracking = ref.current; if (tracking) { if (now >= tracking.cleanAt) { // It's time to tidy up this leaked reaction. tracking.reaction.dispose(); ref.current = null; uncommittedReactionRefs["delete"](ref); } } }); if (uncommittedReactionRefs.size > 0) { // We've just finished a round of cleanups but there are still // some leak candidates outstanding. ensureCleanupTimerRunning(); } } return { addReactionToTrack: function addReactionToTrack(reactionTrackingRef, reaction, /** * On timer based implementation we don't really need this object, * but we keep the same api */ objectRetainedByReact) { reactionTrackingRef.current = createTrackingData(reaction); scheduleCleanupOfReactionIfLeaked(reactionTrackingRef); return reactionTrackingRef.current; }, recordReactionAsCommitted: recordReactionAsCommitted, forceCleanupTimerToRunNowForTests: forceCleanupTimerToRunNowForTests, resetCleanupScheduleForTests: resetCleanupScheduleForTests }; } var _ref = FinalizationRegistryLocal ? /*#__PURE__*/createReactionCleanupTrackingUsingFinalizationRegister(FinalizationRegistryLocal) : /*#__PURE__*/createTimerBasedReactionCleanupTracking(), addReactionToTrack = _ref.addReactionToTrack, recordReactionAsCommitted = _ref.recordReactionAsCommitted, resetCleanupScheduleForTests = _ref.resetCleanupScheduleForTests; var globalIsUsingStaticRendering = false; function enableStaticRendering(enable) { globalIsUsingStaticRendering = enable; } function isUsingStaticRendering() { return globalIsUsingStaticRendering; } function observerComponentNameFor(baseComponentName) { return "observer" + baseComponentName; } /** * We use class to make it easier to detect in heap snapshots by name */ var ObjectToBeRetainedByReact = function ObjectToBeRetainedByReact() {}; function objectToBeRetainedByReactFactory() { return new ObjectToBeRetainedByReact(); } function useObserver(fn, baseComponentName) { if (baseComponentName === void 0) { baseComponentName = "observed"; } if (isUsingStaticRendering()) { return fn(); } var _React$useState = React__default.useState(objectToBeRetainedByReactFactory), objectRetainedByReact = _React$useState[0]; // Force update, see #2982 var _React$useState2 = React__default.useState(), setState = _React$useState2[1]; var forceUpdate = function forceUpdate() { return setState([]); }; // StrictMode/ConcurrentMode/Suspense may mean that our component is // rendered and abandoned multiple times, so we need to track leaked // Reactions. var reactionTrackingRef = React__default.useRef(null); if (!reactionTrackingRef.current) { // First render for this component (or first time since a previous // reaction from an abandoned render was disposed). var newReaction = new mobx.Reaction(observerComponentNameFor(baseComponentName), function () { // Observable has changed, meaning we want to re-render // BUT if we're a component that hasn't yet got to the useEffect() // stage, we might be a component that _started_ to render, but // got dropped, and we don't want to make state changes then. // (It triggers warnings in StrictMode, for a start.) if (trackingData.mounted) { // We have reached useEffect(), so we're mounted, and can trigger an update forceUpdate(); } else { // We haven't yet reached useEffect(), so we'll need to trigger a re-render // when (and if) useEffect() arrives. trackingData.changedBeforeMount = true; } }); var trackingData = addReactionToTrack(reactionTrackingRef, newReaction, objectRetainedByReact); } var reaction = reactionTrackingRef.current.reaction; React__default.useDebugValue(reaction, printDebugValue); React__default.useEffect(function () { // Called on first mount only recordReactionAsCommitted(reactionTrackingRef); if (reactionTrackingRef.current) { // Great. We've already got our reaction from our render; // all we need to do is to record that it's now mounted, // to allow future observable changes to trigger re-renders reactionTrackingRef.current.mounted = true; // Got a change before first mount, force an update if (reactionTrackingRef.current.changedBeforeMount) { reactionTrackingRef.current.changedBeforeMount = false; forceUpdate(); } } else { // The reaction we set up in our render has been disposed. // This can be due to bad timings of renderings, e.g. our // component was paused for a _very_ long time, and our // reaction got cleaned up // Re-create the reaction reactionTrackingRef.current = { reaction: new mobx.Reaction(observerComponentNameFor(baseComponentName), function () { // We've definitely already been mounted at this point forceUpdate(); }), mounted: true, changedBeforeMount: false, cleanAt: Infinity }; forceUpdate(); } return function () { reactionTrackingRef.current.reaction.dispose(); reactionTrackingRef.current = null; }; }, []); // render the original component, but have the // reaction track the observables, so that rendering // can be invalidated (see above) once a dependency changes var rendering; var exception; reaction.track(function () { try { rendering = fn(); } catch (e) { exception = e; } }); if (exception) { throw exception; // re-throw any exceptions caught during rendering } return rendering; } function observer(baseComponent, options) { // The working of observer is explained step by step in this talk: https://www.youtube.com/watch?v=cPF4iBedoF0&feature=youtu.be&t=1307 if (isUsingStaticRendering()) { return baseComponent; } var realOptions = _extends({ forwardRef: false }, options); var baseComponentName = baseComponent.displayName || baseComponent.name; var wrappedComponent = function wrappedComponent(props, ref) { return useObserver(function () { return baseComponent(props, ref); }, baseComponentName); }; // Don't set `displayName` for anonymous components, // so the `displayName` can be customized by user, see #3192. if (baseComponentName !== "") { wrappedComponent.displayName = baseComponentName; } // Support legacy context: `contextTypes` must be applied before `memo` if (baseComponent.contextTypes) { wrappedComponent.contextTypes = baseComponent.contextTypes; } // memo; we are not interested in deep updates // in props; we assume that if deep objects are changed, // this is in observables, which would have been tracked anyway var memoComponent; if (realOptions.forwardRef) { // we have to use forwardRef here because: // 1. it cannot go before memo, only after it // 2. forwardRef converts the function into an actual component, so we can't let the baseComponent do it // since it wouldn't be a callable function anymore memoComponent = React.memo(React.forwardRef(wrappedComponent)); } else { memoComponent = React.memo(wrappedComponent); } copyStaticProperties(baseComponent, memoComponent); { Object.defineProperty(memoComponent, "contextTypes", { set: function set() { var _this$type; throw new Error("[mobx-react-lite] `" + (this.displayName || ((_this$type = this.type) == null ? void 0 : _this$type.displayName) || "Component") + ".contextTypes` must be set before applying `observer`."); } }); } return memoComponent; } // based on https://github.com/mridgway/hoist-non-react-statics/blob/master/src/index.js var hoistBlackList = { $$typeof: true, render: true, compare: true, type: true, // Don't redefine `displayName`, // it's defined as getter-setter pair on `memo` (see #3192). displayName: true }; function copyStaticProperties(base, target) { Object.keys(base).forEach(function (key) { if (!hoistBlackList[key]) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(base, key)); } }); } function ObserverComponent(_ref) { var children = _ref.children, render = _ref.render; var component = children || render; if (typeof component !== "function") { return null; } return useObserver(component); } { ObserverComponent.propTypes = { children: ObserverPropsCheck, render: ObserverPropsCheck }; } ObserverComponent.displayName = "Observer"; function ObserverPropsCheck(props, key, componentName, location, propFullName) { var extraKey = key === "children" ? "render" : "children"; var hasProp = typeof props[key] === "function"; var hasExtraProp = typeof props[extraKey] === "function"; if (hasProp && hasExtraProp) { return new Error("MobX Observer: Do not use children and render in the same time in`" + componentName); } if (hasProp || hasExtraProp) { return null; } return new Error("Invalid prop `" + propFullName + "` of type `" + typeof props[key] + "` supplied to" + " `" + componentName + "`, expected `function`."); } function useLocalObservable(initializer, annotations) { return React.useState(function () { return mobx.observable(initializer(), annotations, { autoBind: true }); })[0]; } function useAsObservableSource(current) { useDeprecated("[mobx-react-lite] 'useAsObservableSource' is deprecated, please store the values directly in an observable, for example by using 'useLocalObservable', and sync future updates using 'useEffect' when needed. See the README for examples."); var _useState = React.useState(function () { return mobx.observable(current, {}, { deep: false }); }), res = _useState[0]; mobx.runInAction(function () { Object.assign(res, current); }); return res; } function useLocalStore(initializer, current) { useDeprecated("[mobx-react-lite] 'useLocalStore' is deprecated, use 'useLocalObservable' instead."); var source = current && useAsObservableSource(current); return React.useState(function () { return mobx.observable(initializer(source), undefined, { autoBind: true }); })[0]; } observerBatching(reactDom.unstable_batchedUpdates); function useObserver$1(fn, baseComponentName) { if (baseComponentName === void 0) { baseComponentName = "observed"; } { useDeprecated("[mobx-react-lite] 'useObserver(fn)' is deprecated. Use `<Observer>{fn}</Observer>` instead, or wrap the entire component in `observer`."); } return useObserver(fn, baseComponentName); } function useStaticRendering(enable) { { console.warn("[mobx-react-lite] 'useStaticRendering' is deprecated, use 'enableStaticRendering' instead"); } enableStaticRendering(enable); } exports.Observer = ObserverComponent; exports.clearTimers = resetCleanupScheduleForTests; exports.enableStaticRendering = enableStaticRendering; exports.isObserverBatched = isObserverBatched; exports.isUsingStaticRendering = isUsingStaticRendering; exports.observer = observer; exports.observerBatching = observerBatching; exports.useAsObservableSource = useAsObservableSource; exports.useLocalObservable = useLocalObservable; exports.useLocalStore = useLocalStore; exports.useObserver = useObserver$1; exports.useStaticRendering = useStaticRendering; //# sourceMappingURL=mobxreactlite.cjs.development.js.map