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 349
/
Copy pathobserverClass.ts
202 lines (180 loc) · 7.49 KB
/
observerClass.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
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
import { PureComponent, Component } 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"
const mobxAdminProperty = $mobx || "$mobx"
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")
const baseRender = target.render
target.render = function() {
return makeComponentReactive.call(this, baseRender)
}
patch(target, "componentWillUnmount", function() {
if (isUsingStaticRendering() === true) return
this.render[mobxAdminProperty]?.dispose()
this[mobxIsUnmounted] = true
if (!this.render[mobxAdminProperty]) {
// Render may have been hot-swapped and/or overriden by a subclass.
const displayName = getDisplayName(this)
console.warn(
`The reactive render of an observer class component (${displayName})
was overriden after MobX attached. This may result in a memory leak if the
overriden reactive render was not properly disposed.`
)
}
})
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 makeComponentReactive(render: any) {
if (isUsingStaticRendering() === true) return render.call(this)
/**
* 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 baseRender = render.bind(this)
let isRenderingPending = false
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.js)
// 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]) Component.prototype.forceUpdate.call(this)
hasError = false
} finally {
setHiddenProp(this, isForcingUpdateKey, false)
if (hasError) reaction.dispose()
}
}
}
})
reaction["reactComponent"] = this
reactiveRender[mobxAdminProperty] = reaction
this.render = reactiveRender
function reactiveRender() {
isRenderingPending = false
let exception = undefined
let rendering = undefined
reaction.track(() => {
try {
rendering = _allowStateChanges(false, baseRender)
} catch (e) {
exception = e
}
})
if (exception) {
throw exception
}
return rendering
}
return reactiveRender.call(this)
}
function observerSCU(nextProps: React.Props<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
if (_allowStateReadsStart && _allowStateReadsEnd) {
prevReadState = _allowStateReadsStart(true)
}
getAtom.call(this).reportObserved()
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)
}
}
})
}