UNPKG

mobx-react

Version:

React bindings for MobX. Create fully reactive components.

387 lines (377 loc) 18 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('mobx'), require('react'), require('mobx-react-lite')) : typeof define === 'function' && define.amd ? define(['exports', 'mobx', 'react', 'mobx-react-lite'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mobxReact = {}, global.mobx, global.React, global.mobxReactLite)); })(this, (function (exports, mobx, React, mobxReactLite) { 'use strict'; function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return n; } var React__namespace = /*#__PURE__*/_interopNamespace(React); function shallowEqual(objA, objB) { //From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js if (is(objA, objB)) { return true; } if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } for (let i = 0; i < keysA.length; i++) { if (!Object.hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { return false; } } return true; } function is(x, y) { // From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js if (x === y) { return x !== 0 || 1 / x === 1 / y; } else { return x !== x && y !== y; } } /** * Utilities for patching componentWillUnmount, to make sure @disposeOnUnmount works correctly icm with user defined hooks * and the handler provided by mobx-react */ const mobxMixins = /*#__PURE__*/Symbol("patchMixins"); const mobxPatchedDefinition = /*#__PURE__*/Symbol("patchedDefinition"); function getMixins(target, methodName) { const mixins = target[mobxMixins] = target[mobxMixins] || {}; const methodMixins = mixins[methodName] = mixins[methodName] || {}; methodMixins.locks = methodMixins.locks || 0; methodMixins.methods = methodMixins.methods || []; return methodMixins; } function wrapper(realMethod, mixins, ...args) { // locks are used to ensure that mixins are invoked only once per invocation, even on recursive calls mixins.locks++; try { let retVal; if (realMethod !== undefined && realMethod !== null) { retVal = realMethod.apply(this, args); } return retVal; } finally { mixins.locks--; if (mixins.locks === 0) { mixins.methods.forEach(mx => { mx.apply(this, args); }); } } } function wrapFunction(realMethod, mixins) { const fn = function fn(...args) { wrapper.call(this, realMethod, mixins, ...args); }; return fn; } function patch(target, methodName, mixinMethod) { const mixins = getMixins(target, methodName); if (mixins.methods.indexOf(mixinMethod) < 0) { mixins.methods.push(mixinMethod); } const oldDefinition = Object.getOwnPropertyDescriptor(target, methodName); if (oldDefinition && oldDefinition[mobxPatchedDefinition]) { // already patched definition, do not repatch return; } const originalMethod = target[methodName]; const newDefinition = createDefinition(target, methodName, oldDefinition ? oldDefinition.enumerable : undefined, mixins, originalMethod); Object.defineProperty(target, methodName, newDefinition); } function createDefinition(target, methodName, enumerable, mixins, originalMethod) { let wrappedFunc = wrapFunction(originalMethod, mixins); return { // @ts-ignore [mobxPatchedDefinition]: true, get: function () { return wrappedFunc; }, set: function (value) { if (this === target) { wrappedFunc = wrapFunction(value, mixins); } else { // when it is an instance of the prototype/a child prototype patch that particular case again separately // since we need to store separate values depending on wether it is the actual instance, the prototype, etc // e.g. the method for super might not be the same as the method for the prototype which might be not the same // as the method for the instance const newDefinition = createDefinition(this, methodName, enumerable, mixins, value); Object.defineProperty(this, methodName, newDefinition); } }, configurable: true, enumerable: enumerable }; } const administrationSymbol = /*#__PURE__*/Symbol("ObserverAdministration"); const isMobXReactObserverSymbol = /*#__PURE__*/Symbol("isMobXReactObserver"); let observablePropDescriptors; { observablePropDescriptors = { props: /*#__PURE__*/createObservablePropDescriptor("props"), state: /*#__PURE__*/createObservablePropDescriptor("state"), context: /*#__PURE__*/createObservablePropDescriptor("context") }; } function getAdministration(component) { var _component$administra; // We create administration lazily, because we can't patch constructor // and the exact moment of initialization partially depends on React internals. // At the time of writing this, the first thing invoked is one of the observable getter/setter (state/props/context). return (_component$administra = component[administrationSymbol]) != null ? _component$administra : component[administrationSymbol] = { reaction: null, mounted: false, reactionInvalidatedBeforeMount: false, forceUpdate: null, name: getDisplayName(component.constructor), state: undefined, props: undefined, context: undefined }; } function makeClassComponentObserver(componentClass) { const { prototype } = componentClass; if (componentClass[isMobXReactObserverSymbol]) { const displayName = getDisplayName(componentClass); throw new Error(`The provided component class (${displayName}) has already been declared as an observer component.`); } else { componentClass[isMobXReactObserverSymbol] = true; } if (prototype.componentWillReact) { throw new Error("The componentWillReact life-cycle event is no longer supported"); } if (componentClass["__proto__"] !== React.PureComponent) { if (!prototype.shouldComponentUpdate) { prototype.shouldComponentUpdate = observerSCU; } else if (prototype.shouldComponentUpdate !== observerSCU) { // n.b. unequal check, instead of existence check, as @observer might be on superclass as well throw new Error("It is not allowed to use shouldComponentUpdate in observer based components."); } } { Object.defineProperties(prototype, observablePropDescriptors); } const originalRender = prototype.render; if (typeof originalRender !== "function") { const displayName = getDisplayName(componentClass); throw new Error(`[mobx-react] class component (${displayName}) is missing \`render\` method.` + `\n\`observer\` requires \`render\` being a function defined on prototype.` + `\n\`render = () => {}\` or \`render = function() {}\` is not supported.`); } prototype.render = function () { Object.defineProperty(this, "render", { // There is no safe way to replace render, therefore it's forbidden. configurable: false, writable: false, value: mobxReactLite.isUsingStaticRendering() ? originalRender : createReactiveRender.call(this, originalRender) }); return this.render(); }; const originalComponentDidMount = prototype.componentDidMount; prototype.componentDidMount = function () { if (this.componentDidMount !== Object.getPrototypeOf(this).componentDidMount) { const displayName = getDisplayName(componentClass); throw new Error(`[mobx-react] \`observer(${displayName}).componentDidMount\` must be defined on prototype.` + `\n\`componentDidMount = () => {}\` or \`componentDidMount = function() {}\` is not supported.`); } // `componentDidMount` may not be called at all. React can abandon the instance after `render`. // That's why we use finalization registry to dispose reaction created during render. // Happens with `<Suspend>` see #3492 // // `componentDidMount` can be called immediately after `componentWillUnmount` without calling `render` in between. // Happens with `<StrictMode>`see #3395. // // If `componentDidMount` is called, it's guaranteed to run synchronously with render (similary to `useLayoutEffect`). // Therefore we don't have to worry about external (observable) state being updated before mount (no state version checking). // // Things may change: "In the future, React will provide a feature that lets components preserve state between unmounts" const admin = getAdministration(this); admin.mounted = true; // Component instance committed, prevent reaction disposal. mobxReactLite._observerFinalizationRegistry.unregister(this); // We don't set forceUpdate before mount because it requires a reference to `this`, // therefore `this` could NOT be garbage collected before mount, // preventing reaction disposal by FinalizationRegistry and leading to memory leak. // As an alternative we could have `admin.instanceRef = new WeakRef(this)`, but lets avoid it if possible. admin.forceUpdate = () => this.forceUpdate(); if (!admin.reaction || admin.reactionInvalidatedBeforeMount) { // Missing reaction: // 1. Instance was unmounted (reaction disposed) and immediately remounted without running render #3395. // 2. Reaction was disposed by finalization registry before mount. Shouldn't ever happen for class components: // `componentDidMount` runs synchronously after render, but our registry are deferred (can't run in between). // In any case we lost subscriptions to observables, so we have to create new reaction and re-render to resubscribe. // The reaction will be created lazily by following render. // Reaction invalidated before mount: // 1. A descendant's `componenDidMount` invalidated it's parent #3730 admin.forceUpdate(); } return originalComponentDidMount == null ? void 0 : originalComponentDidMount.apply(this, arguments); }; // TODO@major Overly complicated "patch" is only needed to support the deprecated @disposeOnUnmount patch(prototype, "componentWillUnmount", function () { var _admin$reaction; if (mobxReactLite.isUsingStaticRendering()) { return; } const admin = getAdministration(this); (_admin$reaction = admin.reaction) == null || _admin$reaction.dispose(); admin.reaction = null; admin.forceUpdate = null; admin.mounted = false; admin.reactionInvalidatedBeforeMount = false; }); return componentClass; } // Generates a friendly name for debugging function getDisplayName(componentClass) { return componentClass.displayName || componentClass.name || "<component>"; } function createReactiveRender(originalRender) { const boundOriginalRender = originalRender.bind(this); const admin = getAdministration(this); function reactiveRender() { if (!admin.reaction) { // Create reaction lazily to support re-mounting #3395 admin.reaction = createReaction(admin); if (!admin.mounted) { // React can abandon this instance and never call `componentDidMount`/`componentWillUnmount`, // we have to make sure reaction will be disposed. mobxReactLite._observerFinalizationRegistry.register(this, admin, this); } } let error = undefined; let renderResult = undefined; admin.reaction.track(() => { try { // TODO@major // Optimization: replace with _allowStateChangesStart/End (not available in mobx@6.0.0) renderResult = mobx._allowStateChanges(false, boundOriginalRender); } catch (e) { error = e; } }); if (error) { throw error; } return renderResult; } return reactiveRender; } function createReaction(admin) { return new mobx.Reaction(`${admin.name}.render()`, () => { if (!admin.mounted) { // This is neccessary to avoid react warning about calling forceUpdate on component that isn't mounted yet. // This happens when component is abandoned after render - our reaction is already created and reacts to changes. // `componenDidMount` runs synchronously after `render`, so unlike functional component, there is no delay during which the reaction could be invalidated. // However `componentDidMount` runs AFTER it's descendants' `componentDidMount`, which CAN invalidate the reaction, see #3730. Therefore remember and forceUpdate on mount. admin.reactionInvalidatedBeforeMount = true; return; } try { admin.forceUpdate == null || admin.forceUpdate(); } catch (error) { var _admin$reaction2; (_admin$reaction2 = admin.reaction) == null || _admin$reaction2.dispose(); admin.reaction = null; } }); } function observerSCU(nextProps, nextState) { if (mobxReactLite.isUsingStaticRendering()) { console.warn("[mobx-react] It seems that a re-rendering of a React component is triggered while in static (server-side) mode. Please make sure components are rendered only once server-side."); } // update on any state changes (as is the default) if (this.state !== nextState) { return true; } // update if props are shallowly not equal, inspired by PureRenderMixin // we could return just 'false' here, and avoid the `skipRender` checks etc // however, it is nicer if lifecycle events are triggered like usually, // so we return true here if props are shallowly modified. return !shallowEqual(this.props, nextProps); } function createObservablePropDescriptor(key) { return { configurable: true, enumerable: true, get() { const admin = getAdministration(this); const derivation = mobx._getGlobalState().trackingDerivation; if (derivation && derivation !== admin.reaction) { throw new Error(`[mobx-react] Cannot read "${admin.name}.${key}" in a reactive context, as it isn't observable. Please use component lifecycle method to copy the value into a local observable first. See https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/README.md#note-on-using-props-and-state-in-derivations`); } return admin[key]; }, set(value) { getAdministration(this)[key] = value; } }; } function observer(component, context) { if (context && context.kind !== "class") { throw new Error("The @observer decorator can be used on classes only"); } if (Object.prototype.isPrototypeOf.call(React__namespace.Component, component) || Object.prototype.isPrototypeOf.call(React__namespace.PureComponent, component)) { // Class component return makeClassComponentObserver(component); } else { // Function component return mobxReactLite.observer(component); } } if (!React.Component) { throw new Error("mobx-react requires React to be available"); } if (!mobx.observable) { throw new Error("mobx-react requires mobx to be available"); } Object.defineProperty(exports, 'Observer', { enumerable: true, get: function () { return mobxReactLite.Observer; } }); Object.defineProperty(exports, '_observerFinalizationRegistry', { enumerable: true, get: function () { return mobxReactLite._observerFinalizationRegistry; } }); Object.defineProperty(exports, 'clearTimers', { enumerable: true, get: function () { return mobxReactLite.clearTimers; } }); Object.defineProperty(exports, 'enableStaticRendering', { enumerable: true, get: function () { return mobxReactLite.enableStaticRendering; } }); Object.defineProperty(exports, 'isUsingStaticRendering', { enumerable: true, get: function () { return mobxReactLite.isUsingStaticRendering; } }); Object.defineProperty(exports, 'useLocalObservable', { enumerable: true, get: function () { return mobxReactLite.useLocalObservable; } }); exports.observer = observer; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=mobxreact.umd.development.js.map