UNPKG

7.67 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 target.render = function () {
55 return makeComponentReactive.call(this, baseRender)
56 }
57 patch(target, "componentWillUnmount", function () {
58 if (isUsingStaticRendering() === true) return
59 this.render[mobxAdminProperty]?.dispose()
60 this[mobxIsUnmounted] = true
61
62 if (!this.render[mobxAdminProperty]) {
63 // Render may have been hot-swapped and/or overriden by a subclass.
64 const displayName = getDisplayName(this)
65 console.warn(
66 `The reactive render of an observer class component (${displayName})
67 was overriden after MobX attached. This may result in a memory leak if the
68 overriden reactive render was not properly disposed.`
69 )
70 }
71 })
72 return componentClass
73}
74
75// Generates a friendly name for debugging
76function getDisplayName(comp: any) {
77 return (
78 comp.displayName ||
79 comp.name ||
80 (comp.constructor && (comp.constructor.displayName || comp.constructor.name)) ||
81 "<component>"
82 )
83}
84
85function makeComponentReactive(render: any) {
86 if (isUsingStaticRendering() === true) return render.call(this)
87
88 /**
89 * If props are shallowly modified, react will render anyway,
90 * so atom.reportChanged() should not result in yet another re-render
91 */
92 setHiddenProp(this, skipRenderKey, false)
93 /**
94 * forceUpdate will re-assign this.props. We don't want that to cause a loop,
95 * so detect these changes
96 */
97 setHiddenProp(this, isForcingUpdateKey, false)
98
99 const initialName = getDisplayName(this)
100 const baseRender = render.bind(this)
101
102 let isRenderingPending = false
103
104 const reaction = new Reaction(`${initialName}.render()`, () => {
105 if (!isRenderingPending) {
106 // N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js)
107 // This unidiomatic React usage but React will correctly warn about this so we continue as usual
108 // See #85 / Pull #44
109 isRenderingPending = true
110 if (this[mobxIsUnmounted] !== true) {
111 let hasError = true
112 try {
113 setHiddenProp(this, isForcingUpdateKey, true)
114 if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this)
115 hasError = false
116 } finally {
117 setHiddenProp(this, isForcingUpdateKey, false)
118 if (hasError) reaction.dispose()
119 }
120 }
121 }
122 })
123
124 reaction["reactComponent"] = this
125 reactiveRender[mobxAdminProperty] = reaction
126 this.render = reactiveRender
127
128 function reactiveRender() {
129 isRenderingPending = false
130 let exception = undefined
131 let rendering = undefined
132 reaction.track(() => {
133 try {
134 rendering = _allowStateChanges(false, baseRender)
135 } catch (e) {
136 exception = e
137 }
138 })
139 if (exception) {
140 throw exception
141 }
142 return rendering
143 }
144
145 return reactiveRender.call(this)
146}
147
148function observerSCU(nextProps: React.Props<any>, nextState: any): boolean {
149 if (isUsingStaticRendering()) {
150 console.warn(
151 "[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."
152 )
153 }
154 // update on any state changes (as is the default)
155 if (this.state !== nextState) {
156 return true
157 }
158 // update if props are shallowly not equal, inspired by PureRenderMixin
159 // we could return just 'false' here, and avoid the `skipRender` checks etc
160 // however, it is nicer if lifecycle events are triggered like usually,
161 // so we return true here if props are shallowly modified.
162 return !shallowEqual(this.props, nextProps)
163}
164
165function makeObservableProp(target: any, propName: string): void {
166 const valueHolderKey = newSymbol(`reactProp_${propName}_valueHolder`)
167 const atomHolderKey = newSymbol(`reactProp_${propName}_atomHolder`)
168 function getAtom() {
169 if (!this[atomHolderKey]) {
170 setHiddenProp(this, atomHolderKey, createAtom("reactive " + propName))
171 }
172 return this[atomHolderKey]
173 }
174 Object.defineProperty(target, propName, {
175 configurable: true,
176 enumerable: true,
177 get: function () {
178 let prevReadState = false
179
180 if (_allowStateReadsStart && _allowStateReadsEnd) {
181 prevReadState = _allowStateReadsStart(true)
182 }
183 getAtom.call(this).reportObserved()
184
185 if (_allowStateReadsStart && _allowStateReadsEnd) {
186 _allowStateReadsEnd(prevReadState)
187 }
188
189 return this[valueHolderKey]
190 },
191 set: function set(v) {
192 if (!this[isForcingUpdateKey] && !shallowEqual(this[valueHolderKey], v)) {
193 setHiddenProp(this, valueHolderKey, v)
194 setHiddenProp(this, skipRenderKey, true)
195 getAtom.call(this).reportChanged()
196 setHiddenProp(this, skipRenderKey, false)
197 } else {
198 setHiddenProp(this, valueHolderKey, v)
199 }
200 }
201 })
202}