[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
});