UNPKG

19.9 kBJavaScriptView Raw
1import { makeObservable, configure, getDependencyTree, Reaction, observable, runInAction } from 'mobx';
2import React, { useState, forwardRef, memo } from 'react';
3import { unstable_batchedUpdates } from 'react-dom';
4
5if (!useState) {
6 throw new Error("mobx-react-lite requires React with Hooks support");
7}
8
9if (!makeObservable) {
10 throw new Error("mobx-react-lite@3 requires mobx at least version 6 to be available");
11}
12
13function defaultNoopBatch(callback) {
14 callback();
15}
16function observerBatching(reactionScheduler) {
17 if (!reactionScheduler) {
18 reactionScheduler = defaultNoopBatch;
19
20 {
21 console.warn("[MobX] Failed to get unstable_batched updates from react-dom / react-native");
22 }
23 }
24
25 configure({
26 reactionScheduler: reactionScheduler
27 });
28}
29var isObserverBatched = function isObserverBatched() {
30 {
31 console.warn("[MobX] Deprecated");
32 }
33
34 return true;
35};
36
37var deprecatedMessages = [];
38function useDeprecated(msg) {
39 if (!deprecatedMessages.includes(msg)) {
40 deprecatedMessages.push(msg);
41 console.warn(msg);
42 }
43}
44
45function printDebugValue(v) {
46 return getDependencyTree(v);
47}
48
49var FinalizationRegistryLocal = typeof FinalizationRegistry === "undefined" ? undefined : FinalizationRegistry;
50
51function createTrackingData(reaction) {
52 var trackingData = {
53 reaction: reaction,
54 mounted: false,
55 changedBeforeMount: false,
56 cleanAt: Date.now() + CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS
57 };
58 return trackingData;
59}
60/**
61 * The minimum time before we'll clean up a Reaction created in a render
62 * for a component that hasn't managed to run its effects. This needs to
63 * be big enough to ensure that a component won't turn up and have its
64 * effects run without being re-rendered.
65 */
66
67var CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS = 10000;
68/**
69 * The frequency with which we'll check for leaked reactions.
70 */
71
72var CLEANUP_TIMER_LOOP_MILLIS = 10000;
73
74/**
75 * FinalizationRegistry-based uncommitted reaction cleanup
76 */
77
78function createReactionCleanupTrackingUsingFinalizationRegister(FinalizationRegistry) {
79 var cleanupTokenToReactionTrackingMap = new Map();
80 var globalCleanupTokensCounter = 1;
81 var registry = new FinalizationRegistry(function cleanupFunction(token) {
82 var trackedReaction = cleanupTokenToReactionTrackingMap.get(token);
83
84 if (trackedReaction) {
85 trackedReaction.reaction.dispose();
86 cleanupTokenToReactionTrackingMap["delete"](token);
87 }
88 });
89 return {
90 addReactionToTrack: function addReactionToTrack(reactionTrackingRef, reaction, objectRetainedByReact) {
91 var token = globalCleanupTokensCounter++;
92 registry.register(objectRetainedByReact, token, reactionTrackingRef);
93 reactionTrackingRef.current = createTrackingData(reaction);
94 reactionTrackingRef.current.finalizationRegistryCleanupToken = token;
95 cleanupTokenToReactionTrackingMap.set(token, reactionTrackingRef.current);
96 return reactionTrackingRef.current;
97 },
98 recordReactionAsCommitted: function recordReactionAsCommitted(reactionRef) {
99 registry.unregister(reactionRef);
100
101 if (reactionRef.current && reactionRef.current.finalizationRegistryCleanupToken) {
102 cleanupTokenToReactionTrackingMap["delete"](reactionRef.current.finalizationRegistryCleanupToken);
103 }
104 },
105 forceCleanupTimerToRunNowForTests: function forceCleanupTimerToRunNowForTests() {// When FinalizationRegistry in use, this this is no-op
106 },
107 resetCleanupScheduleForTests: function resetCleanupScheduleForTests() {// When FinalizationRegistry in use, this this is no-op
108 }
109 };
110}
111
112function _unsupportedIterableToArray(o, minLen) {
113 if (!o) return;
114 if (typeof o === "string") return _arrayLikeToArray(o, minLen);
115 var n = Object.prototype.toString.call(o).slice(8, -1);
116 if (n === "Object" && o.constructor) n = o.constructor.name;
117 if (n === "Map" || n === "Set") return Array.from(o);
118 if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
119}
120
121function _arrayLikeToArray(arr, len) {
122 if (len == null || len > arr.length) len = arr.length;
123
124 for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
125
126 return arr2;
127}
128
129function _createForOfIteratorHelperLoose(o, allowArrayLike) {
130 var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
131 if (it) return (it = it.call(o)).next.bind(it);
132
133 if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
134 if (it) o = it;
135 var i = 0;
136 return function () {
137 if (i >= o.length) return {
138 done: true
139 };
140 return {
141 done: false,
142 value: o[i++]
143 };
144 };
145 }
146
147 throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
148}
149
150/**
151 * timers, gc-style, uncommitted reaction cleanup
152 */
153
154function createTimerBasedReactionCleanupTracking() {
155 /**
156 * Reactions created by components that have yet to be fully mounted.
157 */
158 var uncommittedReactionRefs = new Set();
159 /**
160 * Latest 'uncommitted reactions' cleanup timer handle.
161 */
162
163 var reactionCleanupHandle;
164 /* istanbul ignore next */
165
166 /**
167 * Only to be used by test functions; do not export outside of mobx-react-lite
168 */
169
170 function forceCleanupTimerToRunNowForTests() {
171 // This allows us to control the execution of the cleanup timer
172 // to force it to run at awkward times in unit tests.
173 if (reactionCleanupHandle) {
174 clearTimeout(reactionCleanupHandle);
175 cleanUncommittedReactions();
176 }
177 }
178 /* istanbul ignore next */
179
180
181 function resetCleanupScheduleForTests() {
182 if (uncommittedReactionRefs.size > 0) {
183 for (var _iterator = _createForOfIteratorHelperLoose(uncommittedReactionRefs), _step; !(_step = _iterator()).done;) {
184 var ref = _step.value;
185 var tracking = ref.current;
186
187 if (tracking) {
188 tracking.reaction.dispose();
189 ref.current = null;
190 }
191 }
192
193 uncommittedReactionRefs.clear();
194 }
195
196 if (reactionCleanupHandle) {
197 clearTimeout(reactionCleanupHandle);
198 reactionCleanupHandle = undefined;
199 }
200 }
201
202 function ensureCleanupTimerRunning() {
203 if (reactionCleanupHandle === undefined) {
204 reactionCleanupHandle = setTimeout(cleanUncommittedReactions, CLEANUP_TIMER_LOOP_MILLIS);
205 }
206 }
207
208 function scheduleCleanupOfReactionIfLeaked(ref) {
209 uncommittedReactionRefs.add(ref);
210 ensureCleanupTimerRunning();
211 }
212
213 function recordReactionAsCommitted(reactionRef) {
214 uncommittedReactionRefs["delete"](reactionRef);
215 }
216 /**
217 * Run by the cleanup timer to dispose any outstanding reactions
218 */
219
220
221 function cleanUncommittedReactions() {
222 reactionCleanupHandle = undefined; // Loop through all the candidate leaked reactions; those older
223 // than CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS get tidied.
224
225 var now = Date.now();
226 uncommittedReactionRefs.forEach(function (ref) {
227 var tracking = ref.current;
228
229 if (tracking) {
230 if (now >= tracking.cleanAt) {
231 // It's time to tidy up this leaked reaction.
232 tracking.reaction.dispose();
233 ref.current = null;
234 uncommittedReactionRefs["delete"](ref);
235 }
236 }
237 });
238
239 if (uncommittedReactionRefs.size > 0) {
240 // We've just finished a round of cleanups but there are still
241 // some leak candidates outstanding.
242 ensureCleanupTimerRunning();
243 }
244 }
245
246 return {
247 addReactionToTrack: function addReactionToTrack(reactionTrackingRef, reaction,
248 /**
249 * On timer based implementation we don't really need this object,
250 * but we keep the same api
251 */
252 objectRetainedByReact) {
253 reactionTrackingRef.current = createTrackingData(reaction);
254 scheduleCleanupOfReactionIfLeaked(reactionTrackingRef);
255 return reactionTrackingRef.current;
256 },
257 recordReactionAsCommitted: recordReactionAsCommitted,
258 forceCleanupTimerToRunNowForTests: forceCleanupTimerToRunNowForTests,
259 resetCleanupScheduleForTests: resetCleanupScheduleForTests
260 };
261}
262
263var _ref = FinalizationRegistryLocal ? /*#__PURE__*/createReactionCleanupTrackingUsingFinalizationRegister(FinalizationRegistryLocal) : /*#__PURE__*/createTimerBasedReactionCleanupTracking(),
264 addReactionToTrack = _ref.addReactionToTrack,
265 recordReactionAsCommitted = _ref.recordReactionAsCommitted,
266 resetCleanupScheduleForTests = _ref.resetCleanupScheduleForTests;
267
268var globalIsUsingStaticRendering = false;
269function enableStaticRendering(enable) {
270 globalIsUsingStaticRendering = enable;
271}
272function isUsingStaticRendering() {
273 return globalIsUsingStaticRendering;
274}
275
276function observerComponentNameFor(baseComponentName) {
277 return "observer" + baseComponentName;
278}
279/**
280 * We use class to make it easier to detect in heap snapshots by name
281 */
282
283
284var ObjectToBeRetainedByReact = function ObjectToBeRetainedByReact() {};
285
286function objectToBeRetainedByReactFactory() {
287 return new ObjectToBeRetainedByReact();
288}
289
290function useObserver(fn, baseComponentName) {
291 if (baseComponentName === void 0) {
292 baseComponentName = "observed";
293 }
294
295 if (isUsingStaticRendering()) {
296 return fn();
297 }
298
299 var _React$useState = React.useState(objectToBeRetainedByReactFactory),
300 objectRetainedByReact = _React$useState[0]; // Force update, see #2982
301
302
303 var _React$useState2 = React.useState(),
304 setState = _React$useState2[1];
305
306 var forceUpdate = function forceUpdate() {
307 return setState([]);
308 }; // StrictMode/ConcurrentMode/Suspense may mean that our component is
309 // rendered and abandoned multiple times, so we need to track leaked
310 // Reactions.
311
312
313 var reactionTrackingRef = React.useRef(null);
314
315 if (!reactionTrackingRef.current) {
316 // First render for this component (or first time since a previous
317 // reaction from an abandoned render was disposed).
318 var newReaction = new Reaction(observerComponentNameFor(baseComponentName), function () {
319 // Observable has changed, meaning we want to re-render
320 // BUT if we're a component that hasn't yet got to the useEffect()
321 // stage, we might be a component that _started_ to render, but
322 // got dropped, and we don't want to make state changes then.
323 // (It triggers warnings in StrictMode, for a start.)
324 if (trackingData.mounted) {
325 // We have reached useEffect(), so we're mounted, and can trigger an update
326 forceUpdate();
327 } else {
328 // We haven't yet reached useEffect(), so we'll need to trigger a re-render
329 // when (and if) useEffect() arrives.
330 trackingData.changedBeforeMount = true;
331 }
332 });
333 var trackingData = addReactionToTrack(reactionTrackingRef, newReaction, objectRetainedByReact);
334 }
335
336 var reaction = reactionTrackingRef.current.reaction;
337 React.useDebugValue(reaction, printDebugValue);
338 React.useEffect(function () {
339 // Called on first mount only
340 recordReactionAsCommitted(reactionTrackingRef);
341
342 if (reactionTrackingRef.current) {
343 // Great. We've already got our reaction from our render;
344 // all we need to do is to record that it's now mounted,
345 // to allow future observable changes to trigger re-renders
346 reactionTrackingRef.current.mounted = true; // Got a change before first mount, force an update
347
348 if (reactionTrackingRef.current.changedBeforeMount) {
349 reactionTrackingRef.current.changedBeforeMount = false;
350 forceUpdate();
351 }
352 } else {
353 // The reaction we set up in our render has been disposed.
354 // This can be due to bad timings of renderings, e.g. our
355 // component was paused for a _very_ long time, and our
356 // reaction got cleaned up
357 // Re-create the reaction
358 reactionTrackingRef.current = {
359 reaction: new Reaction(observerComponentNameFor(baseComponentName), function () {
360 // We've definitely already been mounted at this point
361 forceUpdate();
362 }),
363 mounted: true,
364 changedBeforeMount: false,
365 cleanAt: Infinity
366 };
367 forceUpdate();
368 }
369
370 return function () {
371 reactionTrackingRef.current.reaction.dispose();
372 reactionTrackingRef.current = null;
373 };
374 }, []); // render the original component, but have the
375 // reaction track the observables, so that rendering
376 // can be invalidated (see above) once a dependency changes
377
378 var rendering;
379 var exception;
380 reaction.track(function () {
381 try {
382 rendering = fn();
383 } catch (e) {
384 exception = e;
385 }
386 });
387
388 if (exception) {
389 throw exception; // re-throw any exceptions caught during rendering
390 }
391
392 return rendering;
393}
394
395var warnObserverOptionsDeprecated = true;
396var hasSymbol = typeof Symbol === "function" && Symbol["for"]; // Using react-is had some issues (and operates on elements, not on types), see #608 / #609
397
398var ReactForwardRefSymbol = hasSymbol ? /*#__PURE__*/Symbol["for"]("react.forward_ref") : typeof forwardRef === "function" && /*#__PURE__*/forwardRef(function (props) {
399 return null;
400})["$$typeof"];
401var ReactMemoSymbol = hasSymbol ? /*#__PURE__*/Symbol["for"]("react.memo") : typeof memo === "function" && /*#__PURE__*/memo(function (props) {
402 return null;
403})["$$typeof"]; // n.b. base case is not used for actual typings or exported in the typing files
404
405function observer(baseComponent, // TODO remove in next major
406options) {
407 var _options$forwardRef;
408
409 if ( warnObserverOptionsDeprecated && options) {
410 warnObserverOptionsDeprecated = false;
411 console.warn("[mobx-react-lite] `observer(fn, { forwardRef: true })` is deprecated, use `observer(React.forwardRef(fn))`");
412 }
413
414 if (ReactMemoSymbol && baseComponent["$$typeof"] === ReactMemoSymbol) {
415 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.");
416 } // The working of observer is explained step by step in this talk: https://www.youtube.com/watch?v=cPF4iBedoF0&feature=youtu.be&t=1307
417
418
419 if (isUsingStaticRendering()) {
420 return baseComponent;
421 }
422
423 var useForwardRef = (_options$forwardRef = options == null ? void 0 : options.forwardRef) != null ? _options$forwardRef : false;
424 var render = baseComponent;
425 var baseComponentName = baseComponent.displayName || baseComponent.name; // If already wrapped with forwardRef, unwrap,
426 // so we can patch render and apply memo
427
428 if (ReactForwardRefSymbol && baseComponent["$$typeof"] === ReactForwardRefSymbol) {
429 useForwardRef = true;
430 render = baseComponent["render"];
431
432 if (typeof render !== "function") {
433 throw new Error("[mobx-react-lite] `render` property of ForwardRef was not a function");
434 }
435 }
436
437 var observerComponent = function observerComponent(props, ref) {
438 return useObserver(function () {
439 return render(props, ref);
440 }, baseComponentName);
441 }; // Don't set `displayName` for anonymous components,
442 // so the `displayName` can be customized by user, see #3192.
443
444
445 if (baseComponentName !== "") {
446 observerComponent.displayName = baseComponentName;
447 } // Support legacy context: `contextTypes` must be applied before `memo`
448
449
450 if (baseComponent.contextTypes) {
451 observerComponent.contextTypes = baseComponent.contextTypes;
452 }
453
454 if (useForwardRef) {
455 // `forwardRef` must be applied prior `memo`
456 // `forwardRef(observer(cmp))` throws:
457 // "forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))"
458 observerComponent = forwardRef(observerComponent);
459 } // memo; we are not interested in deep updates
460 // in props; we assume that if deep objects are changed,
461 // this is in observables, which would have been tracked anyway
462
463
464 observerComponent = memo(observerComponent);
465 copyStaticProperties(baseComponent, observerComponent);
466
467 {
468 Object.defineProperty(observerComponent, "contextTypes", {
469 set: function set() {
470 var _this$type;
471
472 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`.");
473 }
474 });
475 }
476
477 return observerComponent;
478} // based on https://github.com/mridgway/hoist-non-react-statics/blob/master/src/index.js
479
480var hoistBlackList = {
481 $$typeof: true,
482 render: true,
483 compare: true,
484 type: true,
485 // Don't redefine `displayName`,
486 // it's defined as getter-setter pair on `memo` (see #3192).
487 displayName: true
488};
489
490function copyStaticProperties(base, target) {
491 Object.keys(base).forEach(function (key) {
492 if (!hoistBlackList[key]) {
493 Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(base, key));
494 }
495 });
496}
497
498function ObserverComponent(_ref) {
499 var children = _ref.children,
500 render = _ref.render;
501 var component = children || render;
502
503 if (typeof component !== "function") {
504 return null;
505 }
506
507 return useObserver(component);
508}
509
510{
511 ObserverComponent.propTypes = {
512 children: ObserverPropsCheck,
513 render: ObserverPropsCheck
514 };
515}
516
517ObserverComponent.displayName = "Observer";
518
519function ObserverPropsCheck(props, key, componentName, location, propFullName) {
520 var extraKey = key === "children" ? "render" : "children";
521 var hasProp = typeof props[key] === "function";
522 var hasExtraProp = typeof props[extraKey] === "function";
523
524 if (hasProp && hasExtraProp) {
525 return new Error("MobX Observer: Do not use children and render in the same time in`" + componentName);
526 }
527
528 if (hasProp || hasExtraProp) {
529 return null;
530 }
531
532 return new Error("Invalid prop `" + propFullName + "` of type `" + typeof props[key] + "` supplied to" + " `" + componentName + "`, expected `function`.");
533}
534
535function useLocalObservable(initializer, annotations) {
536 return useState(function () {
537 return observable(initializer(), annotations, {
538 autoBind: true
539 });
540 })[0];
541}
542
543function useAsObservableSource(current) {
544 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.");
545
546 var _useState = useState(function () {
547 return observable(current, {}, {
548 deep: false
549 });
550 }),
551 res = _useState[0];
552
553 runInAction(function () {
554 Object.assign(res, current);
555 });
556 return res;
557}
558
559function useLocalStore(initializer, current) {
560 useDeprecated("[mobx-react-lite] 'useLocalStore' is deprecated, use 'useLocalObservable' instead.");
561 var source = current && useAsObservableSource(current);
562 return useState(function () {
563 return observable(initializer(source), undefined, {
564 autoBind: true
565 });
566 })[0];
567}
568
569observerBatching(unstable_batchedUpdates);
570function useObserver$1(fn, baseComponentName) {
571 if (baseComponentName === void 0) {
572 baseComponentName = "observed";
573 }
574
575 {
576 useDeprecated("[mobx-react-lite] 'useObserver(fn)' is deprecated. Use `<Observer>{fn}</Observer>` instead, or wrap the entire component in `observer`.");
577 }
578
579 return useObserver(fn, baseComponentName);
580}
581function useStaticRendering(enable) {
582 {
583 console.warn("[mobx-react-lite] 'useStaticRendering' is deprecated, use 'enableStaticRendering' instead");
584 }
585
586 enableStaticRendering(enable);
587}
588
589export { ObserverComponent as Observer, resetCleanupScheduleForTests as clearTimers, enableStaticRendering, isObserverBatched, isUsingStaticRendering, observer, observerBatching, useAsObservableSource, useLocalObservable, useLocalStore, useObserver$1 as useObserver, useStaticRendering };
590//# sourceMappingURL=mobxreactlite.esm.development.js.map