@samitouri / QOS-React-2 / commits / e9d0a3ff25

[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', () => {