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

[Fresh] Make transform resilient to plugin order (#15883)

* Run Fresh tests in two modes: with and without destructuring Destructuring transform messes up the way we collect signatures for Hooks. This adds failing tests. * Extract collecting calls to Hooks into a separate visitor This introduces a bit of a perf penalty but makes our plugin resilient to presence of destructuring transform and their order. Fixes new tests. * Inline some logic into the call visitor

Dan Abramov committed Jun 14, 2019 at 14:57 UTC de7a09c1e0425f8719fa9c27a583a28ae5d791cb
2 files changed +1073 -1053
packages/react-fresh/src/ReactFreshBabelPlugin.js
+63 -62
@@ -163,36 +163,6 @@ export default function(babel) {
163 return false;
164 }
165
166 - let hookCalls = new WeakMap();
167 -
168 - function recordHookCall(functionNode, hookCallPath, hookName) {
169 - if (!hookCalls.has(functionNode)) {
170 - hookCalls.set(functionNode, []);
171 - }
172 - let hookCallsForFn = hookCalls.get(functionNode);
173 - let key = '';
174 - if (hookCallPath.parent.type === 'VariableDeclarator') {
175 - // TODO: if there is no LHS, consider some other heuristic.
176 - key = hookCallPath.parentPath.get('id').getSource();
177 - }
178 -
179 - // Some built-in Hooks reset on edits to arguments.
180 - const args = hookCallPath.get('arguments');
181 - if (hookName === 'useState' && args.length > 0) {
182 - // useState second argument is initial state.
183 - key += '(' + args[0].getSource() + ')';
184 - } else if (hookName === 'useReducer' && args.length > 1) {
185 - // useReducer second argument is initial state.
186 - key += '(' + args[1].getSource() + ')';
187 - }
188 -
189 - hookCallsForFn.push({
190 - name: hookName,
191 - callee: hookCallPath.node.callee,
192 - key,
193 - });
194 - }
195 -
166 function isBuiltinHook(hookName) {
167 switch (hookName) {
168 case 'useState':
@@ -299,9 +269,64 @@ export default function(babel) {
269
270 let seenForRegistration = new WeakSet();
271 let seenForSignature = new WeakSet();
302 - let seenForHookCalls = new WeakSet();
272 let seenForOutro = new WeakSet();
273
274 + let hookCalls = new WeakMap();
275 + const HookCallsVisitor = {
276 + CallExpression(path) {
277 + const node = path.node;
278 + const callee = node.callee;
279 +
280 + // Note: this visitor MUST NOT mutate the tree in any way.
281 + // It runs early in a separate traversal and should be very fast.
282 +
283 + let name = null;
284 + switch (callee.type) {
285 + case 'Identifier':
286 + name = callee.name;
287 + break;
288 + case 'MemberExpression':
289 + name = callee.property.name;
290 + break;
291 + }
292 + if (name === null || !/^use[A-Z]/.test(name)) {
293 + return;
294 + }
295 + const fnScope = path.scope.getFunctionParent();
296 + if (fnScope === null) {
297 + return;
298 + }
299 +
300 + // This is a Hook call. Record it.
301 + const fnNode = fnScope.block;
302 + if (!hookCalls.has(fnNode)) {
303 + hookCalls.set(fnNode, []);
304 + }
305 + let hookCallsForFn = hookCalls.get(fnNode);
306 + let key = '';
307 + if (path.parent.type === 'VariableDeclarator') {
308 + // TODO: if there is no LHS, consider some other heuristic.
309 + key = path.parentPath.get('id').getSource();
310 + }
311 +
312 + // Some built-in Hooks reset on edits to arguments.
313 + const args = path.get('arguments');
314 + if (name === 'useState' && args.length > 0) {
315 + // useState second argument is initial state.
316 + key += '(' + args[0].getSource() + ')';
317 + } else if (name === 'useReducer' && args.length > 1) {
318 + // useReducer second argument is initial state.
319 + key += '(' + args[1].getSource() + ')';
320 + }
321 +
322 + hookCallsForFn.push({
323 + callee: path.node.callee,
324 + name,
325 + key,
326 + });
327 + },
328 + };
329 +
330 return {
331 visitor: {
332 ExportDefaultDeclaration(path) {
@@ -617,38 +642,14 @@ export default function(babel) {
642 },
643 );
644 },
620 - CallExpression(path) {
621 - const node = path.node;
622 - const callee = node.callee;
623 -
624 - let name = null;
625 - switch (callee.type) {
626 - case 'Identifier':
627 - name = callee.name;
628 - break;
629 - case 'MemberExpression':
630 - name = callee.property.name;
631 - break;
632 - }
633 - if (name === null || !/^use[A-Z]/.test(name)) {
634 - return;
635 - }
636 -
637 - // Make sure we're not recording the same calls twice.
638 - // This can happen if another Babel plugin replaces parents.
639 - if (seenForHookCalls.has(node)) {
640 - return;
641 - }
642 - seenForHookCalls.add(node);
643 - // Don't mutate the tree above this point.
644 -
645 - const fn = path.scope.getFunctionParent();
646 - if (fn === null) {
647 - return;
648 - }
649 - recordHookCall(fn.block, path, name);
650 - },
645 Program: {
646 + enter(path) {
647 + // This is a separate early visitor because we need to collect Hook calls
648 + // and "const [foo, setFoo] = ..." signatures before the destructuring
649 + // transform mangles them. This extra traversal is not ideal for perf,
650 + // but it's the best we can do until we stop transpiling destructuring.
651 + path.traverse(HookCallsVisitor);
652 + },
653 exit(path) {
654 const registrations = registrationsByProgramPath.get(path);
655 if (registrations === undefined) {
packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js
+1010 -991
@@ -49,937 +49,509 @@ describe('ReactFreshIntegration', () => {
49 document.body.removeChild(container);
50 });
51
52 - function execute(source) {
53 - const compiled = babel.transform(source, {
54 - babelrc: false,
55 - presets: ['react'],
56 - plugins: [freshPlugin, 'transform-es2015-modules-commonjs'],
57 - }).code;
58 - const exportsObj = {};
59 - // eslint-disable-next-line no-new-func
60 - new Function(
61 - 'global',
62 - 'React',
63 - 'exports',
64 - '__register__',
65 - '__signature__',
66 - compiled,
67 - )(global, React, exportsObj, __register__, __signature__);
68 - return exportsObj.default;
69 - }
70 -
71 - function render(source) {
72 - const Component = execute(source);
73 - act(() => {
74 - ReactDOM.render(<Component />, container);
75 - });
76 - // Module initialization shouldn't be counted as a hot update.
77 - expect(ReactFreshRuntime.prepareUpdate()).toBe(null);
78 - }
79 -
80 - function patch(source) {
81 - execute(source);
82 - const hotUpdate = ReactFreshRuntime.prepareUpdate();
83 - act(() => {
84 - scheduleHotUpdate(lastRoot, hotUpdate);
85 - });
86 - }
87 -
88 - function __register__(type, id) {
89 - ReactFreshRuntime.register(type, id);
90 - }
91 -
92 - function __signature__() {
93 - let call = 0;
94 - let savedType;
95 - let hasCustomHooks;
96 - return function(type, key, forceReset, getCustomHooks) {
97 - switch (call++) {
98 - case 0:
99 - savedType = type;
100 - hasCustomHooks = typeof getCustomHooks === 'function';
101 - ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
102 - break;
103 - case 1:
104 - if (hasCustomHooks) {
105 - ReactFreshRuntime.collectCustomHooksForSignature(savedType);
106 - }
107 - break;
108 - }
109 - return type;
110 - };
111 - }
112 -
113 - it('reloads function declarations', () => {
114 - if (__DEV__) {
115 - render(`
116 - function Parent() {
117 - return <Child prop="A" />;
118 - };
119 -
120 - function Child({prop}) {
121 - return <h1>{prop}1</h1>;
122 - };
123 -
124 - export default Parent;
125 - `);
126 - const el = container.firstChild;
127 - expect(el.textContent).toBe('A1');
128 - patch(`
129 - function Parent() {
130 - return <Child prop="B" />;
131 - };
132 -
133 - function Child({prop}) {
134 - return <h1>{prop}2</h1>;
135 - };
136 -
137 - export default Parent;
138 - `);
139 - expect(container.firstChild).toBe(el);
140 - expect(el.textContent).toBe('B2');
141 - }
52 + describe('with compiled destructuring', () => {
53 + runTests(true);
54 });
55
144 - it('reloads arrow functions', () => {
145 - if (__DEV__) {
146 - render(`
147 - const Parent = () => {
148 - return <Child prop="A" />;
149 - };
150 -
151 - const Child = ({prop}) => {
152 - return <h1>{prop}1</h1>;
153 - };
154 -
155 - export default Parent;
156 - `);
157 - const el = container.firstChild;
158 - expect(el.textContent).toBe('A1');
159 - patch(`
160 - const Parent = () => {
161 - return <Child prop="B" />;
162 - };
163 -
164 - const Child = ({prop}) => {
165 - return <h1>{prop}2</h1>;
166 - };
167 -
168 - export default Parent;
169 - `);
170 - expect(container.firstChild).toBe(el);
171 - expect(el.textContent).toBe('B2');
172 - }
56 + describe('without compiled destructuring', () => {
57 + runTests(false);
58 });
59
175 - it('reloads a combination of memo and forwardRef', () => {
176 - if (__DEV__) {
177 - render(`
178 - const {memo} = React;
179 -
180 - const Parent = memo(React.forwardRef(function (props, ref) {
181 - return <Child prop="A" ref={ref} />;
182 - }));
183 -
184 - const Child = React.memo(({prop}) => {
185 - return <h1>{prop}1</h1>;
186 - });
187 -
188 - export default React.memo(Parent);
189 - `);
190 - const el = container.firstChild;
191 - expect(el.textContent).toBe('A1');
192 - patch(`
193 - const {memo} = React;
194 -
195 - const Parent = memo(React.forwardRef(function (props, ref) {
196 - return <Child prop="B" ref={ref} />;
197 - }));
198 -
199 - const Child = React.memo(({prop}) => {
200 - return <h1>{prop}2</h1>;
201 - });
202 -
203 - export default React.memo(Parent);
204 - `);
205 - expect(container.firstChild).toBe(el);
206 - expect(el.textContent).toBe('B2');
60 + function runTests(compileDestructuring) {
61 + function execute(source) {
62 + const compiled = babel.transform(source, {
63 + babelrc: false,
64 + presets: ['react'],
65 + plugins: [
66 + freshPlugin,
67 + 'transform-es2015-modules-commonjs',
68 + compileDestructuring && 'transform-es2015-destructuring',
69 + ].filter(Boolean),
70 + }).code;
71 + const exportsObj = {};
72 + // eslint-disable-next-line no-new-func
73 + new Function(
74 + 'global',
75 + 'React',
76 + 'exports',
77 + '__register__',
78 + '__signature__',
79 + compiled,
80 + )(global, React, exportsObj, __register__, __signature__);
81 + return exportsObj.default;
82 }
208 - });
83
210 - it('reloads default export with named memo', () => {
211 - if (__DEV__) {
212 - render(`
213 - const {memo} = React;
214 -
215 - const Child = React.memo(({prop}) => {
216 - return <h1>{prop}1</h1>;
217 - });
218 -
219 - export default memo(React.forwardRef(function Parent(props, ref) {
220 - return <Child prop="A" ref={ref} />;
221 - }));
222 - `);
223 - const el = container.firstChild;
224 - expect(el.textContent).toBe('A1');
225 - patch(`
226 - const {memo} = React;
227 -
228 - const Child = React.memo(({prop}) => {
229 - return <h1>{prop}2</h1>;
230 - });
231 -
232 - export default memo(React.forwardRef(function Parent(props, ref) {
233 - return <Child prop="B" ref={ref} />;
234 - }));
235 - `);
236 - expect(container.firstChild).toBe(el);
237 - expect(el.textContent).toBe('B2');
84 + function render(source) {
85 + const Component = execute(source);
86 + act(() => {
87 + ReactDOM.render(<Component />, container);
88 + });
89 + // Module initialization shouldn't be counted as a hot update.
90 + expect(ReactFreshRuntime.prepareUpdate()).toBe(null);
91 }
239 - });
92
241 - it('reloads HOCs if they return functions', () => {
242 - if (__DEV__) {
243 - render(`
244 - function hoc(letter) {
245 - return function() {
246 - return <h1>{letter}1</h1>;
247 - }
248 - }
249 -
250 - export default function Parent() {
251 - return <Child />;
252 - }
253 -
254 - const Child = hoc('A');
255 - `);
256 - const el = container.firstChild;
257 - expect(el.textContent).toBe('A1');
258 - patch(`
259 - function hoc(letter) {
260 - return function() {
261 - return <h1>{letter}2</h1>;
262 - }
263 - }
264 -
265 - export default function Parent() {
266 - return React.createElement(Child);
267 - }
268 -
269 - const Child = hoc('B');
270 - `);
271 - expect(container.firstChild).toBe(el);
272 - expect(el.textContent).toBe('B2');
93 + function patch(source) {
94 + execute(source);
95 + const hotUpdate = ReactFreshRuntime.prepareUpdate();
96 + act(() => {
97 + scheduleHotUpdate(lastRoot, hotUpdate);
98 + });
99 }
274 - });
100
276 - it('resets state when renaming a state variable', () => {
277 - if (__DEV__) {
278 - render(`
279 - const {useState} = React;
280 - const S = 1;
281 -
282 - export default function App() {
283 - const [foo, setFoo] = useState(S);
284 - return <h1>A{foo}</h1>;
285 - }
286 - `);
287 - const el = container.firstChild;
288 - expect(el.textContent).toBe('A1');
289 -
290 - patch(`
291 - const {useState} = React;
292 - const S = 2;
101 + function __register__(type, id) {
102 + ReactFreshRuntime.register(type, id);
103 + }
104
294 - export default function App() {
295 - const [foo, setFoo] = useState(S);
296 - return <h1>B{foo}</h1>;
105 + 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 }
298 - `);
299 - // Same state variable name, so state is preserved.
300 - expect(container.firstChild).toBe(el);
301 - expect(el.textContent).toBe('B1');
302 -
303 - patch(`
304 - const {useState} = React;
305 - const S = 3;
306 -
307 - export default function App() {
308 - const [bar, setBar] = useState(S);
309 - return <h1>C{bar}</h1>;
310 - }
311 - `);
312 - // Different state variable name, so state is reset.
313 - expect(container.firstChild).not.toBe(el);
314 - const newEl = container.firstChild;
315 - expect(newEl.textContent).toBe('C3');
127 + return type;
128 + };
129 }
317 - });
130
319 - it('resets state when renaming a state variable in a HOC', () => {
320 - if (__DEV__) {
321 - render(`
322 - const {useState} = React;
323 - const S = 1;
324 -
325 - function hoc(Wrapped) {
326 - return function Generated() {
327 - const [foo, setFoo] = useState(S);
328 - return <Wrapped value={foo} />;
131 + it('reloads function declarations', () => {
132 + if (__DEV__) {
133 + render(`
134 + function Parent() {
135 + return <Child prop="A" />;
136 };
330 - }
331 -
332 - export default hoc(({ value }) => {
333 - return <h1>A{value}</h1>;
334 - });
335 - `);
336 - const el = container.firstChild;
337 - expect(el.textContent).toBe('A1');
338 -
339 - patch(`
340 - const {useState} = React;
341 - const S = 2;
137
343 - function hoc(Wrapped) {
344 - return function Generated() {
345 - const [foo, setFoo] = useState(S);
346 - return <Wrapped value={foo} />;
138 + function Child({prop}) {
139 + return <h1>{prop}1</h1>;
140 };
348 - }
141
350 - export default hoc(({ value }) => {
351 - return <h1>B{value}</h1>;
352 - });
353 - `);
354 - // Same state variable name, so state is preserved.
355 - expect(container.firstChild).toBe(el);
356 - expect(el.textContent).toBe('B1');
357 -
358 - patch(`
359 - const {useState} = React;
360 - const S = 3;
361 -
362 - function hoc(Wrapped) {
363 - return function Generated() {
364 - const [bar, setBar] = useState(S);
365 - return <Wrapped value={bar} />;
142 + export default Parent;
143 + `);
144 + const el = container.firstChild;
145 + expect(el.textContent).toBe('A1');
146 + patch(`
147 + function Parent() {
148 + return <Child prop="B" />;
149 };
367 - }
150
369 - export default hoc(({ value }) => {
370 - return <h1>C{value}</h1>;
371 - });
372 - `);
373 - // Different state variable name, so state is reset.
374 - expect(container.firstChild).not.toBe(el);
375 - const newEl = container.firstChild;
376 - expect(newEl.textContent).toBe('C3');
377 - }
378 - });
379 -
380 - it('resets state when renaming a state variable in a HOC with indirection', () => {
381 - if (__DEV__) {
382 - render(`
383 - const {useState} = React;
384 - const S = 1;
385 -
386 - function hoc(Wrapped) {
387 - return function Generated() {
388 - const [foo, setFoo] = useState(S);
389 - return <Wrapped value={foo} />;
151 + function Child({prop}) {
152 + return <h1>{prop}2</h1>;
153 };
391 - }
392 -
393 - function Indirection({ value }) {
394 - return <h1>A{value}</h1>;
395 - }
396 -
397 - export default hoc(Indirection);
398 - `);
399 - const el = container.firstChild;
400 - expect(el.textContent).toBe('A1');
154
402 - patch(`
403 - const {useState} = React;
404 - const S = 2;
155 + export default Parent;
156 + `);
157 + expect(container.firstChild).toBe(el);
158 + expect(el.textContent).toBe('B2');
159 + }
160 + });
161
406 - function hoc(Wrapped) {
407 - return function Generated() {
408 - const [foo, setFoo] = useState(S);
409 - return <Wrapped value={foo} />;
162 + it('reloads arrow functions', () => {
163 + if (__DEV__) {
164 + render(`
165 + const Parent = () => {
166 + return <Child prop="A" />;
167 };
411 - }
412 -
413 - function Indirection({ value }) {
414 - return <h1>B{value}</h1>;
415 - }
416 -
417 - export default hoc(Indirection);
418 - `);
419 - // Same state variable name, so state is preserved.
420 - expect(container.firstChild).toBe(el);
421 - expect(el.textContent).toBe('B1');
422 -
423 - patch(`
424 - const {useState} = React;
425 - const S = 3;
168
427 - function hoc(Wrapped) {
428 - return function Generated() {
429 - const [bar, setBar] = useState(S);
430 - return <Wrapped value={bar} />;
169 + const Child = ({prop}) => {
170 + return <h1>{prop}1</h1>;
171 };
432 - }
433 -
434 - function Indirection({ value }) {
435 - return <h1>C{value}</h1>;
436 - }
172
438 - export default hoc(Indirection);
439 - `);
440 - // Different state variable name, so state is reset.
441 - expect(container.firstChild).not.toBe(el);
442 - const newEl = container.firstChild;
443 - expect(newEl.textContent).toBe('C3');
444 - }
445 - });
446 -
447 - it('resets effects while preserving state', () => {
448 - if (__DEV__) {
449 - render(`
450 - const {useState} = React;
173 + export default Parent;
174 + `);
175 + const el = container.firstChild;
176 + expect(el.textContent).toBe('A1');
177 + patch(`
178 + const Parent = () => {
179 + return <Child prop="B" />;
180 + };
181
452 - export default function App() {
453 - const [value, setValue] = useState(0);
454 - return <h1>A{value}</h1>;
455 - }
456 - `);
457 - let el = container.firstChild;
458 - expect(el.textContent).toBe('A0');
459 -
460 - // Add an effect.
461 - patch(`
462 - const {useState} = React;
463 -
464 - export default function App() {
465 - const [value, setValue] = useState(0);
466 - React.useEffect(() => {
467 - const id = setInterval(() => {
468 - setValue(v => v + 1);
469 - }, 1000);
470 - return () => clearInterval(id);
471 - }, []);
472 - return <h1>B{value}</h1>;
473 - }
474 - `);
475 - // We added an effect, thereby changing Hook order.
476 - // This causes a remount.
477 - expect(container.firstChild).not.toBe(el);
478 - el = container.firstChild;
479 - expect(el.textContent).toBe('B0');
182 + const Child = ({prop}) => {
183 + return <h1>{prop}2</h1>;
184 + };
185
481 - act(() => {
482 - jest.advanceTimersByTime(1000);
483 - });
484 - expect(el.textContent).toBe('B1');
485 -
486 - patch(`
487 - const {useState} = React;
488 -
489 - export default function App() {
490 - const [value, setValue] = useState(0);
491 - React.useEffect(() => {
492 - const id = setInterval(() => {
493 - setValue(v => v + 10);
494 - }, 1000);
495 - return () => clearInterval(id);
496 - }, []);
497 - return <h1>C{value}</h1>;
498 - }
499 - `);
500 - // Same Hooks are called, so state is preserved.
501 - expect(container.firstChild).toBe(el);
502 - expect(el.textContent).toBe('C1');
186 + export default Parent;
187 + `);
188 + expect(container.firstChild).toBe(el);
189 + expect(el.textContent).toBe('B2');
190 + }
191 + });
192
504 - // Effects are always reset, so timer was reinstalled.
505 - // The new version increments by 10 rather than 1.
506 - act(() => {
507 - jest.advanceTimersByTime(1000);
508 - });
509 - expect(el.textContent).toBe('C11');
193 + it('reloads a combination of memo and forwardRef', () => {
194 + if (__DEV__) {
195 + render(`
196 + const {memo} = React;
197
511 - patch(`
512 - const {useState} = React;
198 + const Parent = memo(React.forwardRef(function (props, ref) {
199 + return <Child prop="A" ref={ref} />;
200 + }));
201
514 - export default function App() {
515 - const [value, setValue] = useState(0);
516 - return <h1>D{value}</h1>;
517 - }
518 - `);
519 - // Removing the effect changes the signature
520 - // and causes the component to remount.
521 - expect(container.firstChild).not.toBe(el);
522 - el = container.firstChild;
523 - expect(el.textContent).toBe('D0');
524 - }
525 - });
202 + const Child = React.memo(({prop}) => {
203 + return <h1>{prop}1</h1>;
204 + });
205
527 - it('does not get confused when custom hooks are reordered', () => {
528 - if (__DEV__) {
529 - render(`
530 - function useFancyState(initialState) {
531 - return React.useState(initialState);
532 - }
206 + export default React.memo(Parent);
207 + `);
208 + const el = container.firstChild;
209 + expect(el.textContent).toBe('A1');
210 + patch(`
211 + const {memo} = React;
212
534 - const App = () => {
535 - const [x, setX] = useFancyState('X');
536 - const [y, setY] = useFancyState('Y');
537 - return <h1>A{x}{y}</h1>;
538 - };
213 + const Parent = memo(React.forwardRef(function (props, ref) {
214 + return <Child prop="B" ref={ref} />;
215 + }));
216
540 - export default App;
541 - `);
542 - let el = container.firstChild;
543 - expect(el.textContent).toBe('AXY');
217 + const Child = React.memo(({prop}) => {
218 + return <h1>{prop}2</h1>;
219 + });
220
545 - patch(`
546 - function useFancyState(initialState) {
547 - return React.useState(initialState);
548 - }
221 + export default React.memo(Parent);
222 + `);
223 + expect(container.firstChild).toBe(el);
224 + expect(el.textContent).toBe('B2');
225 + }
226 + });
227
550 - const App = () => {
551 - const [x, setX] = useFancyState('X');
552 - const [y, setY] = useFancyState('Y');
553 - return <h1>B{x}{y}</h1>;
554 - };
555 -
556 - export default App;
557 - `);
558 - // Same state variables, so no remount.
559 - expect(container.firstChild).toBe(el);
560 - expect(el.textContent).toBe('BXY');
561 -
562 - patch(`
563 - function useFancyState(initialState) {
564 - return React.useState(initialState);
565 - }
228 + it('reloads default export with named memo', () => {
229 + if (__DEV__) {
230 + render(`
231 + const {memo} = React;
232
567 - const App = () => {
568 - const [y, setY] = useFancyState('Y');
569 - const [x, setX] = useFancyState('X');
570 - return <h1>B{x}{y}</h1>;
571 - };
572 -
573 - export default App;
574 - `);
575 - // Hooks were re-ordered. This causes a remount.
576 - // Therefore, Hook calls don't accidentally share state.
577 - expect(container.firstChild).not.toBe(el);
578 - el = container.firstChild;
579 - expect(el.textContent).toBe('BXY');
580 - }
581 - });
233 + const Child = React.memo(({prop}) => {
234 + return <h1>{prop}1</h1>;
235 + });
236
583 - it('does not get confused by Hooks defined inline', () => {
584 - // This is not a recommended pattern but at least it shouldn't break.
585 - if (__DEV__) {
586 - render(`
587 - const App = () => {
588 - const useFancyState = (initialState) => {
589 - const result = React.useState(initialState);
590 - return result;
591 - };
592 - const [x, setX] = useFancyState('X1');
593 - const [y, setY] = useFancyState('Y1');
594 - return <h1>A{x}{y}</h1>;
595 - };
596 -
597 - export default App;
598 - `);
599 - let el = container.firstChild;
600 - expect(el.textContent).toBe('AX1Y1');
601 -
602 - patch(`
603 - const App = () => {
604 - const useFancyState = (initialState) => {
605 - const result = React.useState(initialState);
606 - return result;
607 - };
608 - const [x, setX] = useFancyState('X2');
609 - const [y, setY] = useFancyState('Y2');
610 - return <h1>B{x}{y}</h1>;
611 - };
612 -
613 - export default App;
614 - `);
615 - // Remount even though nothing changed because
616 - // the custom Hook is inside -- and so we don't
617 - // really know whether its signature has changed.
618 - // We could potentially make it work, but for now
619 - // let's assert we don't crash with confusing errors.
620 - expect(container.firstChild).not.toBe(el);
621 - el = container.firstChild;
622 - expect(el.textContent).toBe('BX2Y2');
623 - }
624 - });
237 + export default memo(React.forwardRef(function Parent(props, ref) {
238 + return <Child prop="A" ref={ref} />;
239 + }));
240 + `);
241 + const el = container.firstChild;
242 + expect(el.textContent).toBe('A1');
243 + patch(`
244 + const {memo} = React;
245
626 - it('remounts component if custom hook it uses changes order', () => {
627 - if (__DEV__) {
628 - render(`
629 - const App = () => {
630 - const [x, setX] = useFancyState('X');
631 - const [y, setY] = useFancyState('Y');
632 - return <h1>A{x}{y}</h1>;
633 - };
634 -
635 - const useFancyState = (initialState) => {
636 - const result = useIndirection(initialState);
637 - return result;
638 - };
639 -
640 - function useIndirection(initialState) {
641 - return React.useState(initialState);
642 - }
246 + const Child = React.memo(({prop}) => {
247 + return <h1>{prop}2</h1>;
248 + });
249
644 - export default App;
645 - `);
646 - let el = container.firstChild;
647 - expect(el.textContent).toBe('AXY');
648 -
649 - patch(`
650 - const App = () => {
651 - const [x, setX] = useFancyState('X');
652 - const [y, setY] = useFancyState('Y');
653 - return <h1>B{x}{y}</h1>;
654 - };
655 -
656 - const useFancyState = (initialState) => {
657 - const result = useIndirection();
658 - return result;
659 - };
660 -
661 - function useIndirection(initialState) {
662 - return React.useState(initialState);
663 - }
250 + export default memo(React.forwardRef(function Parent(props, ref) {
251 + return <Child prop="B" ref={ref} />;
252 + }));
253 + `);
254 + expect(container.firstChild).toBe(el);
255 + expect(el.textContent).toBe('B2');
256 + }
257 + });
258
665 - export default App;
666 - `);
667 - // We didn't change anything except the header text.
668 - // So we don't expect a remount.
669 - expect(container.firstChild).toBe(el);
670 - expect(el.textContent).toBe('BXY');
671 -
672 - patch(`
673 - const App = () => {
674 - const [x, setX] = useFancyState('X');
675 - const [y, setY] = useFancyState('Y');
676 - return <h1>C{x}{y}</h1>;
677 - };
678 -
679 - const useFancyState = (initialState) => {
680 - const result = useIndirection(initialState);
681 - return result;
682 - };
683 -
684 - function useIndirection(initialState) {
685 - React.useEffect(() => {});
686 - return React.useState(initialState);
687 - }
259 + it('reloads HOCs if they return functions', () => {
260 + if (__DEV__) {
261 + render(`
262 + function hoc(letter) {
263 + return function() {
264 + return <h1>{letter}1</h1>;
265 + }
266 + }
267
689 - export default App;
690 - `);
691 - // The useIndirection Hook added an affect,
692 - // so we had to remount the component.
693 - expect(container.firstChild).not.toBe(el);
694 - el = container.firstChild;
695 - expect(el.textContent).toBe('CXY');
696 -
697 - patch(`
698 - const App = () => {
699 - const [x, setX] = useFancyState('X');
700 - const [y, setY] = useFancyState('Y');
701 - return <h1>D{x}{y}</h1>;
702 - };
703 -
704 - const useFancyState = (initialState) => {
705 - const result = useIndirection();
706 - return result;
707 - };
708 -
709 - function useIndirection(initialState) {
710 - React.useEffect(() => {});
711 - return React.useState(initialState);
712 - }
268 + export default function Parent() {
269 + return <Child />;
270 + }
271
714 - export default App;
715 - `);
716 - // We didn't change anything except the header text.
717 - // So we don't expect a remount.
718 - expect(container.firstChild).toBe(el);
719 - expect(el.textContent).toBe('DXY');
720 - }
721 - });
272 + const Child = hoc('A');
273 + `);
274 + const el = container.firstChild;
275 + expect(el.textContent).toBe('A1');
276 + patch(`
277 + function hoc(letter) {
278 + return function() {
279 + return <h1>{letter}2</h1>;
280 + }
281 + }
282
723 - it('does not lose the inferred arrow names', () => {
724 - if (__DEV__) {
725 - render(`
726 - const Parent = () => {
727 - return <Child/>;
728 - };
729 -
730 - const Child = () => {
731 - useMyThing();
732 - return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
733 - };
734 -
735 - const useMyThing = () => {
736 - React.useState();
737 - };
738 -
739 - export default Parent;
740 - `);
741 - expect(container.textContent).toBe('Parent Child useMyThing');
742 - }
743 - });
283 + export default function Parent() {
284 + return React.createElement(Child);
285 + }
286
745 - it('does not lose the inferred function names', () => {
746 - if (__DEV__) {
747 - render(`
748 - var Parent = function() {
749 - return <Child/>;
750 - };
751 -
752 - var Child = function() {
753 - useMyThing();
754 - return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
755 - };
756 -
757 - var useMyThing = function() {
758 - React.useState();
759 - };
760 -
761 - export default Parent;
762 - `);
763 - expect(container.textContent).toBe('Parent Child useMyThing');
764 - }
765 - });
287 + const Child = hoc('B');
288 + `);
289 + expect(container.firstChild).toBe(el);
290 + expect(el.textContent).toBe('B2');
291 + }
292 + });
293
767 - it('resets state on every edit with @hot reset annotation', () => {
768 - if (__DEV__) {
769 - render(`
770 - const {useState} = React;
771 - const S = 1;
294 + it('resets state when renaming a state variable', () => {
295 + if (__DEV__) {
296 + render(`
297 + const {useState} = React;
298 + const S = 1;
299
773 - export default function App() {
774 - const [foo, setFoo] = useState(S);
775 - return <h1>A{foo}</h1>;
776 - }
777 - `);
778 - let el = container.firstChild;
779 - expect(el.textContent).toBe('A1');
300 + export default function App() {
301 + const [foo, setFoo] = useState(S);
302 + return <h1>A{foo}</h1>;
303 + }
304 + `);
305 + const el = container.firstChild;
306 + expect(el.textContent).toBe('A1');
307
781 - patch(`
782 - const {useState} = React;
783 - const S = 2;
308 + patch(`
309 + const {useState} = React;
310 + const S = 2;
311
785 - export default function App() {
786 - const [foo, setFoo] = useState(S);
787 - return <h1>B{foo}</h1>;
788 - }
789 - `);
790 - // Same state variable name, so state is preserved.
791 - expect(container.firstChild).toBe(el);
792 - expect(el.textContent).toBe('B1');
312 + export default function App() {
313 + const [foo, setFoo] = useState(S);
314 + return <h1>B{foo}</h1>;
315 + }
316 + `);
317 + // Same state variable name, so state is preserved.
318 + expect(container.firstChild).toBe(el);
319 + expect(el.textContent).toBe('B1');
320
794 - patch(`
795 - const {useState} = React;
796 - const S = 3;
321 + patch(`
322 + const {useState} = React;
323 + const S = 3;
324
798 - /* @hot reset */
325 + export default function App() {
326 + const [bar, setBar] = useState(S);
327 + return <h1>C{bar}</h1>;
328 + }
329 + `);
330 + // Different state variable name, so state is reset.
331 + expect(container.firstChild).not.toBe(el);
332 + const newEl = container.firstChild;
333 + expect(newEl.textContent).toBe('C3');
334 + }
335 + });
336
800 - export default function App() {
801 - const [foo, setFoo] = useState(S);
802 - return <h1>C{foo}</h1>;
803 - }
804 - `);
805 - // Found remount annotation, so state is reset.
806 - expect(container.firstChild).not.toBe(el);
807 - el = container.firstChild;
808 - expect(el.textContent).toBe('C3');
337 + it('resets state when renaming a state variable in a HOC', () => {
338 + if (__DEV__) {
339 + render(`
340 + const {useState} = React;
341 + const S = 1;
342 +
343 + function hoc(Wrapped) {
344 + return function Generated() {
345 + const [foo, setFoo] = useState(S);
346 + return <Wrapped value={foo} />;
347 + };
348 + }
349
810 - patch(`
811 - const {useState} = React;
812 - const S = 4;
350 + export default hoc(({ value }) => {
351 + return <h1>A{value}</h1>;
352 + });
353 + `);
354 + const el = container.firstChild;
355 + expect(el.textContent).toBe('A1');
356
814 - export default function App() {
357 + patch(`
358 + const {useState} = React;
359 + const S = 2;
360 +
361 + function hoc(Wrapped) {
362 + return function Generated() {
363 + const [foo, setFoo] = useState(S);
364 + return <Wrapped value={foo} />;
365 + };
366 + }
367
816 - // @hot reset
368 + export default hoc(({ value }) => {
369 + return <h1>B{value}</h1>;
370 + });
371 + `);
372 + // Same state variable name, so state is preserved.
373 + expect(container.firstChild).toBe(el);
374 + expect(el.textContent).toBe('B1');
375
818 - const [foo, setFoo] = useState(S);
819 - return <h1>D{foo}</h1>;
820 - }
821 - `);
822 - // Found remount annotation, so state is reset.
823 - expect(container.firstChild).not.toBe(el);
824 - el = container.firstChild;
825 - expect(el.textContent).toBe('D4');
826 -
827 - patch(`
828 - const {useState} = React;
829 - const S = 5;
830 -
831 - export default function App() {
832 - const [foo, setFoo] = useState(S);
833 - return <h1>E{foo}</h1>;
834 - }
835 - `);
836 - // There is no remount annotation anymore,
837 - // so preserve the previous state.
838 - expect(container.firstChild).toBe(el);
839 - expect(el.textContent).toBe('E4');
840 -
841 - patch(`
842 - const {useState} = React;
843 - const S = 6;
844 -
845 - export default function App() {
846 - const [foo, setFoo] = useState(S);
847 - return <h1>F{foo}</h1>;
848 - }
849 - `);
850 - // Continue editing.
851 - expect(container.firstChild).toBe(el);
852 - expect(el.textContent).toBe('F4');
376 + patch(`
377 + const {useState} = React;
378 + const S = 3;
379 +
380 + function hoc(Wrapped) {
381 + return function Generated() {
382 + const [bar, setBar] = useState(S);
383 + return <Wrapped value={bar} />;
384 + };
385 + }
386
854 - patch(`
855 - const {useState} = React;
856 - const S = 7;
387 + export default hoc(({ value }) => {
388 + return <h1>C{value}</h1>;
389 + });
390 + `);
391 + // Different state variable name, so state is reset.
392 + expect(container.firstChild).not.toBe(el);
393 + const newEl = container.firstChild;
394 + expect(newEl.textContent).toBe('C3');
395 + }
396 + });
397
858 - export default function App() {
398 + it('resets state when renaming a state variable in a HOC with indirection', () => {
399 + if (__DEV__) {
400 + render(`
401 + const {useState} = React;
402 + const S = 1;
403 +
404 + function hoc(Wrapped) {
405 + return function Generated() {
406 + const [foo, setFoo] = useState(S);
407 + return <Wrapped value={foo} />;
408 + };
409 + }
410
860 - /* @hot reset */
411 + function Indirection({ value }) {
412 + return <h1>A{value}</h1>;
413 + }
414
862 - const [foo, setFoo] = useState(S);
863 - return <h1>G{foo}</h1>;
864 - }
865 - `);
866 - // Force remount one last time.
867 - expect(container.firstChild).not.toBe(el);
868 - el = container.firstChild;
869 - expect(el.textContent).toBe('G7');
870 - }
871 - });
415 + export default hoc(Indirection);
416 + `);
417 + const el = container.firstChild;
418 + expect(el.textContent).toBe('A1');
419
873 - // This is best effort for simple cases.
874 - // We won't attempt to resolve identifiers.
875 - it('resets state when useState initial state is edited', () => {
876 - if (__DEV__) {
877 - render(`
878 - const {useState} = React;
420 + patch(`
421 + const {useState} = React;
422 + const S = 2;
423 +
424 + function hoc(Wrapped) {
425 + return function Generated() {
426 + const [foo, setFoo] = useState(S);
427 + return <Wrapped value={foo} />;
428 + };
429 + }
430
880 - export default function App() {
881 - const [foo, setFoo] = useState(1);
882 - return <h1>A{foo}</h1>;
883 - }
884 - `);
885 - let el = container.firstChild;
886 - expect(el.textContent).toBe('A1');
431 + function Indirection({ value }) {
432 + return <h1>B{value}</h1>;
433 + }
434
888 - patch(`
889 - const {useState} = React;
435 + export default hoc(Indirection);
436 + `);
437 + // Same state variable name, so state is preserved.
438 + expect(container.firstChild).toBe(el);
439 + expect(el.textContent).toBe('B1');
440
891 - export default function App() {
892 - const [foo, setFoo] = useState(1);
893 - return <h1>B{foo}</h1>;
894 - }
895 - `);
896 - // Same initial state, so it's preserved.
897 - expect(container.firstChild).toBe(el);
898 - expect(el.textContent).toBe('B1');
441 + patch(`
442 + const {useState} = React;
443 + const S = 3;
444 +
445 + function hoc(Wrapped) {
446 + return function Generated() {
447 + const [bar, setBar] = useState(S);
448 + return <Wrapped value={bar} />;
449 + };
450 + }
451
900 - patch(`
901 - const {useState} = React;
452 + function Indirection({ value }) {
453 + return <h1>C{value}</h1>;
454 + }
455
903 - export default function App() {
904 - const [foo, setFoo] = useState(2);
905 - return <h1>C{foo}</h1>;
906 - }
907 - `);
908 - // Different initial state, so state is reset.
909 - expect(container.firstChild).not.toBe(el);
910 - el = container.firstChild;
911 - expect(el.textContent).toBe('C2');
912 - }
913 - });
456 + export default hoc(Indirection);
457 + `);
458 + // Different state variable name, so state is reset.
459 + expect(container.firstChild).not.toBe(el);
460 + const newEl = container.firstChild;
461 + expect(newEl.textContent).toBe('C3');
462 + }
463 + });
464
915 - // This is best effort for simple cases.
916 - // We won't attempt to resolve identifiers.
917 - it('resets state when useReducer initial state is edited', () => {
918 - if (__DEV__) {
919 - render(`
920 - const {useReducer} = React;
465 + it('resets effects while preserving state', () => {
466 + if (__DEV__) {
467 + render(`
468 + const {useState} = React;
469
922 - export default function App() {
923 - const [foo, setFoo] = useReducer(x => x, 1);
924 - return <h1>A{foo}</h1>;
925 - }
926 - `);
927 - let el = container.firstChild;
928 - expect(el.textContent).toBe('A1');
470 + export default function App() {
471 + const [value, setValue] = useState(0);
472 + return <h1>A{value}</h1>;
473 + }
474 + `);
475 + let el = container.firstChild;
476 + expect(el.textContent).toBe('A0');
477
930 - patch(`
931 - const {useReducer} = React;
478 + // Add an effect.
479 + patch(`
480 + const {useState} = React;
481 +
482 + export default function App() {
483 + const [value, setValue] = useState(0);
484 + React.useEffect(() => {
485 + const id = setInterval(() => {
486 + setValue(v => v + 1);
487 + }, 1000);
488 + return () => clearInterval(id);
489 + }, []);
490 + return <h1>B{value}</h1>;
491 + }
492 + `);
493 + // We added an effect, thereby changing Hook order.
494 + // This causes a remount.
495 + expect(container.firstChild).not.toBe(el);
496 + el = container.firstChild;
497 + expect(el.textContent).toBe('B0');
498
933 - export default function App() {
934 - const [foo, setFoo] = useReducer(x => x, 1);
935 - return <h1>B{foo}</h1>;
936 - }
937 - `);
938 - // Same initial state, so it's preserved.
939 - expect(container.firstChild).toBe(el);
940 - expect(el.textContent).toBe('B1');
499 + act(() => {
500 + jest.advanceTimersByTime(1000);
501 + });
502 + expect(el.textContent).toBe('B1');
503
942 - patch(`
943 - const {useReducer} = React;
504 + patch(`
505 + const {useState} = React;
506 +
507 + export default function App() {
508 + const [value, setValue] = useState(0);
509 + React.useEffect(() => {
510 + const id = setInterval(() => {
511 + setValue(v => v + 10);
512 + }, 1000);
513 + return () => clearInterval(id);
514 + }, []);
515 + return <h1>C{value}</h1>;
516 + }
517 + `);
518 + // Same Hooks are called, so state is preserved.
519 + expect(container.firstChild).toBe(el);
520 + expect(el.textContent).toBe('C1');
521
945 - export default function App() {
946 - const [foo, setFoo] = useReducer(x => x, 2);
947 - return <h1>C{foo}</h1>;
948 - }
949 - `);
950 - // Different initial state, so state is reset.
951 - expect(container.firstChild).not.toBe(el);
952 - el = container.firstChild;
953 - expect(el.textContent).toBe('C2');
954 - }
955 - });
522 + // Effects are always reset, so timer was reinstalled.
523 + // The new version increments by 10 rather than 1.
524 + act(() => {
525 + jest.advanceTimersByTime(1000);
526 + });
527 + expect(el.textContent).toBe('C11');
528
957 - describe('with inline requires', () => {
958 - beforeEach(() => {
959 - global.FakeModuleSystem = {};
960 - });
529 + patch(`
530 + const {useState} = React;
531
962 - afterEach(() => {
963 - delete global.FakeModuleSystem;
532 + export default function App() {
533 + const [value, setValue] = useState(0);
534 + return <h1>D{value}</h1>;
535 + }
536 + `);
537 + // Removing the effect changes the signature
538 + // and causes the component to remount.
539 + expect(container.firstChild).not.toBe(el);
540 + el = container.firstChild;
541 + expect(el.textContent).toBe('D0');
542 + }
543 });
544
966 - it('remounts component if custom hook it uses changes order on first edit', () => {
967 - // This test verifies that remounting works even if calls to custom Hooks
968 - // were transformed with an inline requires transform, like we have on RN.
969 - // Inline requires make it harder to compare previous and next signatures
970 - // because useFancyState inline require always resolves to the newest version.
971 - // We're not actually using inline requires in the test, but it has similar semantics.
545 + it('does not get confused when custom hooks are reordered', () => {
546 if (__DEV__) {
547 render(`
974 - const FakeModuleSystem = global.FakeModuleSystem;
975 -
976 - FakeModuleSystem.useFancyState = function(initialState) {
548 + function useFancyState(initialState) {
549 return React.useState(initialState);
978 - };
550 + }
551
980 - const App = () => {
981 - const [x, setX] = FakeModuleSystem.useFancyState('X');
982 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
552 + const App = () => {
553 + const [x, setX] = useFancyState('X');
554 + const [y, setY] = useFancyState('Y');
555 return <h1>A{x}{y}</h1>;
556 };
557
@@ -989,124 +561,174 @@ describe('ReactFreshIntegration', () => {
561 expect(el.textContent).toBe('AXY');
562
563 patch(`
992 - const FakeModuleSystem = global.FakeModuleSystem;
993 -
994 - FakeModuleSystem.useFancyState = function(initialState) {
995 - React.useEffect(() => {});
564 + function useFancyState(initialState) {
565 return React.useState(initialState);
997 - };
566 + }
567
568 const App = () => {
1000 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1001 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
569 + const [x, setX] = useFancyState('X');
570 + const [y, setY] = useFancyState('Y');
571 return <h1>B{x}{y}</h1>;
572 };
573
574 export default App;
575 `);
1007 - // The useFancyState Hook added an effect,
1008 - // so we had to remount the component.
1009 - expect(container.firstChild).not.toBe(el);
1010 - el = container.firstChild;
576 + // Same state variables, so no remount.
577 + expect(container.firstChild).toBe(el);
578 expect(el.textContent).toBe('BXY');
579
580 patch(`
1014 - const FakeModuleSystem = global.FakeModuleSystem;
1015 -
1016 - FakeModuleSystem.useFancyState = function(initialState) {
1017 - React.useEffect(() => {});
581 + function useFancyState(initialState) {
582 return React.useState(initialState);
1019 - };
583 + }
584
585 const App = () => {
1022 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1023 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
1024 - return <h1>C{x}{y}</h1>;
586 + const [y, setY] = useFancyState('Y');
587 + const [x, setX] = useFancyState('X');
588 + return <h1>B{x}{y}</h1>;
589 };
590
591 export default App;
592 `);
1029 - // We didn't change anything except the header text.
1030 - // So we don't expect a remount.
1031 - expect(container.firstChild).toBe(el);
1032 - expect(el.textContent).toBe('CXY');
593 + // Hooks were re-ordered. This causes a remount.
594 + // Therefore, Hook calls don't accidentally share state.
595 + expect(container.firstChild).not.toBe(el);
596 + el = container.firstChild;
597 + expect(el.textContent).toBe('BXY');
598 }
599 });
600
1036 - it('remounts component if custom hook it uses changes order on second edit', () => {
601 + it('does not get confused by Hooks defined inline', () => {
602 + // This is not a recommended pattern but at least it shouldn't break.
603 if (__DEV__) {
604 render(`
1039 - const FakeModuleSystem = global.FakeModuleSystem;
605 + const App = () => {
606 + const useFancyState = (initialState) => {
607 + const result = React.useState(initialState);
608 + return result;
609 + };
610 + const [x, setX] = useFancyState('X1');
611 + const [y, setY] = useFancyState('Y1');
612 + return <h1>A{x}{y}</h1>;
613 + };
614
1041 - FakeModuleSystem.useFancyState = function(initialState) {
1042 - return React.useState(initialState);
615 + export default App;
616 + `);
617 + let el = container.firstChild;
618 + expect(el.textContent).toBe('AX1Y1');
619 +
620 + patch(`
621 + const App = () => {
622 + const useFancyState = (initialState) => {
623 + const result = React.useState(initialState);
624 + return result;
625 + };
626 + const [x, setX] = useFancyState('X2');
627 + const [y, setY] = useFancyState('Y2');
628 + return <h1>B{x}{y}</h1>;
629 };
630
631 + export default App;
632 + `);
633 + // Remount even though nothing changed because
634 + // the custom Hook is inside -- and so we don't
635 + // really know whether its signature has changed.
636 + // We could potentially make it work, but for now
637 + // let's assert we don't crash with confusing errors.
638 + expect(container.firstChild).not.toBe(el);
639 + el = container.firstChild;
640 + expect(el.textContent).toBe('BX2Y2');
641 + }
642 + });
643 +
644 + it('remounts component if custom hook it uses changes order', () => {
645 + if (__DEV__) {
646 + render(`
647 const App = () => {
1046 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1047 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
648 + const [x, setX] = useFancyState('X');
649 + const [y, setY] = useFancyState('Y');
650 return <h1>A{x}{y}</h1>;
651 };
652
653 + const useFancyState = (initialState) => {
654 + const result = useIndirection(initialState);
655 + return result;
656 + };
657 +
658 + function useIndirection(initialState) {
659 + return React.useState(initialState);
660 + }
661 +
662 export default App;
663 `);
664 let el = container.firstChild;
665 expect(el.textContent).toBe('AXY');
666
667 patch(`
1057 - const FakeModuleSystem = global.FakeModuleSystem;
1058 -
1059 - FakeModuleSystem.useFancyState = function(initialState) {
1060 - return React.useState(initialState);
1061 - };
1062 -
668 const App = () => {
1064 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1065 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
669 + const [x, setX] = useFancyState('X');
670 + const [y, setY] = useFancyState('Y');
671 return <h1>B{x}{y}</h1>;
672 };
673
674 + const useFancyState = (initialState) => {
675 + const result = useIndirection();
676 + return result;
677 + };
678 +
679 + function useIndirection(initialState) {
680 + return React.useState(initialState);
681 + }
682 +
683 export default App;
684 `);
685 + // We didn't change anything except the header text.
686 + // So we don't expect a remount.
687 expect(container.firstChild).toBe(el);
688 expect(el.textContent).toBe('BXY');
689
690 patch(`
1075 - const FakeModuleSystem = global.FakeModuleSystem;
1076 -
1077 - FakeModuleSystem.useFancyState = function(initialState) {
1078 - React.useEffect(() => {});
1079 - return React.useState(initialState);
1080 - };
1081 -
691 const App = () => {
1083 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1084 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
692 + const [x, setX] = useFancyState('X');
693 + const [y, setY] = useFancyState('Y');
694 return <h1>C{x}{y}</h1>;
695 };
696
697 + const useFancyState = (initialState) => {
698 + const result = useIndirection(initialState);
699 + return result;
700 + };
701 +
702 + function useIndirection(initialState) {
703 + React.useEffect(() => {});
704 + return React.useState(initialState);
705 + }
706 +
707 export default App;
708 `);
1090 - // The useFancyState Hook added an effect,
709 + // The useIndirection Hook added an affect,
710 // so we had to remount the component.
711 expect(container.firstChild).not.toBe(el);
712 el = container.firstChild;
713 expect(el.textContent).toBe('CXY');
714
715 patch(`
1097 - const FakeModuleSystem = global.FakeModuleSystem;
1098 -
1099 - FakeModuleSystem.useFancyState = function(initialState) {
1100 - React.useEffect(() => {});
1101 - return React.useState(initialState);
1102 - };
1103 -
716 const App = () => {
1105 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1106 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
717 + const [x, setX] = useFancyState('X');
718 + const [y, setY] = useFancyState('Y');
719 return <h1>D{x}{y}</h1>;
720 };
721
722 + const useFancyState = (initialState) => {
723 + const result = useIndirection();
724 + return result;
725 + };
726 +
727 + function useIndirection(initialState) {
728 + React.useEffect(() => {});
729 + return React.useState(initialState);
730 + }
731 +
732 export default App;
733 `);
734 // We didn't change anything except the header text.
@@ -1116,140 +738,537 @@ describe('ReactFreshIntegration', () => {
738 }
739 });
740
1119 - it('recovers if evaluating Hook list throws', () => {
741 + it('does not lose the inferred arrow names', () => {
742 + if (__DEV__) {
743 + render(`
744 + const Parent = () => {
745 + return <Child/>;
746 + };
747 +
748 + const Child = () => {
749 + useMyThing();
750 + return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
751 + };
752 +
753 + const useMyThing = () => {
754 + React.useState();
755 + };
756 +
757 + export default Parent;
758 + `);
759 + expect(container.textContent).toBe('Parent Child useMyThing');
760 + }
761 + });
762 +
763 + it('does not lose the inferred function names', () => {
764 if (__DEV__) {
765 render(`
1122 - let FakeModuleSystem = null;
766 + var Parent = function() {
767 + return <Child/>;
768 + };
769 +
770 + var Child = function() {
771 + useMyThing();
772 + return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;
773 + };
774 +
775 + var useMyThing = function() {
776 + React.useState();
777 + };
778
1124 - global.FakeModuleSystem.useFancyState = function(initialState) {
1125 - return React.useState(initialState);
1126 - };
779 + export default Parent;
780 + `);
781 + expect(container.textContent).toBe('Parent Child useMyThing');
782 + }
783 + });
784
1128 - const App = () => {
1129 - FakeModuleSystem = global.FakeModuleSystem;
1130 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1131 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
1132 - return <h1>A{x}{y}</h1>;
1133 - };
785 + it('resets state on every edit with @hot reset annotation', () => {
786 + if (__DEV__) {
787 + render(`
788 + const {useState} = React;
789 + const S = 1;
790
1135 - export default App;
1136 - `);
791 + export default function App() {
792 + const [foo, setFoo] = useState(S);
793 + return <h1>A{foo}</h1>;
794 + }
795 + `);
796 let el = container.firstChild;
1138 - expect(el.textContent).toBe('AXY');
797 + expect(el.textContent).toBe('A1');
798
799 patch(`
1141 - let FakeModuleSystem = null;
1142 -
1143 - global.FakeModuleSystem.useFancyState = function(initialState) {
1144 - React.useEffect(() => {});
1145 - return React.useState(initialState);
1146 - };
1147 -
1148 - const App = () => {
1149 - FakeModuleSystem = global.FakeModuleSystem;
1150 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1151 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
1152 - return <h1>B{x}{y}</h1>;
1153 - };
1154 -
1155 - export default App;
1156 - `);
1157 - // We couldn't evaluate the Hook signatures
1158 - // so we had to remount the component.
800 + const {useState} = React;
801 + const S = 2;
802 +
803 + export default function App() {
804 + const [foo, setFoo] = useState(S);
805 + return <h1>B{foo}</h1>;
806 + }
807 + `);
808 + // Same state variable name, so state is preserved.
809 + expect(container.firstChild).toBe(el);
810 + expect(el.textContent).toBe('B1');
811 +
812 + patch(`
813 + const {useState} = React;
814 + const S = 3;
815 +
816 + /* @hot reset */
817 +
818 + export default function App() {
819 + const [foo, setFoo] = useState(S);
820 + return <h1>C{foo}</h1>;
821 + }
822 + `);
823 + // Found remount annotation, so state is reset.
824 expect(container.firstChild).not.toBe(el);
825 el = container.firstChild;
1161 - expect(el.textContent).toBe('BXY');
826 + expect(el.textContent).toBe('C3');
827 +
828 + patch(`
829 + const {useState} = React;
830 + const S = 4;
831 +
832 + export default function App() {
833 +
834 + // @hot reset
835 +
836 + const [foo, setFoo] = useState(S);
837 + return <h1>D{foo}</h1>;
838 + }
839 + `);
840 + // Found remount annotation, so state is reset.
841 + expect(container.firstChild).not.toBe(el);
842 + el = container.firstChild;
843 + expect(el.textContent).toBe('D4');
844 +
845 + patch(`
846 + const {useState} = React;
847 + const S = 5;
848 +
849 + export default function App() {
850 + const [foo, setFoo] = useState(S);
851 + return <h1>E{foo}</h1>;
852 + }
853 + `);
854 + // There is no remount annotation anymore,
855 + // so preserve the previous state.
856 + expect(container.firstChild).toBe(el);
857 + expect(el.textContent).toBe('E4');
858 +
859 + patch(`
860 + const {useState} = React;
861 + const S = 6;
862 +
863 + export default function App() {
864 + const [foo, setFoo] = useState(S);
865 + return <h1>F{foo}</h1>;
866 + }
867 + `);
868 + // Continue editing.
869 + expect(container.firstChild).toBe(el);
870 + expect(el.textContent).toBe('F4');
871 +
872 + patch(`
873 + const {useState} = React;
874 + const S = 7;
875 +
876 + export default function App() {
877 +
878 + /* @hot reset */
879 +
880 + const [foo, setFoo] = useState(S);
881 + return <h1>G{foo}</h1>;
882 + }
883 + `);
884 + // Force remount one last time.
885 + expect(container.firstChild).not.toBe(el);
886 + el = container.firstChild;
887 + expect(el.textContent).toBe('G7');
888 }
889 });
890
1165 - it('remounts component if custom hook it uses changes order behind an indirection', () => {
891 + // This is best effort for simple cases.
892 + // We won't attempt to resolve identifiers.
893 + it('resets state when useState initial state is edited', () => {
894 if (__DEV__) {
895 render(`
1168 - const FakeModuleSystem = global.FakeModuleSystem;
896 + const {useState} = React;
897
1170 - FakeModuleSystem.useFancyState = function(initialState) {
1171 - return FakeModuleSystem.useIndirection(initialState);
1172 - };
898 + export default function App() {
899 + const [foo, setFoo] = useState(1);
900 + return <h1>A{foo}</h1>;
901 + }
902 + `);
903 + let el = container.firstChild;
904 + expect(el.textContent).toBe('A1');
905
1174 - FakeModuleSystem.useIndirection = function(initialState) {
1175 - return FakeModuleSystem.useOtherIndirection(initialState);
1176 - };
906 + patch(`
907 + const {useState} = React;
908
1178 - FakeModuleSystem.useOtherIndirection = function(initialState) {
1179 - return React.useState(initialState);
1180 - };
909 + export default function App() {
910 + const [foo, setFoo] = useState(1);
911 + return <h1>B{foo}</h1>;
912 + }
913 + `);
914 + // Same initial state, so it's preserved.
915 + expect(container.firstChild).toBe(el);
916 + expect(el.textContent).toBe('B1');
917
1182 - const App = () => {
1183 - const [x, setX] = FakeModuleSystem.useFancyState('X');
1184 - const [y, setY] = FakeModuleSystem.useFancyState('Y');
1185 - return <h1>A{x}{y}</h1>;
1186 - };
918 + patch(`
919 + const {useState} = React;
920
1188 - export default App;
921 + export default function App() {
922 + const [foo, setFoo] = useState(2);
923 + return <h1>C{foo}</h1>;
924 + }
925 + `);
926 + // Different initial state, so state is reset.
927 + expect(container.firstChild).not.toBe(el);
928 + el = container.firstChild;
929 + expect(el.textContent).toBe('C2');
930 + }
931 + });
932 +
933 + // This is best effort for simple cases.
934 + // We won't attempt to resolve identifiers.
935 + it('resets state when useReducer initial state is edited', () => {
936 + if (__DEV__) {
937 + render(`
938 + const {useReducer} = React;
939 +
940 + export default function App() {
941 + const [foo, setFoo] = useReducer(x => x, 1);
942 + return <h1>A{foo}</h1>;
943 + }
944 `);
945 let el = container.firstChild;
1191 - expect(el.textContent).toBe('AXY');
946 + expect(el.textContent).toBe('A1');
947
948 patch(`
1194 - const FakeModuleSystem = global.FakeModuleSystem;
949 + const {useReducer} = React;
950
1196 - FakeModuleSystem.useFancyState = function(initialState) {
1197 - return FakeModuleSystem.useIndirection(initialState);
1198 - };
951 + export default function App() {
952 + const [foo, setFoo] = useReducer(x => x, 1);
953 + return <h1>B{foo}</h1>;
954 + }
955 + `);
956 + // Same initial state, so it's preserved.
957 + expect(container.firstChild).toBe(el);
958 + expect(el.textContent).toBe('B1');
959
1200 - FakeModuleSystem.useIndirection = function(initialState) {
1201 - return FakeModuleSystem.useOtherIndirection(initialState);
1202 - };
960 + patch(`
961 + const {useReducer} = React;
962
1204 - FakeModuleSystem.useOtherIndirection = function(initialState) {
1205 - React.useEffect(() => {});
963 + export default function App() {
964 + const [foo, setFoo] = useReducer(x => x, 2);
965 + return <h1>C{foo}</h1>;
966 + }
967 + `);
968 + // Different initial state, so state is reset.
969 + expect(container.firstChild).not.toBe(el);
970 + el = container.firstChild;
971 + expect(el.textContent).toBe('C2');
972 + }
973 + });
974 +
975 + describe('with inline requires', () => {
976 + beforeEach(() => {
977 + global.FakeModuleSystem = {};
978 + });
979 +
980 + afterEach(() => {
981 + delete global.FakeModuleSystem;
982 + });
983 +
984 + it('remounts component if custom hook it uses changes order on first edit', () => {
985 + // This test verifies that remounting works even if calls to custom Hooks
986 + // were transformed with an inline requires transform, like we have on RN.
987 + // Inline requires make it harder to compare previous and next signatures
988 + // because useFancyState inline require always resolves to the newest version.
989 + // We're not actually using inline requires in the test, but it has similar semantics.
990 + if (__DEV__) {
991 + render(`
992 + const FakeModuleSystem = global.FakeModuleSystem;
993 +
994 + FakeModuleSystem.useFancyState = function(initialState) {
995 + return React.useState(initialState);
996 + };
997 +
998 + const App = () => {
999 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1000 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1001 + return <h1>A{x}{y}</h1>;
1002 + };
1003 +
1004 + export default App;
1005 + `);
1006 + let el = container.firstChild;
1007 + expect(el.textContent).toBe('AXY');
1008 +
1009 + patch(`
1010 + const FakeModuleSystem = global.FakeModuleSystem;
1011 +
1012 + FakeModuleSystem.useFancyState = function(initialState) {
1013 + React.useEffect(() => {});
1014 + return React.useState(initialState);
1015 + };
1016 +
1017 + const App = () => {
1018 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1019 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1020 + return <h1>B{x}{y}</h1>;
1021 + };
1022 +
1023 + export default App;
1024 + `);
1025 + // The useFancyState Hook added an effect,
1026 + // so we had to remount the component.
1027 + expect(container.firstChild).not.toBe(el);
1028 + el = container.firstChild;
1029 + expect(el.textContent).toBe('BXY');
1030 +
1031 + patch(`
1032 + const FakeModuleSystem = global.FakeModuleSystem;
1033 +
1034 + FakeModuleSystem.useFancyState = function(initialState) {
1035 + React.useEffect(() => {});
1036 + return React.useState(initialState);
1037 + };
1038 +
1039 + const App = () => {
1040 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1041 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1042 + return <h1>C{x}{y}</h1>;
1043 + };
1044 +
1045 + export default App;
1046 + `);
1047 + // We didn't change anything except the header text.
1048 + // So we don't expect a remount.
1049 + expect(container.firstChild).toBe(el);
1050 + expect(el.textContent).toBe('CXY');
1051 + }
1052 + });
1053 +
1054 + it('remounts component if custom hook it uses changes order on second edit', () => {
1055 + if (__DEV__) {
1056 + render(`
1057 + const FakeModuleSystem = global.FakeModuleSystem;
1058 +
1059 + FakeModuleSystem.useFancyState = function(initialState) {
1060 + return React.useState(initialState);
1061 + };
1062 +
1063 + const App = () => {
1064 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1065 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1066 + return <h1>A{x}{y}</h1>;
1067 + };
1068 +
1069 + export default App;
1070 + `);
1071 + let el = container.firstChild;
1072 + expect(el.textContent).toBe('AXY');
1073 +
1074 + patch(`
1075 + const FakeModuleSystem = global.FakeModuleSystem;
1076 +
1077 + FakeModuleSystem.useFancyState = function(initialState) {
1078 + return React.useState(initialState);
1079 + };
1080 +
1081 + const App = () => {
1082 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1083 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1084 + return <h1>B{x}{y}</h1>;
1085 + };
1086 +
1087 + export default App;
1088 + `);
1089 + expect(container.firstChild).toBe(el);
1090 + expect(el.textContent).toBe('BXY');
1091 +
1092 + patch(`
1093 + const FakeModuleSystem = global.FakeModuleSystem;
1094 +
1095 + FakeModuleSystem.useFancyState = function(initialState) {
1096 + React.useEffect(() => {});
1097 + return React.useState(initialState);
1098 + };
1099 +
1100 + const App = () => {
1101 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1102 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1103 + return <h1>C{x}{y}</h1>;
1104 + };
1105 +
1106 + export default App;
1107 + `);
1108 + // The useFancyState Hook added an effect,
1109 + // so we had to remount the component.
1110 + expect(container.firstChild).not.toBe(el);
1111 + el = container.firstChild;
1112 + expect(el.textContent).toBe('CXY');
1113 +
1114 + patch(`
1115 + const FakeModuleSystem = global.FakeModuleSystem;
1116 +
1117 + FakeModuleSystem.useFancyState = function(initialState) {
1118 + React.useEffect(() => {});
1119 + return React.useState(initialState);
1120 + };
1121 +
1122 + const App = () => {
1123 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1124 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1125 + return <h1>D{x}{y}</h1>;
1126 + };
1127 +
1128 + export default App;
1129 + `);
1130 + // We didn't change anything except the header text.
1131 + // So we don't expect a remount.
1132 + expect(container.firstChild).toBe(el);
1133 + expect(el.textContent).toBe('DXY');
1134 + }
1135 + });
1136 +
1137 + it('recovers if evaluating Hook list throws', () => {
1138 + if (__DEV__) {
1139 + render(`
1140 + let FakeModuleSystem = null;
1141 +
1142 + global.FakeModuleSystem.useFancyState = function(initialState) {
1143 return React.useState(initialState);
1144 };
1145
1146 const App = () => {
1147 + FakeModuleSystem = global.FakeModuleSystem;
1148 const [x, setX] = FakeModuleSystem.useFancyState('X');
1149 const [y, setY] = FakeModuleSystem.useFancyState('Y');
1212 - return <h1>B{x}{y}</h1>;
1150 + return <h1>A{x}{y}</h1>;
1151 };
1152
1153 export default App;
1154 `);
1155 + let el = container.firstChild;
1156 + expect(el.textContent).toBe('AXY');
1157
1218 - // The useFancyState Hook added an effect,
1219 - // so we had to remount the component.
1220 - expect(container.firstChild).not.toBe(el);
1221 - el = container.firstChild;
1222 - expect(el.textContent).toBe('BXY');
1223 -
1224 - patch(`
1225 - const FakeModuleSystem = global.FakeModuleSystem;
1226 -
1227 - FakeModuleSystem.useFancyState = function(initialState) {
1228 - return FakeModuleSystem.useIndirection(initialState);
1229 - };
1230 -
1231 - FakeModuleSystem.useIndirection = function(initialState) {
1232 - return FakeModuleSystem.useOtherIndirection(initialState);
1233 - };
1158 + patch(`
1159 + let FakeModuleSystem = null;
1160
1235 - FakeModuleSystem.useOtherIndirection = function(initialState) {
1161 + global.FakeModuleSystem.useFancyState = function(initialState) {
1162 React.useEffect(() => {});
1163 return React.useState(initialState);
1164 };
1165
1166 const App = () => {
1167 + FakeModuleSystem = global.FakeModuleSystem;
1168 const [x, setX] = FakeModuleSystem.useFancyState('X');
1169 const [y, setY] = FakeModuleSystem.useFancyState('Y');
1243 - return <h1>C{x}{y}</h1>;
1170 + return <h1>B{x}{y}</h1>;
1171 };
1172
1173 export default App;
1174 `);
1248 - // We didn't change anything except the header text.
1249 - // So we don't expect a remount.
1250 - expect(container.firstChild).toBe(el);
1251 - expect(el.textContent).toBe('CXY');
1252 - }
1175 + // We couldn't evaluate the Hook signatures
1176 + // so we had to remount the component.
1177 + expect(container.firstChild).not.toBe(el);
1178 + el = container.firstChild;
1179 + expect(el.textContent).toBe('BXY');
1180 + }
1181 + });
1182 +
1183 + it('remounts component if custom hook it uses changes order behind an indirection', () => {
1184 + if (__DEV__) {
1185 + render(`
1186 + const FakeModuleSystem = global.FakeModuleSystem;
1187 +
1188 + FakeModuleSystem.useFancyState = function(initialState) {
1189 + return FakeModuleSystem.useIndirection(initialState);
1190 + };
1191 +
1192 + FakeModuleSystem.useIndirection = function(initialState) {
1193 + return FakeModuleSystem.useOtherIndirection(initialState);
1194 + };
1195 +
1196 + FakeModuleSystem.useOtherIndirection = function(initialState) {
1197 + return React.useState(initialState);
1198 + };
1199 +
1200 + const App = () => {
1201 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1202 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1203 + return <h1>A{x}{y}</h1>;
1204 + };
1205 +
1206 + export default App;
1207 + `);
1208 + let el = container.firstChild;
1209 + expect(el.textContent).toBe('AXY');
1210 +
1211 + patch(`
1212 + const FakeModuleSystem = global.FakeModuleSystem;
1213 +
1214 + FakeModuleSystem.useFancyState = function(initialState) {
1215 + return FakeModuleSystem.useIndirection(initialState);
1216 + };
1217 +
1218 + FakeModuleSystem.useIndirection = function(initialState) {
1219 + return FakeModuleSystem.useOtherIndirection(initialState);
1220 + };
1221 +
1222 + FakeModuleSystem.useOtherIndirection = function(initialState) {
1223 + React.useEffect(() => {});
1224 + return React.useState(initialState);
1225 + };
1226 +
1227 + const App = () => {
1228 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1229 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1230 + return <h1>B{x}{y}</h1>;
1231 + };
1232 +
1233 + export default App;
1234 + `);
1235 +
1236 + // The useFancyState Hook added an effect,
1237 + // so we had to remount the component.
1238 + expect(container.firstChild).not.toBe(el);
1239 + el = container.firstChild;
1240 + expect(el.textContent).toBe('BXY');
1241 +
1242 + patch(`
1243 + const FakeModuleSystem = global.FakeModuleSystem;
1244 +
1245 + FakeModuleSystem.useFancyState = function(initialState) {
1246 + return FakeModuleSystem.useIndirection(initialState);
1247 + };
1248 +
1249 + FakeModuleSystem.useIndirection = function(initialState) {
1250 + return FakeModuleSystem.useOtherIndirection(initialState);
1251 + };
1252 +
1253 + FakeModuleSystem.useOtherIndirection = function(initialState) {
1254 + React.useEffect(() => {});
1255 + return React.useState(initialState);
1256 + };
1257 +
1258 + const App = () => {
1259 + const [x, setX] = FakeModuleSystem.useFancyState('X');
1260 + const [y, setY] = FakeModuleSystem.useFancyState('Y');
1261 + return <h1>C{x}{y}</h1>;
1262 + };
1263 +
1264 + export default App;
1265 + `);
1266 + // We didn't change anything except the header text.
1267 + // So we don't expect a remount.
1268 + expect(container.firstChild).toBe(el);
1269 + expect(el.textContent).toBe('CXY');
1270 + }
1271 + });
1272 });
1254 - });
1273 + }
1274 });