-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathobserverClass.ts
More file actions
289 lines (265 loc) · 11.1 KB
/
Copy pathobserverClass.ts
File metadata and controls
289 lines (265 loc) · 11.1 KB
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import { PureComponent, Component, createRef } from "react"
import {
createAtom,
_allowStateChanges,
Reaction,
$mobx,
_allowStateReadsStart,
_allowStateReadsEnd
} from "mobx"
import { isUsingStaticRendering } from "mobx-react-lite"
import { newSymbol, shallowEqual, setHiddenProp, patch } from "./utils/utils"
import {
addReactionToTrack,
reactionToTrackSymbol,
recordReactionAsCommitted
} from "mobx-react-lite"
const mobxAdminProperty = $mobx || "$mobx" // BC
const mobxObserverProperty = newSymbol("isMobXReactObserver")
const mobxIsUnmounted = newSymbol("isUnmounted")
const skipRenderKey = newSymbol("skipRender")
const isForcingUpdateKey = newSymbol("isForcingUpdate")
export function makeClassComponentObserver(
componentClass: React.ComponentClass<any, any>
): React.ComponentClass<any, any> {
const target = componentClass.prototype
if (componentClass[mobxObserverProperty]) {
const displayName = getDisplayName(target)
console.warn(
`The provided component class (${displayName})
has already been declared as an observer component.`
)
} else {
componentClass[mobxObserverProperty] = true
}
if (target.componentWillReact) {
throw new Error("The componentWillReact life-cycle event is no longer supported")
}
if (componentClass["__proto__"] !== PureComponent) {
if (!target.shouldComponentUpdate) {
target.shouldComponentUpdate = observerSCU
} else if (target.shouldComponentUpdate !== observerSCU) {
// n.b. unequal check, instead of existence check, as @observer might be on superclass as well
throw new Error(
"It is not allowed to use shouldComponentUpdate in observer based components."
)
}
}
// this.props and this.state are made observable, just to make sure @computed fields that
// are defined inside the component, and which rely on state or props, re-compute if state or props change
// (otherwise the computed wouldn't update and become stale on props change, since props are not observable)
// 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+
makeObservableProp(target, "props")
makeObservableProp(target, "state")
if (componentClass.contextType) {
makeObservableProp(target, "context")
}
const originalRender = target.render
if (typeof originalRender !== "function") {
const displayName = getDisplayName(target)
throw new Error(
`[mobx-react] class component (${displayName}) is missing \`render\` method.` +
`\n\`observer\` requires \`render\` being a function defined on prototype.` +
`\n\`render = () => {}\` or \`render = function() {}\` is not supported.`
)
}
target.render = function () {
if (!this[reactionToTrackSymbol]) {
this[reactionToTrackSymbol] = createRef()
}
this.render = isUsingStaticRendering()
? originalRender
: createReactiveRender.call(this, originalRender)
return this.render()
}
patch(target, "componentDidMount", function () {
this[mobxIsUnmounted] = false
recordReactionAsCommitted(this[reactionToTrackSymbol])
let hasUpdated = false
if (this[reactionToTrackSymbol].current) {
this[reactionToTrackSymbol].current.mounted = true
if (this[reactionToTrackSymbol].current.changedBeforeMount) {
this[reactionToTrackSymbol].current.changedBeforeMount = false
hasUpdated = true
Component.prototype.forceUpdate.call(this)
}
!this.render[mobxAdminProperty]
} else {
const initialName = getDisplayName(this)
this[reactionToTrackSymbol].current = {
reaction: new Reaction(`${initialName}.render()`, () => {
Component.prototype.forceUpdate.call(this)
}),
mounted: true,
changedBeforeMount: false,
cleanAt: Infinity
}
hasUpdated = true
Component.prototype.forceUpdate.call(this)
}
if (!this.render[mobxAdminProperty] && !hasUpdated) {
// Reaction is re-created automatically during render, but a component can re-mount and skip render #3395.
// To re-create the reaction and re-subscribe to relevant observables we have to force an update.
Component.prototype.forceUpdate.call(this)
}
})
patch(target, "componentWillUnmount", function () {
if (isUsingStaticRendering()) {
return
}
this[reactionToTrackSymbol].current = null
const reaction = this.render[mobxAdminProperty]
if (reaction) {
reaction.dispose()
// Forces reaction to be re-created on next render
this.render[mobxAdminProperty] = null
} else {
// Render may have been hot-swapped and/or overridden by a subclass.
const displayName = getDisplayName(this)
console.warn(
`The reactive render of an observer class component (${displayName})
was overridden after MobX attached. This may result in a memory leak if the
overridden reactive render was not properly disposed.`
)
}
this[mobxIsUnmounted] = true
})
return componentClass
}
// Generates a friendly name for debugging
function getDisplayName(comp: any) {
return (
comp.displayName ||
comp.name ||
(comp.constructor && (comp.constructor.displayName || comp.constructor.name)) ||
"<component>"
)
}
function createReactiveRender(originalRender: any) {
/**
* If props are shallowly modified, react will render anyway,
* so atom.reportChanged() should not result in yet another re-render
*/
setHiddenProp(this, skipRenderKey, false)
/**
* forceUpdate will re-assign this.props. We don't want that to cause a loop,
* so detect these changes
*/
setHiddenProp(this, isForcingUpdateKey, false)
const initialName = getDisplayName(this)
const boundOriginalRender = originalRender.bind(this)
let isRenderingPending = false
const createReaction = () => {
const reaction = new Reaction(`${initialName}.render()`, () => {
if (!isRenderingPending) {
// N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.test.tsx)
// This unidiomatic React usage but React will correctly warn about this so we continue as usual
// See #85 / Pull #44
isRenderingPending = true
if (this[mobxIsUnmounted] !== true) {
let hasError = true
try {
setHiddenProp(this, isForcingUpdateKey, true)
if (!this[skipRenderKey]) {
if (this[reactionToTrackSymbol].current.mounted) {
Component.prototype.forceUpdate.call(this)
} else {
this[reactionToTrackSymbol].current.changedBeforeMount = true
}
}
hasError = false
} finally {
setHiddenProp(this, isForcingUpdateKey, false)
if (hasError) {
reaction.dispose()
this[reactionToTrackSymbol].current = null
// Forces reaction to be re-created on next render
this.render[mobxAdminProperty] = null
}
}
}
}
})
reaction["reactComponent"] = this
return reaction
}
function reactiveRender() {
isRenderingPending = false
// Create reaction lazily to support re-mounting #3395
const reaction = (reactiveRender[mobxAdminProperty] ??= createReaction())
if (!this[reactionToTrackSymbol].current) {
addReactionToTrack(this[reactionToTrackSymbol], reaction, {})
}
let exception: unknown = undefined
let rendering = undefined
reaction.track(() => {
try {
// TODO@major
// Optimization: replace with _allowStateChangesStart/End (not available in mobx@6.0.0)
rendering = _allowStateChanges(false, boundOriginalRender)
} catch (e) {
exception = e
}
})
if (exception) {
throw exception
}
return rendering
}
return reactiveRender
}
function observerSCU(nextProps: React.ClassAttributes<any>, nextState: any): boolean {
if (isUsingStaticRendering()) {
console.warn(
"[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."
)
}
// update on any state changes (as is the default)
if (this.state !== nextState) {
return true
}
// update if props are shallowly not equal, inspired by PureRenderMixin
// we could return just 'false' here, and avoid the `skipRender` checks etc
// however, it is nicer if lifecycle events are triggered like usually,
// so we return true here if props are shallowly modified.
return !shallowEqual(this.props, nextProps)
}
function makeObservableProp(target: any, propName: string): void {
const valueHolderKey = newSymbol(`reactProp_${propName}_valueHolder`)
const atomHolderKey = newSymbol(`reactProp_${propName}_atomHolder`)
function getAtom() {
if (!this[atomHolderKey]) {
setHiddenProp(this, atomHolderKey, createAtom("reactive " + propName))
}
return this[atomHolderKey]
}
Object.defineProperty(target, propName, {
configurable: true,
enumerable: true,
get: function () {
let prevReadState = false
// Why this check? BC?
// @ts-expect-error
if (_allowStateReadsStart && _allowStateReadsEnd) {
prevReadState = _allowStateReadsStart(true)
}
getAtom.call(this).reportObserved()
// Why this check? BC?
// @ts-expect-error
if (_allowStateReadsStart && _allowStateReadsEnd) {
_allowStateReadsEnd(prevReadState)
}
return this[valueHolderKey]
},
set: function set(v) {
if (!this[isForcingUpdateKey] && !shallowEqual(this[valueHolderKey], v)) {
setHiddenProp(this, valueHolderKey, v)
setHiddenProp(this, skipRenderKey, true)
getAtom.call(this).reportChanged()
setHiddenProp(this, skipRenderKey, false)
} else {
setHiddenProp(this, valueHolderKey, v)
}
}
})
}