UNPKG

4.56 kBPlain TextView Raw
1import { Reaction } from "mobx"
2import React from "react"
3import { printDebugValue } from "./utils/printDebugValue"
4import {
5 addReactionToTrack,
6 IReactionTracking,
7 recordReactionAsCommitted
8} from "./utils/reactionCleanupTracking"
9import { isUsingStaticRendering } from "./staticRendering"
10
11function observerComponentNameFor(baseComponentName: string) {
12 return `observer${baseComponentName}`
13}
14
15/**
16 * We use class to make it easier to detect in heap snapshots by name
17 */
18class ObjectToBeRetainedByReact {}
19
20function objectToBeRetainedByReactFactory() {
21 return new ObjectToBeRetainedByReact()
22}
23
24export function useObserver<T>(fn: () => T, baseComponentName: string = "observed"): T {
25 if (isUsingStaticRendering()) {
26 return fn()
27 }
28
29 const [objectRetainedByReact] = React.useState(objectToBeRetainedByReactFactory)
30 // Force update, see #2982
31 const [, setState] = React.useState()
32 const forceUpdate = () => setState([] as any)
33
34 // StrictMode/ConcurrentMode/Suspense may mean that our component is
35 // rendered and abandoned multiple times, so we need to track leaked
36 // Reactions.
37 const reactionTrackingRef = React.useRef<IReactionTracking | null>(null)
38
39 if (!reactionTrackingRef.current) {
40 // First render for this component (or first time since a previous
41 // reaction from an abandoned render was disposed).
42
43 const newReaction = new Reaction(observerComponentNameFor(baseComponentName), () => {
44 // Observable has changed, meaning we want to re-render
45 // BUT if we're a component that hasn't yet got to the useEffect()
46 // stage, we might be a component that _started_ to render, but
47 // got dropped, and we don't want to make state changes then.
48 // (It triggers warnings in StrictMode, for a start.)
49 if (trackingData.mounted) {
50 // We have reached useEffect(), so we're mounted, and can trigger an update
51 forceUpdate()
52 } else {
53 // We haven't yet reached useEffect(), so we'll need to trigger a re-render
54 // when (and if) useEffect() arrives.
55 trackingData.changedBeforeMount = true
56 }
57 })
58
59 const trackingData = addReactionToTrack(
60 reactionTrackingRef,
61 newReaction,
62 objectRetainedByReact
63 )
64 }
65
66 const { reaction } = reactionTrackingRef.current!
67 React.useDebugValue(reaction, printDebugValue)
68
69 React.useEffect(() => {
70 // Called on first mount only
71 recordReactionAsCommitted(reactionTrackingRef)
72
73 if (reactionTrackingRef.current) {
74 // Great. We've already got our reaction from our render;
75 // all we need to do is to record that it's now mounted,
76 // to allow future observable changes to trigger re-renders
77 reactionTrackingRef.current.mounted = true
78 // Got a change before first mount, force an update
79 if (reactionTrackingRef.current.changedBeforeMount) {
80 reactionTrackingRef.current.changedBeforeMount = false
81 forceUpdate()
82 }
83 } else {
84 // The reaction we set up in our render has been disposed.
85 // This can be due to bad timings of renderings, e.g. our
86 // component was paused for a _very_ long time, and our
87 // reaction got cleaned up
88
89 // Re-create the reaction
90 reactionTrackingRef.current = {
91 reaction: new Reaction(observerComponentNameFor(baseComponentName), () => {
92 // We've definitely already been mounted at this point
93 forceUpdate()
94 }),
95 mounted: true,
96 changedBeforeMount: false,
97 cleanAt: Infinity
98 }
99 forceUpdate()
100 }
101
102 return () => {
103 reactionTrackingRef.current!.reaction.dispose()
104 reactionTrackingRef.current = null
105 }
106 }, [])
107
108 // render the original component, but have the
109 // reaction track the observables, so that rendering
110 // can be invalidated (see above) once a dependency changes
111 let rendering!: T
112 let exception
113 reaction.track(() => {
114 try {
115 rendering = fn()
116 } catch (e) {
117 exception = e
118 }
119 })
120
121 if (exception) {
122 throw exception // re-throw any exceptions caught during rendering
123 }
124
125 return rendering
126}