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

mobxjs / mobx / 3919662049

pending completion
3919662049

push

github

GitHub
`useObserver` registry refactor (#3598)

1666 of 2056 branches covered (81.03%)

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

3216 of 3504 relevant lines covered (91.78%)

6475.76 hits per line

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

98.21
/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
            adm.changedBeforeMount = false
115✔
116
        }
117
    }, [])
118

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

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

136
    return rendering
230✔
137
}
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