UNPKG

8.08 kBPlain TextView Raw
1import { PureComponent, Component } from "react"
2import {
3 createAtom,
4 _allowStateChanges,
5 Reaction,
6 $mobx,
7 _allowStateReadsStart,
8 _allowStateReadsEnd
9} from "mobx"
10import { isUsingStaticRendering } from "mobx-react-lite"
11
12import { newSymbol, shallowEqual, setHiddenProp, patch } from "./utils/utils"
13
14const mobxAdminProperty = $mobx || "$mobx"
15const mobxObserverProperty = newSymbol("isMobXReactObserver")
16const mobxIsUnmounted = newSymbol("isUnmounted")
17const skipRenderKey = newSymbol("skipRender")
18const isForcingUpdateKey = newSymbol("isForcingUpdate")
19
20export function makeClassComponentObserver(
21 componentClass: React.ComponentClass<any, any>
22): React.ComponentClass<any, any> {
23 const target = componentClass.prototype
24
25 if (componentClass[mobxObserverProperty]) {
26 const displayName = getDisplayName(target)
27 console.warn(
28 `The provided component class (${displayName})
29 has already been declared as an observer component.`
30 )
31 } else {
32 componentClass[mobxObserverProperty] = true
33 }
34
35 if (target.componentWillReact)
36 throw new Error("The componentWillReact life-cycle event is no longer supported")
37 if (componentClass["__proto__"] !== PureComponent) {
38 if (!target.shouldComponentUpdate) target.shouldComponentUpdate = observerSCU
39 else if (target.shouldComponentUpdate !== observerSCU)
40 // n.b. unequal check, instead of existence check, as @observer might be on superclass as well
41 throw new Error(
42 "It is not allowed to use shouldComponentUpdate in observer based components."
43 )
44 }
45
46 // this.props and this.state are made observable, just to make sure @computed fields that
47 // are defined inside the component, and which rely on state or props, re-compute if state or props change
48 // (otherwise the computed wouldn't update and become stale on props change, since props are not observable)
49 // 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+
50 makeObservableProp(target, "props")
51 makeObservableProp(target, "state")
52
53 const baseRender = target.render
54 if (typeof baseRender !== 'function') {
55 const displayName = getDisplayName(target)
56 throw new Error(
57 `[mobx-react] class component (${displayName}) is missing \`render\` method.`
58 + `\n\`observer\` requires \`render\` being a function defined on prototype.`
59 + `\n\`render = () => {}\` or \`render = function() {}\` is not supported.`
60 )
61 }
62 target.render = function () {
63 return makeComponentReactive.call(this, baseRender)
64 }
65 patch(target, "componentWillUnmount", function () {
66 if (isUsingStaticRendering() === true) return
67 this.render[mobxAdminProperty]?.dispose()
68 this[mobxIsUnmounted] = true
69
70 if (!this.render[mobxAdminProperty]) {
71 // Render may have been hot-swapped and/or overriden by a subclass.
72 const displayName = getDisplayName(this)
73 console.warn(
74 `The reactive render of an observer class component (${displayName})
75 was overriden after MobX attached. This may result in a memory leak if the
76 overriden reactive render was not properly disposed.`
77 )
78 }
79 })
80 return componentClass
81}
82
83// Generates a friendly name for debugging
84function getDisplayName(comp: any) {
85 return (
86 comp.displayName ||
87 comp.name ||
88 (comp.constructor && (comp.constructor.displayName || comp.constructor.name)) ||
89 "<component>"
90 )
91}
92
93function makeComponentReactive(render: any) {
94 if (isUsingStaticRendering() === true) return render.call(this)
95
96 /**
97 * If props are shallowly modified, react will render anyway,
98 * so atom.reportChanged() should not result in yet another re-render
99 */
100 setHiddenProp(this, skipRenderKey, false)
101 /**
102 * forceUpdate will re-assign this.props. We don't want that to cause a loop,
103 * so detect these changes
104 */
105 setHiddenProp(this, isForcingUpdateKey, false)
106
107 const initialName = getDisplayName(this)
108 const baseRender = render.bind(this)
109
110 let isRenderingPending = false
111
112 const reaction = new Reaction(`${initialName}.render()`, () => {
113 if (!isRenderingPending) {
114 // N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js)
115 // This unidiomatic React usage but React will correctly warn about this so we continue as usual
116 // See #85 / Pull #44
117 isRenderingPending = true
118 if (this[mobxIsUnmounted] !== true) {
119 let hasError = true
120 try {
121 setHiddenProp(this, isForcingUpdateKey, true)
122 if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this)
123 hasError = false
124 } finally {
125 setHiddenProp(this, isForcingUpdateKey, false)
126 if (hasError) reaction.dispose()
127 }
128 }
129 }
130 })
131
132 reaction["reactComponent"] = this
133 reactiveRender[mobxAdminProperty] = reaction
134 this.render = reactiveRender
135
136 function reactiveRender() {
137 isRenderingPending = false
138 let exception = undefined
139 let rendering = undefined
140 reaction.track(() => {
141 try {
142 rendering = _allowStateChanges(false, baseRender)
143 } catch (e) {
144 exception = e
145 }
146 })
147 if (exception) {
148 throw exception
149 }
150 return rendering
151 }
152
153 return reactiveRender.call(this)
154}
155
156function observerSCU(nextProps: React.Props<any>, nextState: any): boolean {
157 if (isUsingStaticRendering()) {
158 console.warn(
159 "[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."
160 )
161 }
162 // update on any state changes (as is the default)
163 if (this.state !== nextState) {
164 return true
165 }
166 // update if props are shallowly not equal, inspired by PureRenderMixin
167 // we could return just 'false' here, and avoid the `skipRender` checks etc
168 // however, it is nicer if lifecycle events are triggered like usually,
169 // so we return true here if props are shallowly modified.
170 return !shallowEqual(this.props, nextProps)
171}
172
173function makeObservableProp(target: any, propName: string): void {
174 const valueHolderKey = newSymbol(`reactProp_${propName}_valueHolder`)
175 const atomHolderKey = newSymbol(`reactProp_${propName}_atomHolder`)
176 function getAtom() {
177 if (!this[atomHolderKey]) {
178 setHiddenProp(this, atomHolderKey, createAtom("reactive " + propName))
179 }
180 return this[atomHolderKey]
181 }
182 Object.defineProperty(target, propName, {
183 configurable: true,
184 enumerable: true,
185 get: function () {
186 let prevReadState = false
187
188 if (_allowStateReadsStart && _allowStateReadsEnd) {
189 prevReadState = _allowStateReadsStart(true)
190 }
191 getAtom.call(this).reportObserved()
192
193 if (_allowStateReadsStart && _allowStateReadsEnd) {
194 _allowStateReadsEnd(prevReadState)
195 }
196
197 return this[valueHolderKey]
198 },
199 set: function set(v) {
200 if (!this[isForcingUpdateKey] && !shallowEqual(this[valueHolderKey], v)) {
201 setHiddenProp(this, valueHolderKey, v)
202 setHiddenProp(this, skipRenderKey, true)
203 getAtom.call(this).reportChanged()
204 setHiddenProp(this, skipRenderKey, false)
205 } else {
206 setHiddenProp(this, valueHolderKey, v)
207 }
208 }
209 })
210}