UNPKG

mobx-react

Version:

React bindings for MobX. Create fully reactive components.

742 lines (610 loc) 25.3 kB
import { Reaction, _allowStateChanges, _allowStateReadsStart, _allowStateReadsEnd, $mobx, createAtom, untracked, isObservableMap, isObservableObject, isObservableArray, observable, configure } from 'mobx'; import React__default, { PureComponent, Component, forwardRef, memo, createElement } from 'react'; import { unstable_batchedUpdates } from 'react-dom'; import { isUsingStaticRendering, Observer, observer as observer$1 } from 'mobx-react-lite'; export { Observer, isUsingStaticRendering, useAsObservableSource, useLocalStore, useObserver, useStaticRendering } from 'mobx-react-lite'; let symbolId = 0; function createSymbol(name) { if (typeof Symbol === "function") { return Symbol(name); } const symbol = `__$mobx-react ${name} (${symbolId})`; symbolId++; return symbol; } const createdSymbols = {}; function newSymbol(name) { if (!createdSymbols[name]) { createdSymbols[name] = createSymbol(name); } return createdSymbols[name]; } 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; } } // based on https://github.com/mridgway/hoist-non-react-statics/blob/master/src/index.js const hoistBlackList = { $$typeof: 1, render: 1, compare: 1, type: 1, childContextTypes: 1, contextType: 1, contextTypes: 1, defaultProps: 1, getDefaultProps: 1, getDerivedStateFromError: 1, getDerivedStateFromProps: 1, mixins: 1, propTypes: 1 }; function copyStaticProperties(base, target) { const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(base)); Object.getOwnPropertyNames(base).forEach(key => { if (!hoistBlackList[key] && protoProps.indexOf(key) === -1) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(base, key)); } }); } /** * Helper to set `prop` to `this` as non-enumerable (hidden prop) * @param target * @param prop * @param value */ function setHiddenProp(target, prop, value) { if (!Object.hasOwnProperty.call(target, prop)) { Object.defineProperty(target, prop, { enumerable: false, configurable: true, writable: true, value }); } else { target[prop] = value; } } /** * 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__*/ newSymbol("patchMixins"); const mobxPatchedDefinition = /*#__PURE__*/ newSymbol("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 (...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 { [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 mobxAdminProperty = $mobx || "$mobx"; const mobxIsUnmounted = /*#__PURE__*/ newSymbol("isUnmounted"); const skipRenderKey = /*#__PURE__*/ newSymbol("skipRender"); const isForcingUpdateKey = /*#__PURE__*/ newSymbol("isForcingUpdate"); function makeClassComponentObserver(componentClass) { const target = componentClass.prototype; if (target.componentWillReact) throw new Error("The componentWillReact life-cycle event is no longer supported"); if (componentClass["__proto__"] !== PureComponent) { if (!target.shouldComponentUpdate) target.shouldComponentUpdate = observerSCU;else if (target.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."); } // this.props and this.state are made observable, just to make sure @computed fields that // are defined inside the component, and which rely on state or props, re-compute if state or props change // (otherwise the computed wouldn't update and become stale on props change, since props are not observable) // However, this solution is not without it's own problems: https://github.com/mobxjs/mobx-react/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aobservable-props-or-not+ makeObservableProp(target, "props"); makeObservableProp(target, "state"); const baseRender = target.render; target.render = function () { return makeComponentReactive.call(this, baseRender); }; patch(target, "componentWillUnmount", function () { if (isUsingStaticRendering() === true) return; if (this.render[mobxAdminProperty]) { this.render[mobxAdminProperty].dispose(); } else if (process.env.NODE_ENV !== "production") { const displayName = getDisplayName(this); console.warn(`The render function for an observer component (${displayName}) was modified after MobX attached. This is not supported, since the new function can't be triggered by MobX.`); } this[mobxIsUnmounted] = true; }); return componentClass; } // Generates a friendly name for debugging function getDisplayName(comp) { return comp.displayName || comp.name || comp.constructor && (comp.constructor.displayName || comp.constructor.name) || "<component>"; } function makeComponentReactive(render) { if (isUsingStaticRendering() === true) return render.call(this); /** * If props are shallowly modified, react will render anyway, * so atom.reportChanged() should not result in yet another re-render */ setHiddenProp(this, skipRenderKey, false); /** * forceUpdate will re-assign this.props. We don't want that to cause a loop, * so detect these changes */ setHiddenProp(this, isForcingUpdateKey, false); const initialName = getDisplayName(this); const baseRender = render.bind(this); let isRenderingPending = false; const reaction = new Reaction(`${initialName}.render()`, () => { if (!isRenderingPending) { // N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js) // This unidiomatic React usage but React will correctly warn about this so we continue as usual // See #85 / Pull #44 isRenderingPending = true; if (this[mobxIsUnmounted] !== true) { let hasError = true; try { setHiddenProp(this, isForcingUpdateKey, true); if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this); hasError = false; } finally { setHiddenProp(this, isForcingUpdateKey, false); if (hasError) reaction.dispose(); } } } }); reaction["reactComponent"] = this; reactiveRender[mobxAdminProperty] = reaction; this.render = reactiveRender; function reactiveRender() { isRenderingPending = false; let exception = undefined; let rendering = undefined; reaction.track(() => { try { rendering = _allowStateChanges(false, baseRender); } catch (e) { exception = e; } }); if (exception) { throw exception; } return rendering; } return reactiveRender.call(this); } function observerSCU(nextProps, nextState) { if (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 makeObservableProp(target, propName) { const valueHolderKey = newSymbol(`reactProp_${propName}_valueHolder`); const atomHolderKey = newSymbol(`reactProp_${propName}_atomHolder`); function getAtom() { if (!this[atomHolderKey]) { setHiddenProp(this, atomHolderKey, createAtom("reactive " + propName)); } return this[atomHolderKey]; } Object.defineProperty(target, propName, { configurable: true, enumerable: true, get: function () { let prevReadState = false; if (_allowStateReadsStart && _allowStateReadsEnd) { prevReadState = _allowStateReadsStart(true); } getAtom.call(this).reportObserved(); if (_allowStateReadsStart && _allowStateReadsEnd) { _allowStateReadsEnd(prevReadState); } return this[valueHolderKey]; }, set: function set(v) { if (!this[isForcingUpdateKey] && !shallowEqual(this[valueHolderKey], v)) { setHiddenProp(this, valueHolderKey, v); setHiddenProp(this, skipRenderKey, true); getAtom.call(this).reportChanged(); setHiddenProp(this, skipRenderKey, false); } else { setHiddenProp(this, valueHolderKey, v); } } }); } const hasSymbol = typeof Symbol === "function" && Symbol.for; // Using react-is had some issues (and operates on elements, not on types), see #608 / #609 const ReactForwardRefSymbol = hasSymbol ? /*#__PURE__*/ Symbol.for("react.forward_ref") : typeof forwardRef === "function" && /*#__PURE__*/ forwardRef(props => null)["$$typeof"]; const ReactMemoSymbol = hasSymbol ? /*#__PURE__*/ Symbol.for("react.memo") : typeof memo === "function" && /*#__PURE__*/ memo(props => null)["$$typeof"]; /** * Observer function / decorator */ function observer(component) { if (component["isMobxInjector"] === true) { console.warn("Mobx observer: You are trying to use 'observer' on a component that already has 'inject'. Please apply 'observer' before applying 'inject'"); } if (ReactMemoSymbol && component["$$typeof"] === ReactMemoSymbol) { throw new Error("Mobx observer: You are trying to use 'observer' on function component wrapped to either another observer or 'React.memo'. The observer already applies 'React.memo' for you."); } // Unwrap forward refs into `<Observer>` component // we need to unwrap the render, because it is the inner render that needs to be tracked, // not the ForwardRef HoC if (ReactForwardRefSymbol && component["$$typeof"] === ReactForwardRefSymbol) { const baseRender = component["render"]; if (typeof baseRender !== "function") throw new Error("render property of ForwardRef was not a function"); return forwardRef(function ObserverForwardRef() { return createElement(Observer, null, () => baseRender.apply(undefined, arguments)); }); } // Function component if (typeof component === "function" && (!component.prototype || !component.prototype.render) && !component["isReactClass"] && !Object.prototype.isPrototypeOf.call(Component, component)) { return observer$1(component); } return makeClassComponentObserver(component); } const MobXProviderContext = /*#__PURE__*/ React__default.createContext({}); function Provider(props) { const { children, ...stores } = props; const parentValue = React__default.useContext(MobXProviderContext); const mutableProviderRef = React__default.useRef({ ...parentValue, ...stores }); const value = mutableProviderRef.current; if (process.env.NODE_ENV !== "production") { const newValue = { ...value, ...stores }; // spread in previous state for the context based stores if (!shallowEqual(value, newValue)) { throw new Error("MobX Provider: The set of provided stores has changed. See: https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error."); } } return React__default.createElement(MobXProviderContext.Provider, { value: value }, children); } Provider.displayName = "MobXProvider"; /** * Store Injection */ function createStoreInjector(grabStoresFn, component, injectNames, makeReactive) { // Support forward refs let Injector = React__default.forwardRef((props, ref) => { const newProps = { ...props }; const context = React__default.useContext(MobXProviderContext); Object.assign(newProps, grabStoresFn(context || {}, newProps) || {}); if (ref) { newProps.ref = ref; } return React__default.createElement(component, newProps); }); if (makeReactive) Injector = observer(Injector); Injector["isMobxInjector"] = true; // assigned late to suppress observer warning // Static fields from component should be visible on the generated Injector copyStaticProperties(component, Injector); Injector["wrappedComponent"] = component; Injector.displayName = getInjectName(component, injectNames); return Injector; } function getInjectName(component, injectNames) { let displayName; const componentName = component.displayName || component.name || component.constructor && component.constructor.name || "Component"; if (injectNames) displayName = "inject-with-" + injectNames + "(" + componentName + ")";else displayName = "inject(" + componentName + ")"; return displayName; } function grabStoresByName(storeNames) { return function (baseStores, nextProps) { storeNames.forEach(function (storeName) { if (storeName in nextProps // prefer props over stores ) return; if (!(storeName in baseStores)) throw new Error("MobX injector: Store '" + storeName + "' is not available! Make sure it is provided by some Provider"); nextProps[storeName] = baseStores[storeName]; }); return nextProps; }; } /** * higher order component that injects stores to a child. * takes either a varargs list of strings, which are stores read from the context, * or a function that manually maps the available stores from the context to props: * storesToProps(mobxStores, props, context) => newProps */ function inject( /* fn(stores, nextProps) or ...storeNames */ ...storeNames) { if (typeof arguments[0] === "function") { let grabStoresFn = arguments[0]; return componentClass => createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, true); } else { return componentClass => createStoreInjector(grabStoresByName(storeNames), componentClass, storeNames.join("-"), false); } } const protoStoreKey = /*#__PURE__*/ newSymbol("disposeOnUnmountProto"); const instStoreKey = /*#__PURE__*/ newSymbol("disposeOnUnmountInst"); function runDisposersOnWillUnmount() { [...(this[protoStoreKey] || []), ...(this[instStoreKey] || [])].forEach(propKeyOrFunction => { const prop = typeof propKeyOrFunction === "string" ? this[propKeyOrFunction] : propKeyOrFunction; if (prop !== undefined && prop !== null) { if (Array.isArray(prop)) prop.map(f => f());else prop(); } }); } function disposeOnUnmount(target, propertyKeyOrFunction) { if (Array.isArray(propertyKeyOrFunction)) { return propertyKeyOrFunction.map(fn => disposeOnUnmount(target, fn)); } const c = Object.getPrototypeOf(target).constructor || Object.getPrototypeOf(target.constructor); const c2 = Object.getPrototypeOf(target.constructor); if (!(c === React__default.Component || c === React__default.PureComponent || c2 === React__default.Component || c2 === React__default.PureComponent)) { throw new Error("[mobx-react] disposeOnUnmount only supports direct subclasses of React.Component or React.PureComponent."); } if (typeof propertyKeyOrFunction !== "string" && typeof propertyKeyOrFunction !== "function" && !Array.isArray(propertyKeyOrFunction)) { throw new Error("[mobx-react] disposeOnUnmount only works if the parameter is either a property key or a function."); } // decorator's target is the prototype, so it doesn't have any instance properties like props const isDecorator = typeof propertyKeyOrFunction === "string"; // add property key / function we want run (disposed) to the store const componentWasAlreadyModified = !!target[protoStoreKey] || !!target[instStoreKey]; const store = isDecorator ? // decorators are added to the prototype store target[protoStoreKey] || (target[protoStoreKey] = []) : // functions are added to the instance store target[instStoreKey] || (target[instStoreKey] = []); store.push(propertyKeyOrFunction); // tweak the component class componentWillUnmount if not done already if (!componentWasAlreadyModified) { patch(target, "componentWillUnmount", runDisposersOnWillUnmount); } // return the disposer as is if invoked as a non decorator if (typeof propertyKeyOrFunction !== "string") { return propertyKeyOrFunction; } } function createChainableTypeChecker(validator) { function checkType(isRequired, props, propName, componentName, location, propFullName, ...rest) { return untracked(() => { componentName = componentName || "<<anonymous>>"; propFullName = propFullName || propName; if (props[propName] == null) { if (isRequired) { const actual = props[propName] === null ? "null" : "undefined"; return new Error("The " + location + " `" + propFullName + "` is marked as required " + "in `" + componentName + "`, but its value is `" + actual + "`."); } return null; } else { // @ts-ignore rest arg is necessary for some React internals - fails tests otherwise return validator(props, propName, componentName, location, propFullName, ...rest); } }); } const chainedCheckType = checkType.bind(null, false); // Add isRequired to satisfy Requirable chainedCheckType.isRequired = checkType.bind(null, true); return chainedCheckType; } // Copied from React.PropTypes function isSymbol(propType, propValue) { // Native Symbol. if (propType === "symbol") { return true; } // 19.4.3.5 Symbol.prototype[@@toStringTag] === 'Symbol' if (propValue["@@toStringTag"] === "Symbol") { return true; } // Fallback for non-spec compliant Symbols which are polyfilled. if (typeof Symbol === "function" && propValue instanceof Symbol) { return true; } return false; } // Copied from React.PropTypes function getPropType(propValue) { const propType = typeof propValue; if (Array.isArray(propValue)) { return "array"; } if (propValue instanceof RegExp) { // Old webkits (at least until Android 4.0) return 'function' rather than // 'object' for typeof a RegExp. We'll normalize this here so that /bla/ // passes PropTypes.object. return "object"; } if (isSymbol(propType, propValue)) { return "symbol"; } return propType; } // This handles more types than `getPropType`. Only used for error messages. // Copied from React.PropTypes function getPreciseType(propValue) { const propType = getPropType(propValue); if (propType === "object") { if (propValue instanceof Date) { return "date"; } else if (propValue instanceof RegExp) { return "regexp"; } } return propType; } function createObservableTypeCheckerCreator(allowNativeType, mobxType) { return createChainableTypeChecker((props, propName, componentName, location, propFullName) => { return untracked(() => { if (allowNativeType) { if (getPropType(props[propName]) === mobxType.toLowerCase()) return null; } let mobxChecker; switch (mobxType) { case "Array": mobxChecker = isObservableArray; break; case "Object": mobxChecker = isObservableObject; break; case "Map": mobxChecker = isObservableMap; break; default: throw new Error(`Unexpected mobxType: ${mobxType}`); } const propValue = props[propName]; if (!mobxChecker(propValue)) { const preciseType = getPreciseType(propValue); const nativeTypeExpectationMessage = allowNativeType ? " or javascript `" + mobxType.toLowerCase() + "`" : ""; return new Error("Invalid prop `" + propFullName + "` of type `" + preciseType + "` supplied to" + " `" + componentName + "`, expected `mobx.Observable" + mobxType + "`" + nativeTypeExpectationMessage + "."); } return null; }); }); } function createObservableArrayOfTypeChecker(allowNativeType, typeChecker) { return createChainableTypeChecker((props, propName, componentName, location, propFullName, ...rest) => { return untracked(() => { if (typeof typeChecker !== "function") { return new Error("Property `" + propFullName + "` of component `" + componentName + "` has " + "invalid PropType notation."); } else { let error = createObservableTypeCheckerCreator(allowNativeType, "Array")(props, propName, componentName, location, propFullName); if (error instanceof Error) return error; const propValue = props[propName]; for (let i = 0; i < propValue.length; i++) { error = typeChecker(propValue, i, componentName, location, propFullName + "[" + i + "]", ...rest); if (error instanceof Error) return error; } return null; } }); }); } const observableArray = /*#__PURE__*/ createObservableTypeCheckerCreator(false, "Array"); const observableArrayOf = /*#__PURE__*/ createObservableArrayOfTypeChecker.bind(null, false); const observableMap = /*#__PURE__*/ createObservableTypeCheckerCreator(false, "Map"); const observableObject = /*#__PURE__*/ createObservableTypeCheckerCreator(false, "Object"); const arrayOrObservableArray = /*#__PURE__*/ createObservableTypeCheckerCreator(true, "Array"); const arrayOrObservableArrayOf = /*#__PURE__*/ createObservableArrayOfTypeChecker.bind(null, true); const objectOrObservableObject = /*#__PURE__*/ createObservableTypeCheckerCreator(true, "Object"); const PropTypes = { observableArray, observableArrayOf, observableMap, observableObject, arrayOrObservableArray, arrayOrObservableArrayOf, objectOrObservableObject }; if (!Component) throw new Error("mobx-react requires React to be available"); if (!observable) throw new Error("mobx-react requires mobx to be available"); if (typeof unstable_batchedUpdates === "function") configure({ reactionScheduler: unstable_batchedUpdates }); export { MobXProviderContext, PropTypes, Provider, disposeOnUnmount, inject, observer }; //# sourceMappingURL=mobxreact.esm.js.map