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 | 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 |
|
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 |
|
76 | function 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 |
|
85 | function makeComponentReactive(render: any) {
|
86 | if (isUsingStaticRendering() === true) return render.call(this)
|
87 |
|
88 | |
89 |
|
90 |
|
91 |
|
92 | setHiddenProp(this, skipRenderKey, false)
|
93 | |
94 |
|
95 |
|
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 |
|
107 |
|
108 |
|
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 |
|
148 | function 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 |
|
155 | if (this.state !== nextState) {
|
156 | return true
|
157 | }
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | return !shallowEqual(this.props, nextProps)
|
163 | }
|
164 |
|
165 | function 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 | }
|