• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mobxjs / mobx / 3812600369

pending completion
3812600369

Pull #3598

github

GitHub
Merge 965b6f949 into 879982e18
Pull Request #3598: `useObserver` registry refactor

1658 of 2048 branches covered (80.96%)

51 of 51 new or added lines in 4 files covered. (100.0%)

3215 of 3503 relevant lines covered (91.78%)

6477.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.18
/packages/mobx-react-lite/src/useObserver.ts
1
import { Reaction } from "mobx"
10✔
2
import React from "react"
10✔
3
import { printDebugValue } from "./utils/printDebugValue"
10✔
4
import { observerFinalizationRegistry } from "./utils/observerFinalizationRegistry"
10✔
5
import { isUsingStaticRendering } from "./staticRendering"
10✔
6

7
function observerComponentNameFor(baseComponentName: string) {
8
    return `observer${baseComponentName}`
127✔
9
}
10

11
type ObserverAdministration = {
12
    /** The Reaction created during first render, which may be leaked */
13
    reaction: Reaction | null
14

15
    /**
16
     * Whether the component has yet completed mounting (for us, whether
17
     * its useEffect has run)
18
     */
19
    mounted: boolean
20

21
    /**
22
     * Whether the observables that the component is tracking changed between
23
     * the first render and the first useEffect.
24
     */
25
    changedBeforeMount: boolean
26
}
27

28
/**
29
 * We use class to make it easier to detect in heap snapshots by name
30
 */
31
class ObjectToBeRetainedByReact {}
32

33
function objectToBeRetainedByReactFactory() {
34
    return new ObjectToBeRetainedByReact()
120✔
35
}
36

37
export function useObserver<T>(fn: () => T, baseComponentName: string = "observed"): T {
10✔
38
    if (isUsingStaticRendering()) {
245✔
39
        return fn()
2✔
40
    }
41

42
    const [objectRetainedByReact] = React.useState(objectToBeRetainedByReactFactory)
243✔
43
    // Force update, see #2982
44
    const [, setState] = React.useState()
238✔
45
    const forceUpdate = () => setState([] as any)
238✔
46

47
    // StrictMode/ConcurrentMode/Suspense may mean that our component is
48
    // rendered and abandoned multiple times, so we need to track leaked
49
    // Reactions.
50
    const admRef = React.useRef<ObserverAdministration | null>(null)
238✔
51

52
    if (!admRef.current) {
238✔
53
        // First render
54
        admRef.current = {
120✔
55
            reaction: null,
56
            mounted: false,
57
            changedBeforeMount: false
58
        }
59
    }
60

61
    const adm = admRef.current!
238✔
62

63
    if (!adm.reaction) {
238✔
64
        // First render or component was not committed and reaction was disposed by registry
65
        adm.reaction = new Reaction(observerComponentNameFor(baseComponentName), () => {
120✔
66
            // Observable has changed, meaning we want to re-render
67
            // BUT if we're a component that hasn't yet got to the useEffect()
68
            // stage, we might be a component that _started_ to render, but
69
            // got dropped, and we don't want to make state changes then.
70
            // (It triggers warnings in StrictMode, for a start.)
71
            if (adm.mounted) {
80✔
72
                // We have reached useEffect(), so we're mounted, and can trigger an update
73
                forceUpdate()
78✔
74
            } else {
75
                // We haven't yet reached useEffect(), so we'll need to trigger a re-render
76
                // when (and if) useEffect() arrives.
77
                adm.changedBeforeMount = true
2✔
78
            }
79
        })
80

81
        observerFinalizationRegistry.register(objectRetainedByReact, adm, adm)
120✔
82
    }
83

84
    React.useDebugValue(adm.reaction, printDebugValue)
238✔
85

86
    React.useEffect(() => {
238✔
87
        observerFinalizationRegistry.unregister(adm)
115✔
88

89
        adm.mounted = true
115✔
90

91
        if (adm.reaction) {
115✔
92
            if (adm.changedBeforeMount) {
108✔
93
                // Got a change before mount, force an update
94
                adm.changedBeforeMount = false
1✔
95
                forceUpdate()
1✔
96
            }
97
        } else {
98
            // The reaction we set up in our render has been disposed.
99
            // This can be due to bad timings of renderings, e.g. our
100
            // component was paused for a _very_ long time, and our
101
            // reaction got cleaned up
102

103
            // Re-create the reaction
104
            adm.reaction = new Reaction(observerComponentNameFor(baseComponentName), () => {
7✔
105
                // We've definitely already been mounted at this point
106
                forceUpdate()
×
107
            })
108
            forceUpdate()
7✔
109
        }
110

111
        return () => {
115✔
112
            adm.reaction!.dispose()
115✔
113
            adm.reaction = null
115✔
114
            adm.mounted = false
115✔
115
        }
116
    }, [])
117

118
    // render the original component, but have the
119
    // reaction track the observables, so that rendering
120
    // can be invalidated (see above) once a dependency changes
121
    let rendering!: T
122
    let exception
123
    adm.reaction.track(() => {
238✔
124
        try {
238✔
125
            rendering = fn()
238✔
126
        } catch (e) {
127
            exception = e
8✔
128
        }
129
    })
130

131
    if (exception) {
238✔
132
        throw exception // re-throw any exceptions caught during rendering
8✔
133
    }
134

135
    return rendering
230✔
136
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc