mobx-react-lite
Version:
Lightweight React bindings for MobX based on function components and Hooks
268 lines (253 loc) • 11.7 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('mobx'), require('react')) :
typeof define === 'function' && define.amd ? define(['exports', 'mobx', 'react'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mobxReactLite = {}, global.mobx, global.React));
})(this, (function (exports, mobx, React) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var _getGlobalState2;
if (!React.useState || !React.useSyncExternalStore) {
throw new Error("mobx-react-lite requires React 18 or later");
}
if (!((mobx._getGlobalState == null || (_getGlobalState2 = mobx._getGlobalState()) == null ? void 0 : _getGlobalState2.version) >= 7)) {
throw new Error("mobx-react-lite requires mobx at least version 7 to be available");
}
const REGISTRY_FINALIZE_AFTER = 10000;
const REGISTRY_SWEEP_INTERVAL = 10000;
class TimerBasedFinalizationRegistry {
constructor(finalize) {
this.finalize = void 0;
this.registrations = new Map();
this.sweepTimeout = void 0;
// Bound so it can be used directly as setTimeout callback.
this.sweep = (maxAge = REGISTRY_FINALIZE_AFTER) => {
// cancel timeout so we can force sweep anytime
clearTimeout(this.sweepTimeout);
this.sweepTimeout = undefined;
const now = Date.now();
this.registrations.forEach((registration, token) => {
if (now - registration.registeredAt >= maxAge) {
this.finalize(registration.value);
this.registrations.delete(token);
}
});
if (this.registrations.size > 0) {
this.scheduleSweep();
}
};
// Bound so it can be exported directly as clearTimers test utility.
this.finalizeAllImmediately = () => {
this.sweep(0);
};
this.finalize = finalize;
}
// Token is actually required with this impl
register(target, value, token) {
this.registrations.set(token, {
value,
registeredAt: Date.now()
});
this.scheduleSweep();
}
unregister(token) {
this.registrations.delete(token);
}
scheduleSweep() {
if (this.sweepTimeout === undefined) {
this.sweepTimeout = setTimeout(this.sweep, REGISTRY_SWEEP_INTERVAL);
}
}
}
const UniversalFinalizationRegistry = typeof FinalizationRegistry !== "undefined" ? FinalizationRegistry : TimerBasedFinalizationRegistry;
const observerFinalizationRegistry = /*#__PURE__*/new UniversalFinalizationRegistry(adm => {
var _adm$reaction;
(_adm$reaction = adm.reaction) == null || _adm$reaction.dispose();
adm.reaction = null;
});
let globalIsUsingStaticRendering = false;
function enableStaticRendering(enable) {
globalIsUsingStaticRendering = enable;
}
function isUsingStaticRendering() {
return globalIsUsingStaticRendering;
}
function createReaction(adm) {
adm.reaction = new mobx.Reaction(`observer${adm.name}`, () => {
adm.stateVersion = Symbol();
// onStoreChange won't be available until the component "mounts".
// If state changes in between initial render and mount,
// `useSyncExternalStore` should handle that by checking the state version and issuing update.
adm.onStoreChange == null || adm.onStoreChange();
});
}
function useObserver(render, baseComponentName = "observed") {
if (isUsingStaticRendering()) {
return render();
}
const admRef = React__default["default"].useRef(null);
if (!admRef.current) {
// First render
const _adm = {
reaction: null,
onStoreChange: null,
stateVersion: Symbol(),
name: baseComponentName,
subscribe(onStoreChange) {
// Do NOT access admRef here!
observerFinalizationRegistry.unregister(_adm);
_adm.onStoreChange = onStoreChange;
if (!_adm.reaction) {
// We've lost our reaction and therefore all subscriptions, occurs when:
// 1. Timer based finalization registry disposed reaction before component mounted.
// 2. React "re-mounts" same component without calling render in between (typically <StrictMode>).
// We have to recreate reaction and schedule re-render to recreate subscriptions,
// even if state did not change.
createReaction(_adm);
// `onStoreChange` won't force update if subsequent `getSnapshot` returns same value.
// So we make sure that is not the case
_adm.stateVersion = Symbol();
}
return () => {
var _adm$reaction;
// Do NOT access admRef here!
_adm.onStoreChange = null;
(_adm$reaction = _adm.reaction) == null || _adm$reaction.dispose();
_adm.reaction = null;
};
},
getSnapshot() {
// Do NOT access admRef here!
return _adm.stateVersion;
}
};
admRef.current = _adm;
}
const adm = admRef.current;
if (!adm.reaction) {
// First render or reaction was disposed by registry before subscribe
createReaction(adm);
// StrictMode/ConcurrentMode/Suspense may mean that our component is
// rendered and abandoned multiple times, so we need to track leaked
// Reactions.
observerFinalizationRegistry.register(admRef, adm, adm);
}
React__default["default"].useDebugValue(adm.reaction, mobx.getDependencyTree);
React__default["default"].useSyncExternalStore(
// Both of these must be stable, otherwise it would keep resubscribing every render.
adm.subscribe, adm.getSnapshot, adm.getSnapshot);
// render the original component, but have the
// reaction track the observables, so that rendering
// can be invalidated (see above) once a dependency changes
let renderResult;
let exception;
adm.reaction.track(() => {
try {
renderResult = render();
} catch (e) {
exception = e;
}
});
if (exception) {
throw exception; // re-throw any exceptions caught during rendering
}
return renderResult;
}
var _Object$getOwnPropert, _Object$getOwnPropert2;
const hasSymbol = typeof Symbol === "function" && Symbol.for;
const isFunctionNameConfigurable = (_Object$getOwnPropert = (_Object$getOwnPropert2 = /*#__PURE__*/Object.getOwnPropertyDescriptor(() => {}, "name")) == null ? void 0 : _Object$getOwnPropert2.configurable) != null ? _Object$getOwnPropert : false;
// 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 React.forwardRef === "function" && React.forwardRef(props => null)["$$typeof"];
const ReactMemoSymbol = hasSymbol ? /*#__PURE__*/Symbol.for("react.memo") : typeof React.memo === "function" && React.memo(props => null)["$$typeof"];
// n.b. base case is not used for actual typings or exported in the typing files
function observer(baseComponent) {
if (ReactMemoSymbol && baseComponent["$$typeof"] === ReactMemoSymbol) {
throw new Error(`[mobx-react-lite] You are trying to use \`observer\` on a function component wrapped in either another \`observer\` or \`React.memo\`. The observer already applies 'React.memo' for you.`);
}
// 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;
}
let useForwardRef = false;
let render = baseComponent;
const baseComponentName = baseComponent.displayName || baseComponent.name;
// If already wrapped with forwardRef, unwrap,
// so we can patch render and apply memo
if (ReactForwardRefSymbol && baseComponent["$$typeof"] === ReactForwardRefSymbol) {
useForwardRef = true;
render = baseComponent["render"];
if (typeof render !== "function") {
throw new Error(`[mobx-react-lite] \`render\` property of ForwardRef was not a function`);
}
}
let observerComponent = (props, ref) => {
return useObserver(() => render(props, ref), baseComponentName);
};
observerComponent.displayName = baseComponent.displayName;
if (isFunctionNameConfigurable) {
Object.defineProperty(observerComponent, "name", {
value: baseComponent.name,
writable: true,
configurable: true
});
}
if (useForwardRef) {
// `forwardRef` must be applied prior `memo`
// `forwardRef(observer(cmp))` throws:
// "forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))"
observerComponent = React.forwardRef(observerComponent);
}
// 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
observerComponent = React.memo(observerComponent);
copyStaticProperties(baseComponent, observerComponent);
return observerComponent;
}
// based on https://github.com/mridgway/hoist-non-react-statics/blob/master/src/index.js
const 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(key => {
if (!hoistBlackList[key]) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(base, key));
}
});
}
function ObserverComponent({
children,
render
}) {
if (children && render) {
console.error("MobX Observer: Do not use children and render in the same time in `Observer`");
}
const component = children || render;
if (typeof component !== "function") {
return null;
}
return useObserver(component);
}
ObserverComponent.displayName = "Observer";
function useLocalObservable(initializer, annotations) {
return React.useState(() => mobx.observable(initializer(), annotations, {
autoBind: true
}))[0];
}
var _observerFinalization;
const clearTimers = (_observerFinalization = observerFinalizationRegistry["finalizeAllImmediately"]) != null ? _observerFinalization : () => {};
exports.Observer = ObserverComponent;
exports._observerFinalizationRegistry = observerFinalizationRegistry;
exports.clearTimers = clearTimers;
exports.enableStaticRendering = enableStaticRendering;
exports.isUsingStaticRendering = isUsingStaticRendering;
exports.observer = observer;
exports.useLocalObservable = useLocalObservable;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=mobxreactlite.umd.development.js.map