[Fresh] Track mounted roots via DevTools Hook (#15928)
* Track mounted roots via DevTools Hook * Add helper utilities to the runtime These utilities will likely be needed by all module systems, so let's just put them here. * Wrap more things in __DEV__ * Fix tests to also be DEV-only
Dan Abramov committed
Jun 20, 2019 at 00:12 UTC
e9d0a3ff252ea72b1aa5e8df969304ff5c9aa0a4
5 files changed
+687
-165
packages/react-reconciler/src/ReactFiberHotReloading.js
+34
-14
@@ -38,16 +38,32 @@ export type Family = {|
38
current: any,
39
|};
40
41
-export type HotUpdate = {|
42
- resolveFamily: (any => Family | void) | null,
41
+export type RefreshUpdate = {|
42
staleFamilies: Set<Family>,
43
updatedFamilies: Set<Family>,
44
|};
45
47
-let resolveFamily: (any => Family | void) | null = null;
46
+// Resolves type to a family.
47
+type RefreshHandler = any => Family | void;
48
+
49
+// Used by React Refresh runtime through DevTools Global Hook.
50
+export type SetRefreshHandler = (handler: RefreshHandler | null) => void;
51
+export type ScheduleRefresh = (root: FiberRoot, update: RefreshUpdate) => void;
52
+export type FindHostInstancesForRefresh = (
53
+ root: FiberRoot,
54
+ families: Array<Family>,
55
+) => Set<Instance>;
56
+
57
+let resolveFamily: RefreshHandler | null = null;
58
// $FlowFixMe Flow gets confused by a WeakSet feature check below.
59
let failedBoundaries: WeakSet<Fiber> | null = null;
60
61
+export let setRefreshHandler = (handler: RefreshHandler | null): void => {
62
+ if (__DEV__) {
63
+ resolveFamily = handler;
64
+ }
65
+};
66
+
67
export function resolveFunctionForHotReloading(type: any): any {
68
if (__DEV__) {
69
if (resolveFamily === null) {
@@ -192,7 +208,7 @@ export function isCompatibleFamilyForHotReloading(
208
export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) {
209
if (__DEV__) {
210
if (resolveFamily === null) {
195
- // Not hot reloading.
211
+ // Hot reloading is disabled.
212
return;
213
}
214
if (typeof WeakSet !== 'function') {
@@ -205,12 +221,16 @@ export function markFailedErrorBoundaryForHotReloading(fiber: Fiber) {
221
}
222
}
223
208
-export function scheduleHotUpdate(root: FiberRoot, hotUpdate: HotUpdate): void {
224
+export let scheduleRefresh: ScheduleRefresh = (
225
+ root: FiberRoot,
226
+ update: RefreshUpdate,
227
+): void => {
228
if (__DEV__) {
210
- // TODO: warn if its identity changes over time?
211
- resolveFamily = hotUpdate.resolveFamily;
212
-
213
- const {staleFamilies, updatedFamilies} = hotUpdate;
229
+ if (resolveFamily === null) {
230
+ // Hot reloading is disabled.
231
+ return;
232
+ }
233
+ const {staleFamilies, updatedFamilies} = update;
234
flushPassiveEffects();
235
flushSync(() => {
236
scheduleFibersWithFamiliesRecursively(
@@ -220,7 +240,7 @@ export function scheduleHotUpdate(root: FiberRoot, hotUpdate: HotUpdate): void {
240
);
241
});
242
}
223
-}
243
+};
244
245
function scheduleFibersWithFamiliesRecursively(
246
fiber: Fiber,
@@ -292,10 +312,10 @@ function scheduleFibersWithFamiliesRecursively(
312
}
313
}
314
295
-export function findHostInstancesForHotUpdate(
315
+export let findHostInstancesForRefresh: FindHostInstancesForRefresh = (
316
root: FiberRoot,
317
families: Array<Family>,
298
-): Set<Instance> {
318
+): Set<Instance> => {
319
if (__DEV__) {
320
const hostInstances = new Set();
321
const types = new Set(families.map(family => family.current));
@@ -307,10 +327,10 @@ export function findHostInstancesForHotUpdate(
327
return hostInstances;
328
} else {
329
throw new Error(
310
- 'Did not expect findHostInstancesForHotUpdate to be called in production.',
330
+ 'Did not expect findHostInstancesForRefresh to be called in production.',
331
);
332
}
313
-}
333
+};
334
335
function findHostInstancesForMatchingFibersRecursively(
336
fiber: Fiber,
packages/react-reconciler/src/ReactFiberReconciler.js
+7
-6
@@ -71,8 +71,9 @@ import {Sync} from './ReactFiberExpirationTime';
71
import {revertPassiveEffectsChange} from 'shared/ReactFeatureFlags';
72
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
73
import {
74
- scheduleHotUpdate,
75
- findHostInstancesForHotUpdate,
74
+ scheduleRefresh,
75
+ setRefreshHandler,
76
+ findHostInstancesForRefresh,
77
} from './ReactFiberHotReloading';
78
79
type OpaqueRoot = FiberRoot;
@@ -475,10 +476,6 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
476
477
return injectInternals({
478
...devToolsConfig,
478
- findHostInstancesForHotUpdate: __DEV__
479
- ? findHostInstancesForHotUpdate
480
- : null,
481
- scheduleHotUpdate: __DEV__ ? scheduleHotUpdate : null,
479
overrideHookState,
480
overrideProps,
481
setSuspenseHandler,
@@ -498,5 +495,9 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
495
}
496
return findFiberByHostInstance(instance);
497
},
498
+ // React Refresh
499
+ findHostInstancesForRefresh: __DEV__ ? findHostInstancesForRefresh : null,
500
+ scheduleRefresh: __DEV__ ? scheduleRefresh : null,
501
+ setRefreshHandler: __DEV__ ? setRefreshHandler : null,
502
});
503
}
packages/react-refresh/src/ReactFreshRuntime.js
+376
-70
@@ -7,12 +7,18 @@
7
* @flow
8
*/
9
10
+import type {Instance} from 'react-reconciler/src/ReactFiberHostConfig';
11
+import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
12
import type {
13
Family,
12
- HotUpdate,
14
+ RefreshUpdate,
15
+ ScheduleRefresh,
16
+ FindHostInstancesForRefresh,
17
+ SetRefreshHandler,
18
} from 'react-reconciler/src/ReactFiberHotReloading';
19
20
import {REACT_MEMO_TYPE, REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';
21
+import warningWithoutStack from 'shared/warningWithoutStack';
22
23
type Signature = {|
24
ownKey: string,
@@ -38,10 +44,17 @@ WeakMap<any, Signature> | Map<any, Signature> = new PossiblyWeakMap();
44
const familiesByType: // $FlowIssue
45
WeakMap<any, Family> | Map<any, Family> = new PossiblyWeakMap();
46
41
-// This is cleared on every prepareUpdate() call.
47
+// This is cleared on every performReactRefresh() call.
48
// It is an array of [Family, NextType] tuples.
49
let pendingUpdates: Array<[Family, any]> = [];
50
51
+// This is injected by the renderer via DevTools global hook.
52
+let setRefreshHandler: null | SetRefreshHandler = null;
53
+let scheduleRefresh: null | ScheduleRefresh = null;
54
+let findHostInstancesForRefresh: null | FindHostInstancesForRefresh = null;
55
+
56
+let mountedRoots = new Set();
57
+
58
function computeFullKey(signature: Signature): string {
59
if (signature.fullKey !== null) {
60
return signature.fullKey;
@@ -123,76 +136,132 @@ function resolveFamily(type) {
136
return familiesByType.get(type);
137
}
138
126
-export function prepareUpdate(): HotUpdate | null {
127
- if (pendingUpdates.length === 0) {
128
- return null;
129
- }
139
+export function performReactRefresh(): boolean {
140
+ if (__DEV__) {
141
+ if (pendingUpdates.length === 0) {
142
+ return false;
143
+ }
144
131
- const staleFamilies = new Set();
132
- const updatedFamilies = new Set();
145
+ const staleFamilies = new Set();
146
+ const updatedFamilies = new Set();
147
134
- const updates = pendingUpdates;
135
- pendingUpdates = [];
136
- updates.forEach(([family, nextType]) => {
137
- // Now that we got a real edit, we can create associations
138
- // that will be read by the React reconciler.
139
- const prevType = family.current;
140
- familiesByType.set(prevType, family);
141
- familiesByType.set(nextType, family);
142
- family.current = nextType;
148
+ const updates = pendingUpdates;
149
+ pendingUpdates = [];
150
+ updates.forEach(([family, nextType]) => {
151
+ // Now that we got a real edit, we can create associations
152
+ // that will be read by the React reconciler.
153
+ const prevType = family.current;
154
+ familiesByType.set(prevType, family);
155
+ familiesByType.set(nextType, family);
156
+ family.current = nextType;
157
144
- // Determine whether this should be a re-render or a re-mount.
145
- if (canPreserveStateBetween(prevType, nextType)) {
146
- updatedFamilies.add(family);
147
- } else {
148
- staleFamilies.add(family);
158
+ // Determine whether this should be a re-render or a re-mount.
159
+ if (canPreserveStateBetween(prevType, nextType)) {
160
+ updatedFamilies.add(family);
161
+ } else {
162
+ staleFamilies.add(family);
163
+ }
164
+ });
165
+
166
+ const update: RefreshUpdate = {
167
+ updatedFamilies,
168
+ staleFamilies,
169
+ };
170
+
171
+ if (typeof setRefreshHandler !== 'function') {
172
+ warningWithoutStack(
173
+ false,
174
+ 'Could not find the setRefreshHandler() implementation. ' +
175
+ 'This likely means that injectIntoGlobalHook() was either ' +
176
+ 'called before the global DevTools hook was set up, or after the ' +
177
+ 'renderer has already initialized. Please file an issue with a reproducing case.',
178
+ );
179
+ return false;
180
+ }
181
+
182
+ if (typeof scheduleRefresh !== 'function') {
183
+ warningWithoutStack(
184
+ false,
185
+ 'Could not find the scheduleRefresh() implementation. ' +
186
+ 'This likely means that injectIntoGlobalHook() was either ' +
187
+ 'called before the global DevTools hook was set up, or after the ' +
188
+ 'renderer has already initialized. Please file an issue with a reproducing case.',
189
+ );
190
+ return false;
191
}
150
- });
192
+ const scheduleRefreshForRoot = scheduleRefresh;
193
+
194
+ // Even if there are no roots, set the handler on first update.
195
+ // This ensures that if *new* roots are mounted, they'll use the resolve handler.
196
+ setRefreshHandler(resolveFamily);
197
152
- return {
153
- resolveFamily,
154
- updatedFamilies,
155
- staleFamilies,
156
- };
198
+ let didError = false;
199
+ let firstError = null;
200
+ mountedRoots.forEach(root => {
201
+ try {
202
+ scheduleRefreshForRoot(root, update);
203
+ } catch (err) {
204
+ if (!didError) {
205
+ didError = true;
206
+ firstError = err;
207
+ }
208
+ // Keep trying other roots.
209
+ }
210
+ });
211
+ if (didError) {
212
+ throw firstError;
213
+ }
214
+ return true;
215
+ } else {
216
+ throw new Error(
217
+ 'Unexpected call to React Refresh in a production environment.',
218
+ );
219
+ }
220
}
221
222
export function register(type: any, id: string): void {
160
- if (type === null) {
161
- return;
162
- }
163
- if (typeof type !== 'function' && typeof type !== 'object') {
164
- return;
165
- }
223
+ if (__DEV__) {
224
+ if (type === null) {
225
+ return;
226
+ }
227
+ if (typeof type !== 'function' && typeof type !== 'object') {
228
+ return;
229
+ }
230
167
- // This can happen in an edge case, e.g. if we register
168
- // return value of a HOC but it returns a cached component.
169
- // Ignore anything but the first registration for each type.
170
- if (allTypes.has(type)) {
171
- return;
172
- }
173
- allTypes.add(type);
231
+ // This can happen in an edge case, e.g. if we register
232
+ // return value of a HOC but it returns a cached component.
233
+ // Ignore anything but the first registration for each type.
234
+ if (allTypes.has(type)) {
235
+ return;
236
+ }
237
+ allTypes.add(type);
238
175
- // Create family or remember to update it.
176
- // None of this bookkeeping affects reconciliation
177
- // until the first prepareUpdate() call above.
178
- let family = allFamiliesByID.get(id);
179
- if (family === undefined) {
180
- family = {current: type};
181
- allFamiliesByID.set(id, family);
182
- } else {
183
- pendingUpdates.push([family, type]);
184
- }
239
+ // Create family or remember to update it.
240
+ // None of this bookkeeping affects reconciliation
241
+ // until the first prepareUpdate() call above.
242
+ let family = allFamiliesByID.get(id);
243
+ if (family === undefined) {
244
+ family = {current: type};
245
+ allFamiliesByID.set(id, family);
246
+ } else {
247
+ pendingUpdates.push([family, type]);
248
+ }
249
186
- // Visit inner types because we might not have registered them.
187
- if (typeof type === 'object' && type !== null) {
188
- switch (type.$$typeof) {
189
- case REACT_FORWARD_REF_TYPE:
190
- register(type.render, id + '$render');
191
- break;
192
- case REACT_MEMO_TYPE:
193
- register(type.type, id + '$type');
194
- break;
250
+ // Visit inner types because we might not have registered them.
251
+ if (typeof type === 'object' && type !== null) {
252
+ switch (type.$$typeof) {
253
+ case REACT_FORWARD_REF_TYPE:
254
+ register(type.render, id + '$render');
255
+ break;
256
+ case REACT_MEMO_TYPE:
257
+ register(type.type, id + '$type');
258
+ break;
259
+ }
260
}
261
+ } else {
262
+ throw new Error(
263
+ 'Unexpected call to React Refresh in a production environment.',
264
+ );
265
}
266
}
267
@@ -202,23 +271,260 @@ export function setSignature(
271
forceReset?: boolean = false,
272
getCustomHooks?: () => Array<Function>,
273
): void {
205
- allSignaturesByType.set(type, {
206
- forceReset,
207
- ownKey: key,
208
- fullKey: null,
209
- getCustomHooks: getCustomHooks || (() => []),
210
- });
274
+ if (__DEV__) {
275
+ allSignaturesByType.set(type, {
276
+ forceReset,
277
+ ownKey: key,
278
+ fullKey: null,
279
+ getCustomHooks: getCustomHooks || (() => []),
280
+ });
281
+ } else {
282
+ throw new Error(
283
+ 'Unexpected call to React Refresh in a production environment.',
284
+ );
285
+ }
286
}
287
288
// This is lazily called during first render for a type.
289
// It captures Hook list at that time so inline requires don't break comparisons.
290
export function collectCustomHooksForSignature(type: any) {
216
- const signature = allSignaturesByType.get(type);
217
- if (signature !== undefined) {
218
- computeFullKey(signature);
291
+ if (__DEV__) {
292
+ const signature = allSignaturesByType.get(type);
293
+ if (signature !== undefined) {
294
+ computeFullKey(signature);
295
+ }
296
+ } else {
297
+ throw new Error(
298
+ 'Unexpected call to React Refresh in a production environment.',
299
+ );
300
}
301
}
302
303
export function getFamilyByID(id: string): Family | void {
223
- return allFamiliesByID.get(id);
304
+ if (__DEV__) {
305
+ return allFamiliesByID.get(id);
306
+ } else {
307
+ throw new Error(
308
+ 'Unexpected call to React Refresh in a production environment.',
309
+ );
310
+ }
311
+}
312
+
313
+export function findAffectedHostInstances(
314
+ families: Array<Family>,
315
+): Set<Instance> {
316
+ if (__DEV__) {
317
+ if (typeof findHostInstancesForRefresh !== 'function') {
318
+ warningWithoutStack(
319
+ false,
320
+ 'Could not find the findHostInstancesForRefresh() implementation. ' +
321
+ 'This likely means that injectIntoGlobalHook() was either ' +
322
+ 'called before the global DevTools hook was set up, or after the ' +
323
+ 'renderer has already initialized. Please file an issue with a reproducing case.',
324
+ );
325
+ return new Set();
326
+ }
327
+ const findInstances = findHostInstancesForRefresh;
328
+ let affectedInstances = new Set();
329
+ mountedRoots.forEach(root => {
330
+ const instancesForRoot = findInstances(root, families);
331
+ instancesForRoot.forEach(inst => {
332
+ affectedInstances.add(inst);
333
+ });
334
+ });
335
+ return affectedInstances;
336
+ } else {
337
+ throw new Error(
338
+ 'Unexpected call to React Refresh in a production environment.',
339
+ );
340
+ }
341
+}
342
+
343
+export function injectIntoGlobalHook(globalObject: any): void {
344
+ if (__DEV__) {
345
+ // For React Native, the global hook will be set up by require('react-devtools-core').
346
+ // That code will run before us. So we need to monkeypatch functions on existing hook.
347
+
348
+ // For React Web, the global hook will be set up by the extension.
349
+ // This will also run before us.
350
+ let hook = globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__;
351
+ if (hook === undefined) {
352
+ // However, if there is no DevTools extension, we'll need to set up the global hook ourselves.
353
+ // Note that in this case it's important that renderer code runs *after* this method call.
354
+ // Otherwise, the renderer will think that there is no global hook, and won't do the injection.
355
+ globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
356
+ supportsFiber: true,
357
+ inject() {},
358
+ onCommitFiberRoot(id: mixed, root: FiberRoot) {},
359
+ onCommitFiberUnmount() {},
360
+ };
361
+ }
362
+
363
+ // Here, we just want to get a reference to scheduleRefresh.
364
+ const oldInject = hook.inject;
365
+ hook.inject = function(injected) {
366
+ findHostInstancesForRefresh = ((injected: any)
367
+ .findHostInstancesForRefresh: FindHostInstancesForRefresh);
368
+ scheduleRefresh = ((injected: any).scheduleRefresh: ScheduleRefresh);
369
+ setRefreshHandler = ((injected: any)
370
+ .setRefreshHandler: SetRefreshHandler);
371
+ return oldInject.apply(this, arguments);
372
+ };
373
+
374
+ // We also want to track currently mounted roots.
375
+ const oldOnCommitFiberRoot = hook.onCommitFiberRoot;
376
+ hook.onCommitFiberRoot = function(id: mixed, root: FiberRoot) {
377
+ const current = root.current;
378
+ const alternate = current.alternate;
379
+
380
+ // We need to determine whether this root has just (un)mounted.
381
+ // This logic is copy-pasted from similar logic in the DevTools backend.
382
+ // If this breaks with some refactoring, you'll want to update DevTools too.
383
+
384
+ if (alternate !== null) {
385
+ const wasMounted =
386
+ alternate.memoizedState != null &&
387
+ alternate.memoizedState.element != null;
388
+ const isMounted =
389
+ current.memoizedState != null &&
390
+ current.memoizedState.element != null;
391
+
392
+ if (!wasMounted && isMounted) {
393
+ // Mount a new root.
394
+ mountedRoots.add(root);
395
+ } else if (wasMounted && isMounted) {
396
+ // Update an existing root.
397
+ // This doesn't affect our mounted root Set.
398
+ } else if (wasMounted && !isMounted) {
399
+ // Unmount an existing root.
400
+ mountedRoots.delete(root);
401
+ }
402
+ } else {
403
+ // Mount a new root.
404
+ mountedRoots.add(root);
405
+ }
406
+
407
+ return oldOnCommitFiberRoot.apply(this, arguments);
408
+ };
409
+ } else {
410
+ throw new Error(
411
+ 'Unexpected call to React Refresh in a production environment.',
412
+ );
413
+ }
414
+}
415
+
416
+// Exposed for testing.
417
+export function _getMountedRootCount() {
418
+ if (__DEV__) {
419
+ return mountedRoots.size;
420
+ } else {
421
+ throw new Error(
422
+ 'Unexpected call to React Refresh in a production environment.',
423
+ );
424
+ }
425
+}
426
+
427
+// This is a wrapper over more primitive functions for setting signature.
428
+// Signatures let us decide whether the Hook order has changed on refresh.
429
+//
430
+// This function is intended to be used as a transform target, e.g.:
431
+// var _s = createSignatureFunctionForTransform()
432
+//
433
+// function Hello() {
434
+// const [foo, setFoo] = useState(0);
435
+// const value = useCustomHook();
436
+// _s(); /* Second call triggers collecting the custom Hook list.
437
+// * This doesn't happen during the module evaluation because we
438
+// * don't want to change the module order with inline requires.
439
+// * Next calls are noops. */
440
+// return <h1>Hi</h1>;
441
+// }
442
+//
443
+// /* First call specifies the signature: */
444
+// _s(
445
+// Hello,
446
+// 'useState{[foo, setFoo]}(0)',
447
+// () => [useCustomHook], /* Lazy to avoid triggering inline requires */
448
+// );
449
+export function createSignatureFunctionForTransform() {
450
+ if (__DEV__) {
451
+ let call = 0;
452
+ let savedType;
453
+ let hasCustomHooks;
454
+ return function<T>(
455
+ type: T,
456
+ key: string,
457
+ forceReset?: boolean,
458
+ getCustomHooks?: () => Array<Function>,
459
+ ): T {
460
+ switch (call++) {
461
+ case 0:
462
+ savedType = type;
463
+ hasCustomHooks = typeof getCustomHooks === 'function';
464
+ setSignature(type, key, forceReset, getCustomHooks);
465
+ break;
466
+ case 1:
467
+ if (hasCustomHooks) {
468
+ collectCustomHooksForSignature(savedType);
469
+ }
470
+ break;
471
+ }
472
+ return type;
473
+ };
474
+ } else {
475
+ throw new Error(
476
+ 'Unexpected call to React Refresh in a production environment.',
477
+ );
478
+ }
479
+}
480
+
481
+export function isLikelyComponentType(type: any): boolean {
482
+ if (__DEV__) {
483
+ switch (typeof type) {
484
+ case 'function': {
485
+ // First, deal with classes.
486
+ if (type.prototype != null) {
487
+ if (type.prototype.isReactComponent) {
488
+ // React class.
489
+ return true;
490
+ }
491
+ const ownNames = Object.getOwnPropertyNames(type.prototype);
492
+ if (ownNames.length > 1 || ownNames[0] !== 'constructor') {
493
+ // This looks like a class.
494
+ return false;
495
+ }
496
+ // eslint-disable-next-line no-proto
497
+ if (type.prototype.__proto__ !== Object.prototype) {
498
+ // It has a superclass.
499
+ return false;
500
+ }
501
+ // Pass through.
502
+ // This looks like a regular function with empty prototype.
503
+ }
504
+ // For plain functions and arrows, use name as a heuristic.
505
+ const name = type.name || type.displayName;
506
+ return typeof name === 'string' && /^[A-Z]/.test(name);
507
+ }
508
+ case 'object': {
509
+ if (type != null) {
510
+ switch (type.$$typeof) {
511
+ case REACT_FORWARD_REF_TYPE:
512
+ case REACT_MEMO_TYPE:
513
+ // Definitely React components.
514
+ return true;
515
+ default:
516
+ return false;
517
+ }
518
+ }
519
+ return false;
520
+ }
521
+ default: {
522
+ return false;
523
+ }
524
+ }
525
+ } else {
526
+ throw new Error(
527
+ 'Unexpected call to React Refresh in a production environment.',
528
+ );
529
+ }
530
}
packages/react-refresh/src/__tests__/ReactFresh-test.js
+250
-28
@@ -16,38 +16,34 @@ let ReactDOM;
16
let ReactFreshRuntime;
17
let Scheduler;
18
let act;
19
+let createReactClass;
20
21
describe('ReactFresh', () => {
22
let container;
22
- let lastRoot;
23
- let findHostInstancesForHotUpdate;
24
- let scheduleHotUpdate;
23
24
beforeEach(() => {
27
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
28
- supportsFiber: true,
29
- inject: injected => {
30
- scheduleHotUpdate = injected.scheduleHotUpdate;
31
- findHostInstancesForHotUpdate = injected.findHostInstancesForHotUpdate;
32
- },
33
- onCommitFiberRoot: (id, root) => {
34
- lastRoot = root;
35
- },
36
- onCommitFiberUnmount: () => {},
37
- };
38
-
39
- jest.resetModules();
40
- React = require('react');
41
- ReactDOM = require('react-dom');
42
- ReactFreshRuntime = require('react-refresh/runtime');
43
- Scheduler = require('scheduler');
44
- act = require('react-dom/test-utils').act;
45
- container = document.createElement('div');
46
- document.body.appendChild(container);
25
+ if (__DEV__) {
26
+ jest.resetModules();
27
+ React = require('react');
28
+ ReactFreshRuntime = require('react-refresh/runtime');
29
+ ReactFreshRuntime.injectIntoGlobalHook(global);
30
+ ReactDOM = require('react-dom');
31
+ Scheduler = require('scheduler');
32
+ act = require('react-dom/test-utils').act;
33
+ createReactClass = require('create-react-class/factory')(
34
+ React.Component,
35
+ React.isValidElement,
36
+ new React.Component().updater,
37
+ );
38
+ container = document.createElement('div');
39
+ document.body.appendChild(container);
40
+ }
41
});
42
43
afterEach(() => {
50
- document.body.removeChild(container);
44
+ if (__DEV__) {
45
+ document.body.removeChild(container);
46
+ }
47
});
48
49
function prepare(version) {
@@ -65,8 +61,7 @@ describe('ReactFresh', () => {
61
62
function patch(version) {
63
const Component = version();
68
- const hotUpdate = ReactFreshRuntime.prepareUpdate();
69
- scheduleHotUpdate(lastRoot, hotUpdate);
64
+ ReactFreshRuntime.performReactRefresh();
65
return Component;
66
}
67
@@ -1919,7 +1914,7 @@ describe('ReactFresh', () => {
1914
1915
ReactDOM.render(tree, container);
1916
const elements = container.querySelectorAll('section');
1922
- // Each tree above products exactly three <section> elements:
1917
+ // Each tree above produces exactly three <section> elements:
1918
expect(elements.length).toBe(3);
1919
elements.forEach(el => {
1920
expect(el.dataset.color).toBe('blue');
@@ -3037,11 +3032,238 @@ describe('ReactFresh', () => {
3032
3033
function testFindHostInstancesForFamilies(families, expectedNodes) {
3034
const foundInstances = Array.from(
3040
- findHostInstancesForHotUpdate(lastRoot, families),
3035
+ ReactFreshRuntime.findAffectedHostInstances(families),
3036
);
3037
expect(foundInstances.length).toEqual(expectedNodes.length);
3038
foundInstances.forEach((node, i) => {
3039
expect(node).toBe(expectedNodes[i]);
3040
});
3041
}
3042
+
3043
+ it('can update multiple roots independently', () => {
3044
+ if (__DEV__) {
3045
+ // Declare the first version.
3046
+ const HelloV1 = () => {
3047
+ const [val, setVal] = React.useState(0);
3048
+ return (
3049
+ <p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
3050
+ {val}
3051
+ </p>
3052
+ );
3053
+ };
3054
+ __register__(HelloV1, 'Hello');
3055
+
3056
+ // Perform a hot update before any roots exist.
3057
+ const HelloV2 = () => {
3058
+ const [val, setVal] = React.useState(0);
3059
+ return (
3060
+ <p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
3061
+ {val}
3062
+ </p>
3063
+ );
3064
+ };
3065
+ __register__(HelloV2, 'Hello');
3066
+ ReactFreshRuntime.performReactRefresh();
3067
+
3068
+ // Mount three roots.
3069
+ let cont1 = document.createElement('div');
3070
+ let cont2 = document.createElement('div');
3071
+ let cont3 = document.createElement('div');
3072
+ document.body.appendChild(cont1);
3073
+ document.body.appendChild(cont2);
3074
+ document.body.appendChild(cont3);
3075
+ try {
3076
+ ReactDOM.render(<HelloV1 id={1} />, cont1);
3077
+ ReactDOM.render(<HelloV2 id={2} />, cont2);
3078
+ ReactDOM.render(<HelloV1 id={3} />, cont3);
3079
+
3080
+ // Expect we see the V2 color.
3081
+ expect(cont1.firstChild.style.color).toBe('red');
3082
+ expect(cont2.firstChild.style.color).toBe('red');
3083
+ expect(cont3.firstChild.style.color).toBe('red');
3084
+ expect(cont1.firstChild.textContent).toBe('0');
3085
+ expect(cont2.firstChild.textContent).toBe('0');
3086
+ expect(cont3.firstChild.textContent).toBe('0');
3087
+
3088
+ // Bump the state for each of them.
3089
+ act(() => {
3090
+ cont1.firstChild.dispatchEvent(
3091
+ new MouseEvent('click', {bubbles: true}),
3092
+ );
3093
+ cont2.firstChild.dispatchEvent(
3094
+ new MouseEvent('click', {bubbles: true}),
3095
+ );
3096
+ cont3.firstChild.dispatchEvent(
3097
+ new MouseEvent('click', {bubbles: true}),
3098
+ );
3099
+ });
3100
+ expect(cont1.firstChild.style.color).toBe('red');
3101
+ expect(cont2.firstChild.style.color).toBe('red');
3102
+ expect(cont3.firstChild.style.color).toBe('red');
3103
+ expect(cont1.firstChild.textContent).toBe('1');
3104
+ expect(cont2.firstChild.textContent).toBe('1');
3105
+ expect(cont3.firstChild.textContent).toBe('1');
3106
+
3107
+ // Perform another hot update.
3108
+ const HelloV3 = () => {
3109
+ const [val, setVal] = React.useState(0);
3110
+ return (
3111
+ <p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
3112
+ {val}
3113
+ </p>
3114
+ );
3115
+ };
3116
+ __register__(HelloV3, 'Hello');
3117
+ ReactFreshRuntime.performReactRefresh();
3118
+
3119
+ // It should affect all roots.
3120
+ expect(cont1.firstChild.style.color).toBe('green');
3121
+ expect(cont2.firstChild.style.color).toBe('green');
3122
+ expect(cont3.firstChild.style.color).toBe('green');
3123
+ expect(cont1.firstChild.textContent).toBe('1');
3124
+ expect(cont2.firstChild.textContent).toBe('1');
3125
+ expect(cont3.firstChild.textContent).toBe('1');
3126
+
3127
+ // Unmount the second root.
3128
+ ReactDOM.unmountComponentAtNode(cont2);
3129
+ // Make the first root throw and unmount on hot update.
3130
+ const HelloV4 = ({id}) => {
3131
+ if (id === 1) {
3132
+ throw new Error('Oops.');
3133
+ }
3134
+ const [val, setVal] = React.useState(0);
3135
+ return (
3136
+ <p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
3137
+ {val}
3138
+ </p>
3139
+ );
3140
+ };
3141
+ __register__(HelloV4, 'Hello');
3142
+ expect(() => {
3143
+ ReactFreshRuntime.performReactRefresh();
3144
+ }).toThrow('Oops.');
3145
+
3146
+ // Still, we expect the last root to be updated.
3147
+ expect(cont1.innerHTML).toBe('');
3148
+ expect(cont2.innerHTML).toBe('');
3149
+ expect(cont3.firstChild.style.color).toBe('orange');
3150
+ expect(cont3.firstChild.textContent).toBe('1');
3151
+ } finally {
3152
+ document.body.removeChild(cont1);
3153
+ document.body.removeChild(cont2);
3154
+ document.body.removeChild(cont3);
3155
+ }
3156
+ }
3157
+ });
3158
+
3159
+ // Module runtimes can use this to decide whether
3160
+ // to propagate an update up to the modules that imported it,
3161
+ // or to stop at the current module because it's a component.
3162
+ // This can't and doesn't need to be 100% precise.
3163
+ it('can detect likely component types', () => {
3164
+ function useTheme() {}
3165
+ function Widget() {}
3166
+
3167
+ if (__DEV__) {
3168
+ expect(ReactFreshRuntime.isLikelyComponentType(false)).toBe(false);
3169
+ expect(ReactFreshRuntime.isLikelyComponentType(null)).toBe(false);
3170
+ expect(ReactFreshRuntime.isLikelyComponentType('foo')).toBe(false);
3171
+
3172
+ // We need to hit a balance here.
3173
+ // If we lean towards assuming everything is a component,
3174
+ // editing modules that export plain functions won't trigger
3175
+ // a proper reload because we will bottle up the update.
3176
+ // So we're being somewhat conservative.
3177
+ expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false);
3178
+ expect(ReactFreshRuntime.isLikelyComponentType(function() {})).toBe(
3179
+ false,
3180
+ );
3181
+ expect(
3182
+ ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}),
3183
+ ).toBe(false);
3184
+ const loadUser = () => {};
3185
+ expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false);
3186
+ const useStore = () => {};
3187
+ expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false);
3188
+ expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false);
3189
+
3190
+ // These seem like function components.
3191
+ let Button = () => {};
3192
+ expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true);
3193
+ expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true);
3194
+ let anon = (() => () => {})();
3195
+ anon.displayName = 'Foo';
3196
+ expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true);
3197
+
3198
+ // These seem like class components.
3199
+ class Btn extends React.Component {}
3200
+ class PureBtn extends React.PureComponent {}
3201
+ expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true);
3202
+ expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true);
3203
+ expect(
3204
+ ReactFreshRuntime.isLikelyComponentType(
3205
+ createReactClass({render() {}}),
3206
+ ),
3207
+ ).toBe(true);
3208
+
3209
+ // These don't.
3210
+ class Figure {
3211
+ move() {}
3212
+ }
3213
+ expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false);
3214
+ class Point extends Figure {}
3215
+ expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false);
3216
+
3217
+ // Run the same tests without Babel.
3218
+ // This tests real arrow functions and classes, as implemented in Node.
3219
+
3220
+ // eslint-disable-next-line no-new-func
3221
+ new Function(
3222
+ 'global',
3223
+ 'React',
3224
+ 'ReactFreshRuntime',
3225
+ 'expect',
3226
+ 'createReactClass',
3227
+ `
3228
+ expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false);
3229
+ expect(ReactFreshRuntime.isLikelyComponentType(function() {})).toBe(false);
3230
+ expect(
3231
+ ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}),
3232
+ ).toBe(false);
3233
+ const loadUser = () => {};
3234
+ expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false);
3235
+ const useStore = () => {};
3236
+ expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false);
3237
+ function useTheme() {}
3238
+ expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false);
3239
+
3240
+ // These seem like function components.
3241
+ let Button = () => {};
3242
+ expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true);
3243
+ function Widget() {}
3244
+ expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true);
3245
+ let anon = (() => () => {})();
3246
+ anon.displayName = 'Foo';
3247
+ expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true);
3248
+
3249
+ // These seem like class components.
3250
+ class Btn extends React.Component {}
3251
+ class PureBtn extends React.PureComponent {}
3252
+ expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true);
3253
+ expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true);
3254
+ expect(
3255
+ ReactFreshRuntime.isLikelyComponentType(createReactClass({render() {}})),
3256
+ ).toBe(true);
3257
+
3258
+ // These don't.
3259
+ class Figure {
3260
+ move() {}
3261
+ }
3262
+ expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false);
3263
+ class Point extends Figure {}
3264
+ expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false);
3265
+ `,
3266
+ )(global, React, ReactFreshRuntime, expect, createReactClass);
3267
+ }
3268
+ });
3269
});
packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+20
-47
@@ -21,32 +21,27 @@ let freshPlugin = require('react-refresh/babel');
21
22
describe('ReactFreshIntegration', () => {
23
let container;
24
- let lastRoot;
25
- let scheduleHotUpdate;
24
25
beforeEach(() => {
28
- global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
29
- supportsFiber: true,
30
- inject: injected => {
31
- scheduleHotUpdate = injected.scheduleHotUpdate;
32
- },
33
- onCommitFiberRoot: (id, root) => {
34
- lastRoot = root;
35
- },
36
- onCommitFiberUnmount: () => {},
37
- };
38
-
39
- jest.resetModules();
40
- React = require('react');
41
- ReactDOM = require('react-dom');
42
- ReactFreshRuntime = require('react-refresh/runtime');
43
- act = require('react-dom/test-utils').act;
44
- container = document.createElement('div');
45
- document.body.appendChild(container);
26
+ if (__DEV__) {
27
+ jest.resetModules();
28
+ React = require('react');
29
+ ReactFreshRuntime = require('react-refresh/runtime');
30
+ ReactFreshRuntime.injectIntoGlobalHook(global);
31
+ ReactDOM = require('react-dom');
32
+ act = require('react-dom/test-utils').act;
33
+ container = document.createElement('div');
34
+ document.body.appendChild(container);
35
+ }
36
});
37
38
afterEach(() => {
49
- document.body.removeChild(container);
39
+ if (__DEV__) {
40
+ ReactDOM.unmountComponentAtNode(container);
41
+ // Ensure we don't leak memory by holding onto dead roots.
42
+ expect(ReactFreshRuntime._getMountedRootCount()).toBe(0);
43
+ document.body.removeChild(container);
44
+ }
45
});
46
47
describe('with compiled destructuring', () => {
@@ -87,15 +82,15 @@ describe('ReactFreshIntegration', () => {
82
ReactDOM.render(<Component />, container);
83
});
84
// Module initialization shouldn't be counted as a hot update.
90
- expect(ReactFreshRuntime.prepareUpdate()).toBe(null);
85
+ expect(ReactFreshRuntime.performReactRefresh()).toBe(false);
86
}
87
88
function patch(source) {
89
execute(source);
95
- const hotUpdate = ReactFreshRuntime.prepareUpdate();
90
act(() => {
97
- scheduleHotUpdate(lastRoot, hotUpdate);
91
+ expect(ReactFreshRuntime.performReactRefresh()).toBe(true);
92
});
93
+ expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
94
}
95
96
function __register__(type, id) {
@@ -103,29 +98,7 @@ describe('ReactFreshIntegration', () => {
98
}
99
100
function __signature__() {
106
- let call = 0;
107
- let savedType;
108
- let hasCustomHooks;
109
- return function(type, key, forceReset, getCustomHooks) {
110
- switch (call++) {
111
- case 0:
112
- savedType = type;
113
- hasCustomHooks = typeof getCustomHooks === 'function';
114
- ReactFreshRuntime.setSignature(
115
- type,
116
- key,
117
- forceReset,
118
- getCustomHooks,
119
- );
120
- break;
121
- case 1:
122
- if (hasCustomHooks) {
123
- ReactFreshRuntime.collectCustomHooksForSignature(savedType);
124
- }
125
- break;
126
- }
127
- return type;
128
- };
101
+ return ReactFreshRuntime.createSignatureFunctionForTransform();
102
}
103
104
it('reloads function declarations', () => {