mobx-react
Version:
React bindings for MobX. Create fully reactive components.
387 lines (377 loc) • 18 kB
JavaScript
(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