This repository has been archived by the owner on Dec 31, 2020. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 90
/
Copy pathuseObserver.ts
119 lines (102 loc) · 4.31 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
import { Reaction } from "mobx"
import React from "react"
import { printDebugValue } from "./printDebugValue"
import {
createTrackingData,
IReactionTracking,
recordReactionAsCommitted,
scheduleCleanupOfReactionIfLeaked
} from "./reactionCleanupTracking"
import { isUsingStaticRendering } from "./staticRendering"
import { useForceUpdate } from "./utils"
export type ForceUpdateHook = () => () => void
export interface IUseObserverOptions {
useForceUpdate?: ForceUpdateHook
}
const EMPTY_OBJECT = {}
function observerComponentNameFor(baseComponentName: string) {
return `observer${baseComponentName}`
}
export function useObserver<T>(
fn: () => T,
baseComponentName: string = "observed",
options: IUseObserverOptions = EMPTY_OBJECT
): T {
if (isUsingStaticRendering()) {
return fn()
}
const wantedForceUpdateHook = options.useForceUpdate || useForceUpdate
const forceUpdate = wantedForceUpdateHook()
// 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. The easiest way to do that is just to
// drop our current reaction and allow useEffect() to recreate it.
newReaction.dispose()
reactionTrackingRef.current = null
}
})
const trackingData = createTrackingData(newReaction)
reactionTrackingRef.current = trackingData
scheduleCleanupOfReactionIfLeaked(reactionTrackingRef)
}
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
} else {
// The reaction we set up in our render has been disposed.
// This is either due to bad timings of renderings, e.g. our
// component was paused for a _very_ long time, and our
// reaction got cleaned up, or we got a observable change
// between render and useEffect
// Re-create the reaction
reactionTrackingRef.current = {
reaction: new Reaction(observerComponentNameFor(baseComponentName), () => {
// We've definitely already been mounted at this point
forceUpdate()
}),
cleanAt: Infinity
}
forceUpdate()
}
return () => reactionTrackingRef.current!.reaction.dispose()
}, [])
// 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 catched during rendering
}
return rendering
}