findDOMNode when a component is not yet mounted or unmounted
First we need to check if a component subtree is mounted at all. If it is, we need to search down the fiber for the first host node. However, we might be searching the "work in progress" instead of current. One realization is that it doesn't matter if we search work in progress or current if they're the same. They will generally be the same unless there is an insertion pending or something in the alternate tree was already deleted. So if we find one of those cases, we switch to look in the alternate tree instead.
Sebastian Markbage committed
Oct 24, 2016 at 16:02 UTC
d5752aca0dc40f4ab3ec480940d045dc1fddf889
3 files changed
+195
-10
src/renderers/noop/ReactNoop.js
+12
@@ -154,6 +154,18 @@ var ReactNoop = {
154
}
155
},
156
157
+ findInstance(componentOrElement : Element | ?ReactComponent<any, any, any>) : null | Instance | TextInstance {
158
+ if (componentOrElement == null) {
159
+ return null;
160
+ }
161
+ // Unsound duck typing.
162
+ const component = (componentOrElement : any);
163
+ if (component.tag === TERMINAL_TAG || component.tag === TEXT_TAG) {
164
+ return component;
165
+ }
166
+ return NoopRenderer.findHostInstance(component);
167
+ },
168
+
169
flushAnimationPri() {
170
var cb = scheduledAnimationCallback;
171
if (cb === null) {
src/renderers/shared/fiber/ReactFiberTreeReflection.js
+35
-10
@@ -17,7 +17,6 @@ import type { Fiber } from 'ReactFiber';
17
var ReactInstanceMap = require('ReactInstanceMap');
18
19
var {
20
- ClassComponent,
20
HostContainer,
21
HostComponent,
22
HostText,
@@ -28,13 +27,9 @@ var {
27
Placement,
28
} = require('ReactTypeOfSideEffect');
29
31
-exports.isMounted = function(component : ReactComponent<any, any, any>) : boolean {
32
- var parent : ?Fiber = ReactInstanceMap.get(component);
33
- if (!parent) {
34
- return false;
35
- }
36
- let node = parent;
37
- if (!parent.alternate) {
30
+function isFiberMounted(fiber : Fiber) : boolean {
31
+ let node = fiber;
32
+ if (!fiber.alternate) {
33
// If there is no alternate, this might be a new tree that isn't inserted
34
// yet. If it is, then it will have a pending insertion effect on it.
35
if ((node.effectTag & Placement) !== NoEffect) {
@@ -58,17 +53,47 @@ exports.isMounted = function(component : ReactComponent<any, any, any>) : boolea
53
}
54
// If we didn't hit the root, that means that we're in an disconnected tree.
55
return false;
56
+}
57
+
58
+exports.isMounted = function(component : ReactComponent<any, any, any>) : boolean {
59
+ var fiber : ?Fiber = ReactInstanceMap.get(component);
60
+ if (!fiber) {
61
+ return false;
62
+ }
63
+ return isFiberMounted(fiber);
64
};
65
66
exports.findCurrentHostFiber = function(component : ReactComponent<any, any, any>) : Fiber | null {
64
- var parent : ?Fiber = ReactInstanceMap.get(component);
67
+ let parent = ReactInstanceMap.get(component);
68
if (!parent) {
69
return null;
70
}
71
69
- // TODO: This search is incomplete because this could be one of two possible fibers.
72
+ if (!isFiberMounted(parent)) {
73
+ // First check if this node itself is mounted.
74
+ return null;
75
+ }
76
+
77
+ let didTryOtherTree = false;
78
+
79
+ // Next we'll drill down this component to find the first HostComponent/Text.
80
let node : Fiber = parent;
81
while (true) {
82
+ if ((node.effectTag & Placement) !== NoEffect || !node.return) {
83
+ // If any node along the way was deleted, or is an insertion, that means
84
+ // that we're actually in a work in progress to update this component with
85
+ // a different component. We need to look in the "current" fiber instead.
86
+ if (!parent.alternate) {
87
+ return null;
88
+ }
89
+ if (didTryOtherTree) {
90
+ // Safety, to avoid an infinite loop if something goes wrong.
91
+ throw new Error('This should never hit this infinite loop.');
92
+ }
93
+ didTryOtherTree = true;
94
+ node = parent = parent.alternate;
95
+ continue;
96
+ }
97
if (node.tag === HostComponent || node.tag === HostText) {
98
return node;
99
} else if (node.child) {
src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
+148
@@ -124,4 +124,152 @@ describe('ReactIncrementalReflection', () => {
124
125
});
126
127
+ it('finds no node before insertion and correct node before deletion', () => {
128
+
129
+ let ops = [];
130
+
131
+ let classInstance = null;
132
+
133
+ class Component extends React.Component {
134
+ componentWillMount() {
135
+ classInstance = this;
136
+ ops.push('componentWillMount', ReactNoop.findInstance(this));
137
+ }
138
+ componentDidMount() {
139
+ ops.push('componentDidMount', ReactNoop.findInstance(this));
140
+ }
141
+ componentWillUpdate() {
142
+ ops.push('componentWillUpdate', ReactNoop.findInstance(this));
143
+ }
144
+ componentDidUpdate() {
145
+ ops.push('componentDidUpdate', ReactNoop.findInstance(this));
146
+ }
147
+ render() {
148
+ ops.push('render');
149
+ return this.props.step < 2 ?
150
+ <span ref={ref => this.span = ref} /> :
151
+ this.props.step === 2 ?
152
+ <div ref={ref => this.div = ref} /> :
153
+ null;
154
+ }
155
+ }
156
+
157
+ function Sibling() {
158
+ // Sibling is used to assert that we've rendered past the first component.
159
+ ops.push('render sibling');
160
+ return <span />;
161
+ }
162
+
163
+ function Foo(props) {
164
+ return [
165
+ <Component step={props.step} />,
166
+ <Sibling />,
167
+ ];
168
+ }
169
+
170
+ ReactNoop.render(<Foo step={0} />);
171
+ // Flush past Component but don't complete rendering everything yet.
172
+ ReactNoop.flushDeferredPri(30);
173
+
174
+ expect(ops).toEqual([
175
+ 'componentWillMount', null,
176
+ 'render',
177
+ 'render sibling',
178
+ ]);
179
+
180
+ ops = [];
181
+
182
+ expect(classInstance).toBeDefined();
183
+ // The instance has been complete but is still not committed so it should
184
+ // not find any host nodes in it.
185
+ expect(ReactNoop.findInstance(classInstance)).toBe(null);
186
+
187
+ ReactNoop.flush();
188
+
189
+ const hostSpan = classInstance.span;
190
+ expect(hostSpan).toBeDefined();
191
+
192
+ expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
193
+
194
+ expect(ops).toEqual([
195
+ 'componentDidMount', hostSpan,
196
+ ]);
197
+
198
+ ops = [];
199
+
200
+ // Flush next step which will cause an update but not yet render a new host
201
+ // node.
202
+ ReactNoop.render(<Foo step={1} />);
203
+ ReactNoop.flush();
204
+
205
+ expect(ops).toEqual([
206
+ 'componentWillUpdate', hostSpan,
207
+ 'render',
208
+ 'render sibling',
209
+ 'componentDidUpdate', hostSpan,
210
+ ]);
211
+
212
+ expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
213
+
214
+ ops = [];
215
+
216
+ // The next step will render a new host node but won't get committed yet.
217
+ // We expect this to mutate the original Fiber.
218
+ ReactNoop.render(<Foo step={2} />);
219
+ ReactNoop.flushDeferredPri(30);
220
+
221
+ expect(ops).toEqual([
222
+ 'componentWillUpdate', hostSpan,
223
+ 'render',
224
+ 'render sibling',
225
+ ]);
226
+
227
+ ops = [];
228
+
229
+ // This should still be the host span.
230
+ expect(ReactNoop.findInstance(classInstance)).toBe(hostSpan);
231
+
232
+ // When we finally flush the tree it will get committed.
233
+ ReactNoop.flush();
234
+
235
+ const hostDiv = classInstance.div;
236
+
237
+ expect(hostDiv).toBeDefined();
238
+ expect(hostSpan).not.toBe(hostDiv);
239
+
240
+ expect(ops).toEqual([
241
+ 'componentDidUpdate', hostDiv,
242
+ ]);
243
+
244
+ ops = [];
245
+
246
+ // We should now find the new host node.
247
+ expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
248
+
249
+ // Finally we will render to null but not yet commit it.
250
+ ReactNoop.render(<Foo step={3} />);
251
+ ReactNoop.flushDeferredPri(25);
252
+
253
+ expect(ops).toEqual([
254
+ 'componentWillUpdate', hostDiv,
255
+ 'render',
256
+ 'render sibling',
257
+ ]);
258
+
259
+ ops = [];
260
+
261
+ // This should still be the host div since the deletion is not committed.
262
+ expect(ReactNoop.findInstance(classInstance)).toBe(hostDiv);
263
+
264
+ ReactNoop.flush();
265
+
266
+ expect(ops).toEqual([
267
+ 'componentDidUpdate', null,
268
+ ]);
269
+
270
+ // This should still be the host div since the deletion is not committed.
271
+ expect(ReactNoop.findInstance(classInstance)).toBe(null);
272
+ });
273
+
274
+
275
});