1 | import { PureComponent, Component } from "react"
|
2 | import {
|
3 | createAtom,
|
4 | _allowStateChanges,
|
5 | Reaction,
|
6 | $mobx,
|
7 | _allowStateReadsStart,
|
8 | _allowStateReadsEnd
|
9 | } from "mobx"
|
10 | import { isUsingStaticRendering } from "mobx-react-lite"
|
11 |
|
12 | import { newSymbol, shallowEqual, setHiddenProp, patch } from "./utils/utils"
|
13 |
|
14 | const mobxAdminProperty = $mobx || "$mobx"
|
15 | const mobxObserverProperty = newSymbol("isMobXReactObserver")
|
16 | const mobxIsUnmounted = newSymbol("isUnmounted")
|
17 | const skipRenderKey = newSymbol("skipRender")
|
18 | const isForcingUpdateKey = newSymbol("isForcingUpdate")
|
19 |
|
20 | export 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 |
|
41 | throw new Error(
|
42 | "It is not allowed to use shouldComponentUpdate in observer based components."
|
43 | )
|
44 | }
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
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 |
|
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 |
|
84 | function 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 |
|
93 | function makeComponentReactive(render: any) {
|
94 | if (isUsingStaticRendering() === true) return render.call(this)
|
95 |
|
96 | |
97 |
|
98 |
|
99 |
|
100 | setHiddenProp(this, skipRenderKey, false)
|
101 | |
102 |
|
103 |
|
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 |
|
115 |
|
116 |
|
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 |
|
156 | function 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 |
|
163 | if (this.state !== nextState) {
|
164 | return true
|
165 | }
|
166 |
|
167 |
|
168 |
|
169 |
|
170 | return !shallowEqual(this.props, nextProps)
|
171 | }
|
172 |
|
173 | function 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 | }
|