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

mobxjs / mobx / 3812415023

pending completion
3812415023

Pull #3590

github

GitHub
Merge dd52381cb into 879982e18
Pull Request #3590: Better React 18 support

1661 of 2053 branches covered (80.91%)

156 of 156 new or added lines in 10 files covered. (100.0%)

3200 of 3488 relevant lines covered (91.74%)

6512.07 hits per line

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

88.97
/packages/mobx-react/src/observerClass.ts
1
import { PureComponent, Component, ComponentClass, ClassAttributes } from "react"
12✔
2
import {
12✔
3
    createAtom,
4
    _allowStateChanges,
5
    Reaction,
6
    //$mobx, // TODO
7
    _allowStateReadsStart,
8
    _allowStateReadsEnd,
9
    _getGlobalState,
10
    IAtom
11
} from "mobx"
12
import {
12✔
13
    isUsingStaticRendering,
14
    _observerFinalizationRegistry as observerFinalizationRegistry
15
} from "mobx-react-lite"
16
import { shallowEqual, patch } from "./utils/utils"
12✔
17

18
// TODO symbols
19

20
const administrationSymbol = Symbol("ObserverAdministration")
12✔
21
//const mobxAdminProperty = $mobx // TODO
22
const isMobXReactObserverSymbol = Symbol("isMobXReactObserver")
12✔
23

24
type ObserverAdministration = {
25
    reaction: Reaction | null // also serves as disposed flag
26
    forceUpdate: Function | null
27
    mounted: boolean // we could use forceUpdate as mounted flag
28
    name: string
29
    propsAtom: IAtom
30
    stateAtom: IAtom
31
    contextAtom: IAtom
32
    props: any
33
    state: any
34
    context: any
35
    // Setting this.props causes forceUpdate, because this.props is observable.
36
    // forceUpdate sets this.props.
37
    // This flag is used to avoid the loop.
38
    isUpdating: boolean
39
}
40

41
function getAdministration(component: Component): ObserverAdministration {
42
    // We create administration lazily, because we can't patch constructor
43
    // and the exact moment of initialization partially depends on React internals.
44
    // At the time of writing this, the first thing invoked is one of the observable getter/setter (state/props/context).
45
    return (component[administrationSymbol] ??= {
1,832✔
46
        reaction: null,
47
        mounted: false,
48
        forceUpdate: null,
49
        name: getDisplayName(component.constructor as ComponentClass),
50
        state: undefined,
51
        props: undefined,
52
        context: undefined,
53
        propsAtom: createAtom("props"),
54
        stateAtom: createAtom("state"),
55
        contextAtom: createAtom("context"),
56
        isUpdating: false
57
    })
58
}
59

60
export function makeClassComponentObserver(
12✔
61
    componentClass: ComponentClass<any, any>
62
): ComponentClass<any, any> {
63
    const { prototype } = componentClass
50✔
64

65
    if (componentClass[isMobXReactObserverSymbol]) {
50✔
66
        const displayName = getDisplayName(componentClass)
1✔
67
        console.warn(
1✔
68
            `The provided component class (${displayName})
69
                has already been declared as an observer component.`
70
        )
71
    } else {
72
        componentClass[isMobXReactObserverSymbol] = true
49✔
73
    }
74

75
    if (prototype.componentWillReact) {
50!
76
        throw new Error("The componentWillReact life-cycle event is no longer supported")
×
77
    }
78
    if (componentClass["__proto__"] !== PureComponent) {
50✔
79
        if (!prototype.shouldComponentUpdate) {
47✔
80
            prototype.shouldComponentUpdate = observerSCU
46✔
81
        } else if (prototype.shouldComponentUpdate !== observerSCU) {
1!
82
            // n.b. unequal check, instead of existence check, as @observer might be on superclass as well
83
            throw new Error(
×
84
                "It is not allowed to use shouldComponentUpdate in observer based components."
85
            )
86
        }
87
    }
88

89
    // this.props and this.state are made observable, just to make sure @computed fields that
90
    // are defined inside the component, and which rely on state or props, re-compute if state or props change
91
    // (otherwise the computed wouldn't update and become stale on props change, since props are not observable)
92
    // However, this solution is not without it's own problems: https://github.com/mobxjs/mobx-react/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aobservable-props-or-not+
93
    Object.defineProperties(prototype, {
50✔
94
        props: observablePropsDescriptor,
95
        state: observableStateDescriptor,
96
        context: observableContextDescriptor
97
    })
98

99
    const originalRender = prototype.render
50✔
100
    if (typeof originalRender !== "function") {
50✔
101
        const displayName = getDisplayName(componentClass)
1✔
102
        throw new Error(
1✔
103
            `[mobx-react] class component (${displayName}) is missing \`render\` method.` +
104
                `\n\`observer\` requires \`render\` being a function defined on prototype.` +
105
                `\n\`render = () => {}\` or \`render = function() {}\` is not supported.`
106
        )
107
    }
108

109
    prototype.render = function () {
49✔
110
        Object.defineProperty(this, "render", {
56✔
111
            // There is no safe way to replace render, therefore it's forbidden.
112
            configurable: false,
113
            writable: false,
114
            value: isUsingStaticRendering()
56✔
115
                ? originalRender
116
                : createReactiveRender.call(this, originalRender)
117
        })
118
        return this.render()
56✔
119
    }
120

121
    patch(prototype, "componentDidMount", function () {
49✔
122
        // `componentDidMount` may not be called at all. React can abandon the instance after `render`.
123
        // That's why we use finalization registry to dispose reaction created during render.
124
        // Happens with `<Suspend>` see #3492
125
        //
126
        // `componentDidMount` can be called immediately after `componentWillUnmount` without calling `render` in between.
127
        // Happens with `<StrictMode>`see #3395.
128
        //
129
        // If `componentDidMount` is called, it's guaranteed to run synchronously with render (similary to `useLayoutEffect`).
130
        // Therefore we don't have to worry about external (observable) state being updated before mount (no state version checking).
131
        //
132
        // Things may change: "In the future, React will provide a feature that lets components preserve state between unmounts"
133

134
        const admin = getAdministration(this)
56✔
135

136
        admin.mounted = true
56✔
137

138
        // Component instance committed, prevent reaction disposal.
139
        observerFinalizationRegistry.unregister(admin)
56✔
140

141
        // We don't set forceUpdate before mount because it requires a reference to `this`,
142
        // therefore `this` could NOT be garbage collected before mount,
143
        // preventing reaction disposal by FinalizationRegistry and leading to memory leak.
144
        // As an alternative we could have `admin.instanceRef = new WeakRef(this)`, but lets avoid it if possible.
145
        admin.forceUpdate = () => this.forceUpdate()
56✔
146

147
        if (!admin.reaction) {
56✔
148
            // 1. Instance was unmounted (reaction disposed) and immediately remounted without running render #3395.
149
            // 2. Reaction was disposed by finalization registry before mount. Shouldn't ever happen for class components:
150
            // `componentDidMount` runs synchronously after render, but our registry are deferred (can't run in between).
151
            // In any case we lost subscriptions to observables, so we have to create new reaction and re-render to resubscribe.
152
            // The reaction will be created lazily by following render.
153
            admin.forceUpdate()
3✔
154
        }
155
    })
156

157
    patch(prototype, "componentWillUnmount", function () {
49✔
158
        if (isUsingStaticRendering()) {
56✔
159
            return
1✔
160
        }
161
        const admin = getAdministration(this)
55✔
162
        admin.reaction?.dispose()
55!
163
        admin.reaction = null
55✔
164
        admin.forceUpdate = null
55✔
165
        admin.mounted = false
55✔
166
    })
167

168
    return componentClass
49✔
169
}
170

171
// Generates a friendly name for debugging
172
function getDisplayName(componentClass: ComponentClass) {
173
    return componentClass.displayName || componentClass.name || "<component>"
60!
174
}
175

176
function createReactiveRender(originalRender: any) {
177
    const boundOriginalRender = originalRender.bind(this)
55✔
178

179
    const admin = getAdministration(this)
55✔
180

181
    function reactiveRender() {
182
        if (!admin.reaction) {
103✔
183
            // Create reaction lazily to support re-mounting #3395
184
            admin.reaction = createReaction(admin)
57✔
185
            if (!admin.mounted) {
57✔
186
                // React can abandon this instance and never call `componentDidMount`/`componentWillUnmount`,
187
                // we have to make sure reaction will be disposed.
188
                observerFinalizationRegistry.register(this, admin, this)
55✔
189
            }
190
        }
191

192
        let error: unknown = undefined
103✔
193
        let renderResult = undefined
103✔
194
        admin.reaction.track(() => {
103✔
195
            try {
103✔
196
                // TODO@major
197
                // Optimization: replace with _allowStateChangesStart/End (not available in mobx@6.0.0)
198
                renderResult = _allowStateChanges(false, boundOriginalRender)
103✔
199
            } catch (e) {
200
                error = e
4✔
201
            }
202
        })
203
        if (error) {
103✔
204
            throw error
4✔
205
        }
206
        return renderResult
99✔
207
    }
208

209
    return reactiveRender
55✔
210
}
211

212
function createReaction(admin: ObserverAdministration) {
213
    return new Reaction(`${admin.name}.render()`, () => {
57✔
214
        if (admin.isUpdating) {
32✔
215
            // Reaction is suppressed when setting new state/props/context,
216
            // this is when component is already being updated.
217
            return
9✔
218
        }
219

220
        if (!admin.mounted) {
23✔
221
            // This is neccessary to avoid react warning about calling forceUpdate on component that isn't mounted yet.
222
            // This happens when component is abandoned after render - our reaction is already created and reacts to changes.
223
            // Due to the synchronous nature of `componenDidMount`, we don't have to worry that component could eventually mount and require update.
224
            return
1✔
225
        }
226

227
        try {
22✔
228
            // forceUpdate sets new `props`, since we made it observable, it would `reportChanged`, causing a loop.
229
            admin.isUpdating = true
22✔
230
            admin.forceUpdate?.()
22!
231
        } catch (error) {
232
            admin.reaction?.dispose()
×
233
            admin.reaction = null
×
234
        } finally {
235
            admin.isUpdating = false
22✔
236
        }
237
    })
238
}
239

240
function observerSCU(nextProps: ClassAttributes<any>, nextState: any): boolean {
241
    if (isUsingStaticRendering()) {
14!
242
        console.warn(
×
243
            "[mobx-react] It seems that a re-rendering of a React component is triggered while in static (server-side) mode. Please make sure components are rendered only once server-side."
244
        )
245
    }
246
    // update on any state changes (as is the default)
247
    if (this.state !== nextState) {
14✔
248
        return true
3✔
249
    }
250
    // update if props are shallowly not equal, inspired by PureRenderMixin
251
    // we could return just 'false' here, and avoid the `skipRender` checks etc
252
    // however, it is nicer if lifecycle events are triggered like usually,
253
    // so we return true here if props are shallowly modified.
254
    return !shallowEqual(this.props, nextProps)
11✔
255
}
256

257
function createObservablePropDescriptor(key: "props" | "state" | "context") {
258
    const atomKey = `${key}Atom`
36✔
259
    return {
36✔
260
        configurable: true,
261
        enumerable: true,
262
        get() {
263
            const admin = getAdministration(this)
931✔
264

265
            let prevReadState = _allowStateReadsStart(true)
931✔
266

267
            admin[atomKey].reportObserved()
931✔
268

269
            _allowStateReadsEnd(prevReadState)
931✔
270

271
            return admin[key]
931✔
272
        },
273
        set(value) {
274
            const admin = getAdministration(this)
735✔
275
            // forceUpdate issued by reaction sets new props.
276
            // It sets isUpdating to true to prevent loop.
277
            if (!admin.isUpdating && !shallowEqual(admin[key], value)) {
735✔
278
                admin[key] = value
189✔
279
                // This notifies all observers including our component,
280
                // but we don't want to cause `forceUpdate`, because component is already updating,
281
                // therefore supress component reaction.
282
                admin.isUpdating = true
189✔
283
                admin[atomKey].reportChanged()
189✔
284
                admin.isUpdating = false
189✔
285
            } else {
286
                admin[key] = value
546✔
287
            }
288
        }
289
    }
290
}
291

292
const observablePropsDescriptor = createObservablePropDescriptor("props")
12✔
293
const observableStateDescriptor = createObservablePropDescriptor("state")
12✔
294
const observableContextDescriptor = createObservablePropDescriptor("context")
12✔
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

© 2025 Coveralls, Inc