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