diff --git a/packages/react-fresh/src/ReactFreshBabelPlugin.js b/packages/react-fresh/src/ReactFreshBabelPlugin.js
index 96eab42908bb..eb4658556167 100644
--- a/packages/react-fresh/src/ReactFreshBabelPlugin.js
+++ b/packages/react-fresh/src/ReactFreshBabelPlugin.js
@@ -163,36 +163,6 @@ export default function(babel) {
return false;
}
- let hookCalls = new WeakMap();
-
- function recordHookCall(functionNode, hookCallPath, hookName) {
- if (!hookCalls.has(functionNode)) {
- hookCalls.set(functionNode, []);
- }
- let hookCallsForFn = hookCalls.get(functionNode);
- let key = '';
- if (hookCallPath.parent.type === 'VariableDeclarator') {
- // TODO: if there is no LHS, consider some other heuristic.
- key = hookCallPath.parentPath.get('id').getSource();
- }
-
- // Some built-in Hooks reset on edits to arguments.
- const args = hookCallPath.get('arguments');
- if (hookName === 'useState' && args.length > 0) {
- // useState second argument is initial state.
- key += '(' + args[0].getSource() + ')';
- } else if (hookName === 'useReducer' && args.length > 1) {
- // useReducer second argument is initial state.
- key += '(' + args[1].getSource() + ')';
- }
-
- hookCallsForFn.push({
- name: hookName,
- callee: hookCallPath.node.callee,
- key,
- });
- }
-
function isBuiltinHook(hookName) {
switch (hookName) {
case 'useState':
@@ -299,9 +269,64 @@ export default function(babel) {
let seenForRegistration = new WeakSet();
let seenForSignature = new WeakSet();
- let seenForHookCalls = new WeakSet();
let seenForOutro = new WeakSet();
+ let hookCalls = new WeakMap();
+ const HookCallsVisitor = {
+ CallExpression(path) {
+ const node = path.node;
+ const callee = node.callee;
+
+ // Note: this visitor MUST NOT mutate the tree in any way.
+ // It runs early in a separate traversal and should be very fast.
+
+ let name = null;
+ switch (callee.type) {
+ case 'Identifier':
+ name = callee.name;
+ break;
+ case 'MemberExpression':
+ name = callee.property.name;
+ break;
+ }
+ if (name === null || !/^use[A-Z]/.test(name)) {
+ return;
+ }
+ const fnScope = path.scope.getFunctionParent();
+ if (fnScope === null) {
+ return;
+ }
+
+ // This is a Hook call. Record it.
+ const fnNode = fnScope.block;
+ if (!hookCalls.has(fnNode)) {
+ hookCalls.set(fnNode, []);
+ }
+ let hookCallsForFn = hookCalls.get(fnNode);
+ let key = '';
+ if (path.parent.type === 'VariableDeclarator') {
+ // TODO: if there is no LHS, consider some other heuristic.
+ key = path.parentPath.get('id').getSource();
+ }
+
+ // Some built-in Hooks reset on edits to arguments.
+ const args = path.get('arguments');
+ if (name === 'useState' && args.length > 0) {
+ // useState second argument is initial state.
+ key += '(' + args[0].getSource() + ')';
+ } else if (name === 'useReducer' && args.length > 1) {
+ // useReducer second argument is initial state.
+ key += '(' + args[1].getSource() + ')';
+ }
+
+ hookCallsForFn.push({
+ callee: path.node.callee,
+ name,
+ key,
+ });
+ },
+ };
+
return {
visitor: {
ExportDefaultDeclaration(path) {
@@ -617,38 +642,14 @@ export default function(babel) {
},
);
},
- CallExpression(path) {
- const node = path.node;
- const callee = node.callee;
-
- let name = null;
- switch (callee.type) {
- case 'Identifier':
- name = callee.name;
- break;
- case 'MemberExpression':
- name = callee.property.name;
- break;
- }
- if (name === null || !/^use[A-Z]/.test(name)) {
- return;
- }
-
- // Make sure we're not recording the same calls twice.
- // This can happen if another Babel plugin replaces parents.
- if (seenForHookCalls.has(node)) {
- return;
- }
- seenForHookCalls.add(node);
- // Don't mutate the tree above this point.
-
- const fn = path.scope.getFunctionParent();
- if (fn === null) {
- return;
- }
- recordHookCall(fn.block, path, name);
- },
Program: {
+ enter(path) {
+ // This is a separate early visitor because we need to collect Hook calls
+ // and "const [foo, setFoo] = ..." signatures before the destructuring
+ // transform mangles them. This extra traversal is not ideal for perf,
+ // but it's the best we can do until we stop transpiling destructuring.
+ path.traverse(HookCallsVisitor);
+ },
exit(path) {
const registrations = registrationsByProgramPath.get(path);
if (registrations === undefined) {
diff --git a/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js b/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js
index ce4d088679b7..50617ad0d2f8 100644
--- a/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js
+++ b/packages/react-fresh/src/__tests__/ReactFreshIntegration-test.js
@@ -49,937 +49,509 @@ describe('ReactFreshIntegration', () => {
document.body.removeChild(container);
});
- function execute(source) {
- const compiled = babel.transform(source, {
- babelrc: false,
- presets: ['react'],
- plugins: [freshPlugin, 'transform-es2015-modules-commonjs'],
- }).code;
- const exportsObj = {};
- // eslint-disable-next-line no-new-func
- new Function(
- 'global',
- 'React',
- 'exports',
- '__register__',
- '__signature__',
- compiled,
- )(global, React, exportsObj, __register__, __signature__);
- return exportsObj.default;
- }
-
- function render(source) {
- const Component = execute(source);
- act(() => {
- ReactDOM.render(, container);
- });
- // Module initialization shouldn't be counted as a hot update.
- expect(ReactFreshRuntime.prepareUpdate()).toBe(null);
- }
-
- function patch(source) {
- execute(source);
- const hotUpdate = ReactFreshRuntime.prepareUpdate();
- act(() => {
- scheduleHotUpdate(lastRoot, hotUpdate);
- });
- }
-
- function __register__(type, id) {
- ReactFreshRuntime.register(type, id);
- }
-
- function __signature__() {
- let call = 0;
- let savedType;
- let hasCustomHooks;
- return function(type, key, forceReset, getCustomHooks) {
- switch (call++) {
- case 0:
- savedType = type;
- hasCustomHooks = typeof getCustomHooks === 'function';
- ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
- break;
- case 1:
- if (hasCustomHooks) {
- ReactFreshRuntime.collectCustomHooksForSignature(savedType);
- }
- break;
- }
- return type;
- };
- }
-
- it('reloads function declarations', () => {
- if (__DEV__) {
- render(`
- function Parent() {
- return ;
- };
-
- function Child({prop}) {
- return
{prop}1
;
- };
-
- export default Parent;
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
- patch(`
- function Parent() {
- return ;
- };
-
- function Child({prop}) {
- return {prop}2
;
- };
-
- export default Parent;
- `);
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B2');
- }
+ describe('with compiled destructuring', () => {
+ runTests(true);
});
- it('reloads arrow functions', () => {
- if (__DEV__) {
- render(`
- const Parent = () => {
- return ;
- };
-
- const Child = ({prop}) => {
- return {prop}1
;
- };
-
- export default Parent;
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
- patch(`
- const Parent = () => {
- return ;
- };
-
- const Child = ({prop}) => {
- return {prop}2
;
- };
-
- export default Parent;
- `);
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B2');
- }
+ describe('without compiled destructuring', () => {
+ runTests(false);
});
- it('reloads a combination of memo and forwardRef', () => {
- if (__DEV__) {
- render(`
- const {memo} = React;
-
- const Parent = memo(React.forwardRef(function (props, ref) {
- return ;
- }));
-
- const Child = React.memo(({prop}) => {
- return {prop}1
;
- });
-
- export default React.memo(Parent);
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
- patch(`
- const {memo} = React;
-
- const Parent = memo(React.forwardRef(function (props, ref) {
- return ;
- }));
-
- const Child = React.memo(({prop}) => {
- return {prop}2
;
- });
-
- export default React.memo(Parent);
- `);
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B2');
+ function runTests(compileDestructuring) {
+ function execute(source) {
+ const compiled = babel.transform(source, {
+ babelrc: false,
+ presets: ['react'],
+ plugins: [
+ freshPlugin,
+ 'transform-es2015-modules-commonjs',
+ compileDestructuring && 'transform-es2015-destructuring',
+ ].filter(Boolean),
+ }).code;
+ const exportsObj = {};
+ // eslint-disable-next-line no-new-func
+ new Function(
+ 'global',
+ 'React',
+ 'exports',
+ '__register__',
+ '__signature__',
+ compiled,
+ )(global, React, exportsObj, __register__, __signature__);
+ return exportsObj.default;
}
- });
- it('reloads default export with named memo', () => {
- if (__DEV__) {
- render(`
- const {memo} = React;
-
- const Child = React.memo(({prop}) => {
- return {prop}1
;
- });
-
- export default memo(React.forwardRef(function Parent(props, ref) {
- return ;
- }));
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
- patch(`
- const {memo} = React;
-
- const Child = React.memo(({prop}) => {
- return {prop}2
;
- });
-
- export default memo(React.forwardRef(function Parent(props, ref) {
- return ;
- }));
- `);
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B2');
+ function render(source) {
+ const Component = execute(source);
+ act(() => {
+ ReactDOM.render(, container);
+ });
+ // Module initialization shouldn't be counted as a hot update.
+ expect(ReactFreshRuntime.prepareUpdate()).toBe(null);
}
- });
- it('reloads HOCs if they return functions', () => {
- if (__DEV__) {
- render(`
- function hoc(letter) {
- return function() {
- return {letter}1
;
- }
- }
-
- export default function Parent() {
- return ;
- }
-
- const Child = hoc('A');
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
- patch(`
- function hoc(letter) {
- return function() {
- return {letter}2
;
- }
- }
-
- export default function Parent() {
- return React.createElement(Child);
- }
-
- const Child = hoc('B');
- `);
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B2');
+ function patch(source) {
+ execute(source);
+ const hotUpdate = ReactFreshRuntime.prepareUpdate();
+ act(() => {
+ scheduleHotUpdate(lastRoot, hotUpdate);
+ });
}
- });
- it('resets state when renaming a state variable', () => {
- if (__DEV__) {
- render(`
- const {useState} = React;
- const S = 1;
-
- export default function App() {
- const [foo, setFoo] = useState(S);
- return A{foo}
;
- }
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
-
- patch(`
- const {useState} = React;
- const S = 2;
+ function __register__(type, id) {
+ ReactFreshRuntime.register(type, id);
+ }
- export default function App() {
- const [foo, setFoo] = useState(S);
- return B{foo}
;
+ function __signature__() {
+ let call = 0;
+ let savedType;
+ let hasCustomHooks;
+ return function(type, key, forceReset, getCustomHooks) {
+ switch (call++) {
+ case 0:
+ savedType = type;
+ hasCustomHooks = typeof getCustomHooks === 'function';
+ ReactFreshRuntime.setSignature(
+ type,
+ key,
+ forceReset,
+ getCustomHooks,
+ );
+ break;
+ case 1:
+ if (hasCustomHooks) {
+ ReactFreshRuntime.collectCustomHooksForSignature(savedType);
+ }
+ break;
}
- `);
- // Same state variable name, so state is preserved.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B1');
-
- patch(`
- const {useState} = React;
- const S = 3;
-
- export default function App() {
- const [bar, setBar] = useState(S);
- return C{bar}
;
- }
- `);
- // Different state variable name, so state is reset.
- expect(container.firstChild).not.toBe(el);
- const newEl = container.firstChild;
- expect(newEl.textContent).toBe('C3');
+ return type;
+ };
}
- });
- it('resets state when renaming a state variable in a HOC', () => {
- if (__DEV__) {
- render(`
- const {useState} = React;
- const S = 1;
-
- function hoc(Wrapped) {
- return function Generated() {
- const [foo, setFoo] = useState(S);
- return ;
+ it('reloads function declarations', () => {
+ if (__DEV__) {
+ render(`
+ function Parent() {
+ return ;
};
- }
-
- export default hoc(({ value }) => {
- return A{value}
;
- });
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
-
- patch(`
- const {useState} = React;
- const S = 2;
- function hoc(Wrapped) {
- return function Generated() {
- const [foo, setFoo] = useState(S);
- return ;
+ function Child({prop}) {
+ return {prop}1
;
};
- }
- export default hoc(({ value }) => {
- return B{value}
;
- });
- `);
- // Same state variable name, so state is preserved.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B1');
-
- patch(`
- const {useState} = React;
- const S = 3;
-
- function hoc(Wrapped) {
- return function Generated() {
- const [bar, setBar] = useState(S);
- return ;
+ export default Parent;
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
+ patch(`
+ function Parent() {
+ return ;
};
- }
- export default hoc(({ value }) => {
- return C{value}
;
- });
- `);
- // Different state variable name, so state is reset.
- expect(container.firstChild).not.toBe(el);
- const newEl = container.firstChild;
- expect(newEl.textContent).toBe('C3');
- }
- });
-
- it('resets state when renaming a state variable in a HOC with indirection', () => {
- if (__DEV__) {
- render(`
- const {useState} = React;
- const S = 1;
-
- function hoc(Wrapped) {
- return function Generated() {
- const [foo, setFoo] = useState(S);
- return ;
+ function Child({prop}) {
+ return {prop}2
;
};
- }
-
- function Indirection({ value }) {
- return A{value}
;
- }
-
- export default hoc(Indirection);
- `);
- const el = container.firstChild;
- expect(el.textContent).toBe('A1');
- patch(`
- const {useState} = React;
- const S = 2;
+ export default Parent;
+ `);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B2');
+ }
+ });
- function hoc(Wrapped) {
- return function Generated() {
- const [foo, setFoo] = useState(S);
- return ;
+ it('reloads arrow functions', () => {
+ if (__DEV__) {
+ render(`
+ const Parent = () => {
+ return ;
};
- }
-
- function Indirection({ value }) {
- return B{value}
;
- }
-
- export default hoc(Indirection);
- `);
- // Same state variable name, so state is preserved.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B1');
-
- patch(`
- const {useState} = React;
- const S = 3;
- function hoc(Wrapped) {
- return function Generated() {
- const [bar, setBar] = useState(S);
- return ;
+ const Child = ({prop}) => {
+ return {prop}1
;
};
- }
-
- function Indirection({ value }) {
- return C{value}
;
- }
- export default hoc(Indirection);
- `);
- // Different state variable name, so state is reset.
- expect(container.firstChild).not.toBe(el);
- const newEl = container.firstChild;
- expect(newEl.textContent).toBe('C3');
- }
- });
-
- it('resets effects while preserving state', () => {
- if (__DEV__) {
- render(`
- const {useState} = React;
+ export default Parent;
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
+ patch(`
+ const Parent = () => {
+ return ;
+ };
- export default function App() {
- const [value, setValue] = useState(0);
- return A{value}
;
- }
- `);
- let el = container.firstChild;
- expect(el.textContent).toBe('A0');
-
- // Add an effect.
- patch(`
- const {useState} = React;
-
- export default function App() {
- const [value, setValue] = useState(0);
- React.useEffect(() => {
- const id = setInterval(() => {
- setValue(v => v + 1);
- }, 1000);
- return () => clearInterval(id);
- }, []);
- return B{value}
;
- }
- `);
- // We added an effect, thereby changing Hook order.
- // This causes a remount.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('B0');
+ const Child = ({prop}) => {
+ return {prop}2
;
+ };
- act(() => {
- jest.advanceTimersByTime(1000);
- });
- expect(el.textContent).toBe('B1');
-
- patch(`
- const {useState} = React;
-
- export default function App() {
- const [value, setValue] = useState(0);
- React.useEffect(() => {
- const id = setInterval(() => {
- setValue(v => v + 10);
- }, 1000);
- return () => clearInterval(id);
- }, []);
- return C{value}
;
- }
- `);
- // Same Hooks are called, so state is preserved.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('C1');
+ export default Parent;
+ `);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B2');
+ }
+ });
- // Effects are always reset, so timer was reinstalled.
- // The new version increments by 10 rather than 1.
- act(() => {
- jest.advanceTimersByTime(1000);
- });
- expect(el.textContent).toBe('C11');
+ it('reloads a combination of memo and forwardRef', () => {
+ if (__DEV__) {
+ render(`
+ const {memo} = React;
- patch(`
- const {useState} = React;
+ const Parent = memo(React.forwardRef(function (props, ref) {
+ return ;
+ }));
- export default function App() {
- const [value, setValue] = useState(0);
- return D{value}
;
- }
- `);
- // Removing the effect changes the signature
- // and causes the component to remount.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('D0');
- }
- });
+ const Child = React.memo(({prop}) => {
+ return {prop}1
;
+ });
- it('does not get confused when custom hooks are reordered', () => {
- if (__DEV__) {
- render(`
- function useFancyState(initialState) {
- return React.useState(initialState);
- }
+ export default React.memo(Parent);
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
+ patch(`
+ const {memo} = React;
- const App = () => {
- const [x, setX] = useFancyState('X');
- const [y, setY] = useFancyState('Y');
- return A{x}{y}
;
- };
+ const Parent = memo(React.forwardRef(function (props, ref) {
+ return ;
+ }));
- export default App;
- `);
- let el = container.firstChild;
- expect(el.textContent).toBe('AXY');
+ const Child = React.memo(({prop}) => {
+ return {prop}2
;
+ });
- patch(`
- function useFancyState(initialState) {
- return React.useState(initialState);
- }
+ export default React.memo(Parent);
+ `);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B2');
+ }
+ });
- const App = () => {
- const [x, setX] = useFancyState('X');
- const [y, setY] = useFancyState('Y');
- return B{x}{y}
;
- };
-
- export default App;
- `);
- // Same state variables, so no remount.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('BXY');
-
- patch(`
- function useFancyState(initialState) {
- return React.useState(initialState);
- }
+ it('reloads default export with named memo', () => {
+ if (__DEV__) {
+ render(`
+ const {memo} = React;
- const App = () => {
- const [y, setY] = useFancyState('Y');
- const [x, setX] = useFancyState('X');
- return B{x}{y}
;
- };
-
- export default App;
- `);
- // Hooks were re-ordered. This causes a remount.
- // Therefore, Hook calls don't accidentally share state.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('BXY');
- }
- });
+ const Child = React.memo(({prop}) => {
+ return {prop}1
;
+ });
- it('does not get confused by Hooks defined inline', () => {
- // This is not a recommended pattern but at least it shouldn't break.
- if (__DEV__) {
- render(`
- const App = () => {
- const useFancyState = (initialState) => {
- const result = React.useState(initialState);
- return result;
- };
- const [x, setX] = useFancyState('X1');
- const [y, setY] = useFancyState('Y1');
- return A{x}{y}
;
- };
-
- export default App;
- `);
- let el = container.firstChild;
- expect(el.textContent).toBe('AX1Y1');
-
- patch(`
- const App = () => {
- const useFancyState = (initialState) => {
- const result = React.useState(initialState);
- return result;
- };
- const [x, setX] = useFancyState('X2');
- const [y, setY] = useFancyState('Y2');
- return B{x}{y}
;
- };
-
- export default App;
- `);
- // Remount even though nothing changed because
- // the custom Hook is inside -- and so we don't
- // really know whether its signature has changed.
- // We could potentially make it work, but for now
- // let's assert we don't crash with confusing errors.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('BX2Y2');
- }
- });
+ export default memo(React.forwardRef(function Parent(props, ref) {
+ return ;
+ }));
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
+ patch(`
+ const {memo} = React;
- it('remounts component if custom hook it uses changes order', () => {
- if (__DEV__) {
- render(`
- const App = () => {
- const [x, setX] = useFancyState('X');
- const [y, setY] = useFancyState('Y');
- return A{x}{y}
;
- };
-
- const useFancyState = (initialState) => {
- const result = useIndirection(initialState);
- return result;
- };
-
- function useIndirection(initialState) {
- return React.useState(initialState);
- }
+ const Child = React.memo(({prop}) => {
+ return {prop}2
;
+ });
- export default App;
- `);
- let el = container.firstChild;
- expect(el.textContent).toBe('AXY');
-
- patch(`
- const App = () => {
- const [x, setX] = useFancyState('X');
- const [y, setY] = useFancyState('Y');
- return B{x}{y}
;
- };
-
- const useFancyState = (initialState) => {
- const result = useIndirection();
- return result;
- };
-
- function useIndirection(initialState) {
- return React.useState(initialState);
- }
+ export default memo(React.forwardRef(function Parent(props, ref) {
+ return ;
+ }));
+ `);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B2');
+ }
+ });
- export default App;
- `);
- // We didn't change anything except the header text.
- // So we don't expect a remount.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('BXY');
-
- patch(`
- const App = () => {
- const [x, setX] = useFancyState('X');
- const [y, setY] = useFancyState('Y');
- return C{x}{y}
;
- };
-
- const useFancyState = (initialState) => {
- const result = useIndirection(initialState);
- return result;
- };
-
- function useIndirection(initialState) {
- React.useEffect(() => {});
- return React.useState(initialState);
- }
+ it('reloads HOCs if they return functions', () => {
+ if (__DEV__) {
+ render(`
+ function hoc(letter) {
+ return function() {
+ return {letter}1
;
+ }
+ }
- export default App;
- `);
- // The useIndirection Hook added an affect,
- // so we had to remount the component.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('CXY');
-
- patch(`
- const App = () => {
- const [x, setX] = useFancyState('X');
- const [y, setY] = useFancyState('Y');
- return D{x}{y}
;
- };
-
- const useFancyState = (initialState) => {
- const result = useIndirection();
- return result;
- };
-
- function useIndirection(initialState) {
- React.useEffect(() => {});
- return React.useState(initialState);
- }
+ export default function Parent() {
+ return ;
+ }
- export default App;
- `);
- // We didn't change anything except the header text.
- // So we don't expect a remount.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('DXY');
- }
- });
+ const Child = hoc('A');
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
+ patch(`
+ function hoc(letter) {
+ return function() {
+ return {letter}2
;
+ }
+ }
- it('does not lose the inferred arrow names', () => {
- if (__DEV__) {
- render(`
- const Parent = () => {
- return ;
- };
-
- const Child = () => {
- useMyThing();
- return {Parent.name} {Child.name} {useMyThing.name}
;
- };
-
- const useMyThing = () => {
- React.useState();
- };
-
- export default Parent;
- `);
- expect(container.textContent).toBe('Parent Child useMyThing');
- }
- });
+ export default function Parent() {
+ return React.createElement(Child);
+ }
- it('does not lose the inferred function names', () => {
- if (__DEV__) {
- render(`
- var Parent = function() {
- return ;
- };
-
- var Child = function() {
- useMyThing();
- return {Parent.name} {Child.name} {useMyThing.name}
;
- };
-
- var useMyThing = function() {
- React.useState();
- };
-
- export default Parent;
- `);
- expect(container.textContent).toBe('Parent Child useMyThing');
- }
- });
+ const Child = hoc('B');
+ `);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B2');
+ }
+ });
- it('resets state on every edit with @hot reset annotation', () => {
- if (__DEV__) {
- render(`
- const {useState} = React;
- const S = 1;
+ it('resets state when renaming a state variable', () => {
+ if (__DEV__) {
+ render(`
+ const {useState} = React;
+ const S = 1;
- export default function App() {
- const [foo, setFoo] = useState(S);
- return A{foo}
;
- }
- `);
- let el = container.firstChild;
- expect(el.textContent).toBe('A1');
+ export default function App() {
+ const [foo, setFoo] = useState(S);
+ return A{foo}
;
+ }
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
- patch(`
- const {useState} = React;
- const S = 2;
+ patch(`
+ const {useState} = React;
+ const S = 2;
- export default function App() {
- const [foo, setFoo] = useState(S);
- return B{foo}
;
- }
- `);
- // Same state variable name, so state is preserved.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B1');
+ export default function App() {
+ const [foo, setFoo] = useState(S);
+ return B{foo}
;
+ }
+ `);
+ // Same state variable name, so state is preserved.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B1');
- patch(`
- const {useState} = React;
- const S = 3;
+ patch(`
+ const {useState} = React;
+ const S = 3;
- /* @hot reset */
+ export default function App() {
+ const [bar, setBar] = useState(S);
+ return C{bar}
;
+ }
+ `);
+ // Different state variable name, so state is reset.
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('C3');
+ }
+ });
- export default function App() {
- const [foo, setFoo] = useState(S);
- return C{foo}
;
- }
- `);
- // Found remount annotation, so state is reset.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('C3');
+ it('resets state when renaming a state variable in a HOC', () => {
+ if (__DEV__) {
+ render(`
+ const {useState} = React;
+ const S = 1;
+
+ function hoc(Wrapped) {
+ return function Generated() {
+ const [foo, setFoo] = useState(S);
+ return ;
+ };
+ }
- patch(`
- const {useState} = React;
- const S = 4;
+ export default hoc(({ value }) => {
+ return A{value}
;
+ });
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
- export default function App() {
+ patch(`
+ const {useState} = React;
+ const S = 2;
+
+ function hoc(Wrapped) {
+ return function Generated() {
+ const [foo, setFoo] = useState(S);
+ return ;
+ };
+ }
- // @hot reset
+ export default hoc(({ value }) => {
+ return B{value}
;
+ });
+ `);
+ // Same state variable name, so state is preserved.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B1');
- const [foo, setFoo] = useState(S);
- return D{foo}
;
- }
- `);
- // Found remount annotation, so state is reset.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('D4');
-
- patch(`
- const {useState} = React;
- const S = 5;
-
- export default function App() {
- const [foo, setFoo] = useState(S);
- return E{foo}
;
- }
- `);
- // There is no remount annotation anymore,
- // so preserve the previous state.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('E4');
-
- patch(`
- const {useState} = React;
- const S = 6;
-
- export default function App() {
- const [foo, setFoo] = useState(S);
- return F{foo}
;
- }
- `);
- // Continue editing.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('F4');
+ patch(`
+ const {useState} = React;
+ const S = 3;
+
+ function hoc(Wrapped) {
+ return function Generated() {
+ const [bar, setBar] = useState(S);
+ return ;
+ };
+ }
- patch(`
- const {useState} = React;
- const S = 7;
+ export default hoc(({ value }) => {
+ return C{value}
;
+ });
+ `);
+ // Different state variable name, so state is reset.
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('C3');
+ }
+ });
- export default function App() {
+ it('resets state when renaming a state variable in a HOC with indirection', () => {
+ if (__DEV__) {
+ render(`
+ const {useState} = React;
+ const S = 1;
+
+ function hoc(Wrapped) {
+ return function Generated() {
+ const [foo, setFoo] = useState(S);
+ return ;
+ };
+ }
- /* @hot reset */
+ function Indirection({ value }) {
+ return A{value}
;
+ }
- const [foo, setFoo] = useState(S);
- return G{foo}
;
- }
- `);
- // Force remount one last time.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('G7');
- }
- });
+ export default hoc(Indirection);
+ `);
+ const el = container.firstChild;
+ expect(el.textContent).toBe('A1');
- // This is best effort for simple cases.
- // We won't attempt to resolve identifiers.
- it('resets state when useState initial state is edited', () => {
- if (__DEV__) {
- render(`
- const {useState} = React;
+ patch(`
+ const {useState} = React;
+ const S = 2;
+
+ function hoc(Wrapped) {
+ return function Generated() {
+ const [foo, setFoo] = useState(S);
+ return ;
+ };
+ }
- export default function App() {
- const [foo, setFoo] = useState(1);
- return A{foo}
;
- }
- `);
- let el = container.firstChild;
- expect(el.textContent).toBe('A1');
+ function Indirection({ value }) {
+ return B{value}
;
+ }
- patch(`
- const {useState} = React;
+ export default hoc(Indirection);
+ `);
+ // Same state variable name, so state is preserved.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B1');
- export default function App() {
- const [foo, setFoo] = useState(1);
- return B{foo}
;
- }
- `);
- // Same initial state, so it's preserved.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B1');
+ patch(`
+ const {useState} = React;
+ const S = 3;
+
+ function hoc(Wrapped) {
+ return function Generated() {
+ const [bar, setBar] = useState(S);
+ return ;
+ };
+ }
- patch(`
- const {useState} = React;
+ function Indirection({ value }) {
+ return C{value}
;
+ }
- export default function App() {
- const [foo, setFoo] = useState(2);
- return C{foo}
;
- }
- `);
- // Different initial state, so state is reset.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('C2');
- }
- });
+ export default hoc(Indirection);
+ `);
+ // Different state variable name, so state is reset.
+ expect(container.firstChild).not.toBe(el);
+ const newEl = container.firstChild;
+ expect(newEl.textContent).toBe('C3');
+ }
+ });
- // This is best effort for simple cases.
- // We won't attempt to resolve identifiers.
- it('resets state when useReducer initial state is edited', () => {
- if (__DEV__) {
- render(`
- const {useReducer} = React;
+ it('resets effects while preserving state', () => {
+ if (__DEV__) {
+ render(`
+ const {useState} = React;
- export default function App() {
- const [foo, setFoo] = useReducer(x => x, 1);
- return A{foo}
;
- }
- `);
- let el = container.firstChild;
- expect(el.textContent).toBe('A1');
+ export default function App() {
+ const [value, setValue] = useState(0);
+ return A{value}
;
+ }
+ `);
+ let el = container.firstChild;
+ expect(el.textContent).toBe('A0');
- patch(`
- const {useReducer} = React;
+ // Add an effect.
+ patch(`
+ const {useState} = React;
+
+ export default function App() {
+ const [value, setValue] = useState(0);
+ React.useEffect(() => {
+ const id = setInterval(() => {
+ setValue(v => v + 1);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+ return B{value}
;
+ }
+ `);
+ // We added an effect, thereby changing Hook order.
+ // This causes a remount.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('B0');
- export default function App() {
- const [foo, setFoo] = useReducer(x => x, 1);
- return B{foo}
;
- }
- `);
- // Same initial state, so it's preserved.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('B1');
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+ expect(el.textContent).toBe('B1');
- patch(`
- const {useReducer} = React;
+ patch(`
+ const {useState} = React;
+
+ export default function App() {
+ const [value, setValue] = useState(0);
+ React.useEffect(() => {
+ const id = setInterval(() => {
+ setValue(v => v + 10);
+ }, 1000);
+ return () => clearInterval(id);
+ }, []);
+ return C{value}
;
+ }
+ `);
+ // Same Hooks are called, so state is preserved.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('C1');
- export default function App() {
- const [foo, setFoo] = useReducer(x => x, 2);
- return C{foo}
;
- }
- `);
- // Different initial state, so state is reset.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('C2');
- }
- });
+ // Effects are always reset, so timer was reinstalled.
+ // The new version increments by 10 rather than 1.
+ act(() => {
+ jest.advanceTimersByTime(1000);
+ });
+ expect(el.textContent).toBe('C11');
- describe('with inline requires', () => {
- beforeEach(() => {
- global.FakeModuleSystem = {};
- });
+ patch(`
+ const {useState} = React;
- afterEach(() => {
- delete global.FakeModuleSystem;
+ export default function App() {
+ const [value, setValue] = useState(0);
+ return D{value}
;
+ }
+ `);
+ // Removing the effect changes the signature
+ // and causes the component to remount.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('D0');
+ }
});
- it('remounts component if custom hook it uses changes order on first edit', () => {
- // This test verifies that remounting works even if calls to custom Hooks
- // were transformed with an inline requires transform, like we have on RN.
- // Inline requires make it harder to compare previous and next signatures
- // because useFancyState inline require always resolves to the newest version.
- // We're not actually using inline requires in the test, but it has similar semantics.
+ it('does not get confused when custom hooks are reordered', () => {
if (__DEV__) {
render(`
- const FakeModuleSystem = global.FakeModuleSystem;
-
- FakeModuleSystem.useFancyState = function(initialState) {
+ function useFancyState(initialState) {
return React.useState(initialState);
- };
+ }
- const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ const App = () => {
+ const [x, setX] = useFancyState('X');
+ const [y, setY] = useFancyState('Y');
return A{x}{y}
;
};
@@ -989,124 +561,174 @@ describe('ReactFreshIntegration', () => {
expect(el.textContent).toBe('AXY');
patch(`
- const FakeModuleSystem = global.FakeModuleSystem;
-
- FakeModuleSystem.useFancyState = function(initialState) {
- React.useEffect(() => {});
+ function useFancyState(initialState) {
return React.useState(initialState);
- };
+ }
const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ const [x, setX] = useFancyState('X');
+ const [y, setY] = useFancyState('Y');
return B{x}{y}
;
};
export default App;
`);
- // The useFancyState Hook added an effect,
- // so we had to remount the component.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
+ // Same state variables, so no remount.
+ expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('BXY');
patch(`
- const FakeModuleSystem = global.FakeModuleSystem;
-
- FakeModuleSystem.useFancyState = function(initialState) {
- React.useEffect(() => {});
+ function useFancyState(initialState) {
return React.useState(initialState);
- };
+ }
const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
- return C{x}{y}
;
+ const [y, setY] = useFancyState('Y');
+ const [x, setX] = useFancyState('X');
+ return B{x}{y}
;
};
export default App;
`);
- // We didn't change anything except the header text.
- // So we don't expect a remount.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('CXY');
+ // Hooks were re-ordered. This causes a remount.
+ // Therefore, Hook calls don't accidentally share state.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('BXY');
}
});
- it('remounts component if custom hook it uses changes order on second edit', () => {
+ it('does not get confused by Hooks defined inline', () => {
+ // This is not a recommended pattern but at least it shouldn't break.
if (__DEV__) {
render(`
- const FakeModuleSystem = global.FakeModuleSystem;
+ const App = () => {
+ const useFancyState = (initialState) => {
+ const result = React.useState(initialState);
+ return result;
+ };
+ const [x, setX] = useFancyState('X1');
+ const [y, setY] = useFancyState('Y1');
+ return A{x}{y}
;
+ };
- FakeModuleSystem.useFancyState = function(initialState) {
- return React.useState(initialState);
+ export default App;
+ `);
+ let el = container.firstChild;
+ expect(el.textContent).toBe('AX1Y1');
+
+ patch(`
+ const App = () => {
+ const useFancyState = (initialState) => {
+ const result = React.useState(initialState);
+ return result;
+ };
+ const [x, setX] = useFancyState('X2');
+ const [y, setY] = useFancyState('Y2');
+ return B{x}{y}
;
};
+ export default App;
+ `);
+ // Remount even though nothing changed because
+ // the custom Hook is inside -- and so we don't
+ // really know whether its signature has changed.
+ // We could potentially make it work, but for now
+ // let's assert we don't crash with confusing errors.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('BX2Y2');
+ }
+ });
+
+ it('remounts component if custom hook it uses changes order', () => {
+ if (__DEV__) {
+ render(`
const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ const [x, setX] = useFancyState('X');
+ const [y, setY] = useFancyState('Y');
return A{x}{y}
;
};
+ const useFancyState = (initialState) => {
+ const result = useIndirection(initialState);
+ return result;
+ };
+
+ function useIndirection(initialState) {
+ return React.useState(initialState);
+ }
+
export default App;
`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
- const FakeModuleSystem = global.FakeModuleSystem;
-
- FakeModuleSystem.useFancyState = function(initialState) {
- return React.useState(initialState);
- };
-
const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ const [x, setX] = useFancyState('X');
+ const [y, setY] = useFancyState('Y');
return B{x}{y}
;
};
+ const useFancyState = (initialState) => {
+ const result = useIndirection();
+ return result;
+ };
+
+ function useIndirection(initialState) {
+ return React.useState(initialState);
+ }
+
export default App;
`);
+ // We didn't change anything except the header text.
+ // So we don't expect a remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('BXY');
patch(`
- const FakeModuleSystem = global.FakeModuleSystem;
-
- FakeModuleSystem.useFancyState = function(initialState) {
- React.useEffect(() => {});
- return React.useState(initialState);
- };
-
const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ const [x, setX] = useFancyState('X');
+ const [y, setY] = useFancyState('Y');
return C{x}{y}
;
};
+ const useFancyState = (initialState) => {
+ const result = useIndirection(initialState);
+ return result;
+ };
+
+ function useIndirection(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ }
+
export default App;
`);
- // The useFancyState Hook added an effect,
+ // The useIndirection Hook added an affect,
// so we had to remount the component.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('CXY');
patch(`
- const FakeModuleSystem = global.FakeModuleSystem;
-
- FakeModuleSystem.useFancyState = function(initialState) {
- React.useEffect(() => {});
- return React.useState(initialState);
- };
-
const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ const [x, setX] = useFancyState('X');
+ const [y, setY] = useFancyState('Y');
return D{x}{y}
;
};
+ const useFancyState = (initialState) => {
+ const result = useIndirection();
+ return result;
+ };
+
+ function useIndirection(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ }
+
export default App;
`);
// We didn't change anything except the header text.
@@ -1116,140 +738,537 @@ describe('ReactFreshIntegration', () => {
}
});
- it('recovers if evaluating Hook list throws', () => {
+ it('does not lose the inferred arrow names', () => {
+ if (__DEV__) {
+ render(`
+ const Parent = () => {
+ return ;
+ };
+
+ const Child = () => {
+ useMyThing();
+ return {Parent.name} {Child.name} {useMyThing.name}
;
+ };
+
+ const useMyThing = () => {
+ React.useState();
+ };
+
+ export default Parent;
+ `);
+ expect(container.textContent).toBe('Parent Child useMyThing');
+ }
+ });
+
+ it('does not lose the inferred function names', () => {
if (__DEV__) {
render(`
- let FakeModuleSystem = null;
+ var Parent = function() {
+ return ;
+ };
+
+ var Child = function() {
+ useMyThing();
+ return {Parent.name} {Child.name} {useMyThing.name}
;
+ };
+
+ var useMyThing = function() {
+ React.useState();
+ };
- global.FakeModuleSystem.useFancyState = function(initialState) {
- return React.useState(initialState);
- };
+ export default Parent;
+ `);
+ expect(container.textContent).toBe('Parent Child useMyThing');
+ }
+ });
- const App = () => {
- FakeModuleSystem = global.FakeModuleSystem;
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
- return A{x}{y}
;
- };
+ it('resets state on every edit with @hot reset annotation', () => {
+ if (__DEV__) {
+ render(`
+ const {useState} = React;
+ const S = 1;
- export default App;
- `);
+ export default function App() {
+ const [foo, setFoo] = useState(S);
+ return A{foo}
;
+ }
+ `);
let el = container.firstChild;
- expect(el.textContent).toBe('AXY');
+ expect(el.textContent).toBe('A1');
patch(`
- let FakeModuleSystem = null;
-
- global.FakeModuleSystem.useFancyState = function(initialState) {
- React.useEffect(() => {});
- return React.useState(initialState);
- };
-
- const App = () => {
- FakeModuleSystem = global.FakeModuleSystem;
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
- return B{x}{y}
;
- };
-
- export default App;
- `);
- // We couldn't evaluate the Hook signatures
- // so we had to remount the component.
+ const {useState} = React;
+ const S = 2;
+
+ export default function App() {
+ const [foo, setFoo] = useState(S);
+ return B{foo}
;
+ }
+ `);
+ // Same state variable name, so state is preserved.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B1');
+
+ patch(`
+ const {useState} = React;
+ const S = 3;
+
+ /* @hot reset */
+
+ export default function App() {
+ const [foo, setFoo] = useState(S);
+ return C{foo}
;
+ }
+ `);
+ // Found remount annotation, so state is reset.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
- expect(el.textContent).toBe('BXY');
+ expect(el.textContent).toBe('C3');
+
+ patch(`
+ const {useState} = React;
+ const S = 4;
+
+ export default function App() {
+
+ // @hot reset
+
+ const [foo, setFoo] = useState(S);
+ return D{foo}
;
+ }
+ `);
+ // Found remount annotation, so state is reset.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('D4');
+
+ patch(`
+ const {useState} = React;
+ const S = 5;
+
+ export default function App() {
+ const [foo, setFoo] = useState(S);
+ return E{foo}
;
+ }
+ `);
+ // There is no remount annotation anymore,
+ // so preserve the previous state.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('E4');
+
+ patch(`
+ const {useState} = React;
+ const S = 6;
+
+ export default function App() {
+ const [foo, setFoo] = useState(S);
+ return F{foo}
;
+ }
+ `);
+ // Continue editing.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('F4');
+
+ patch(`
+ const {useState} = React;
+ const S = 7;
+
+ export default function App() {
+
+ /* @hot reset */
+
+ const [foo, setFoo] = useState(S);
+ return G{foo}
;
+ }
+ `);
+ // Force remount one last time.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('G7');
}
});
- it('remounts component if custom hook it uses changes order behind an indirection', () => {
+ // This is best effort for simple cases.
+ // We won't attempt to resolve identifiers.
+ it('resets state when useState initial state is edited', () => {
if (__DEV__) {
render(`
- const FakeModuleSystem = global.FakeModuleSystem;
+ const {useState} = React;
- FakeModuleSystem.useFancyState = function(initialState) {
- return FakeModuleSystem.useIndirection(initialState);
- };
+ export default function App() {
+ const [foo, setFoo] = useState(1);
+ return A{foo}
;
+ }
+ `);
+ let el = container.firstChild;
+ expect(el.textContent).toBe('A1');
- FakeModuleSystem.useIndirection = function(initialState) {
- return FakeModuleSystem.useOtherIndirection(initialState);
- };
+ patch(`
+ const {useState} = React;
- FakeModuleSystem.useOtherIndirection = function(initialState) {
- return React.useState(initialState);
- };
+ export default function App() {
+ const [foo, setFoo] = useState(1);
+ return B{foo}
;
+ }
+ `);
+ // Same initial state, so it's preserved.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B1');
- const App = () => {
- const [x, setX] = FakeModuleSystem.useFancyState('X');
- const [y, setY] = FakeModuleSystem.useFancyState('Y');
- return A{x}{y}
;
- };
+ patch(`
+ const {useState} = React;
- export default App;
+ export default function App() {
+ const [foo, setFoo] = useState(2);
+ return C{foo}
;
+ }
+ `);
+ // Different initial state, so state is reset.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('C2');
+ }
+ });
+
+ // This is best effort for simple cases.
+ // We won't attempt to resolve identifiers.
+ it('resets state when useReducer initial state is edited', () => {
+ if (__DEV__) {
+ render(`
+ const {useReducer} = React;
+
+ export default function App() {
+ const [foo, setFoo] = useReducer(x => x, 1);
+ return A{foo}
;
+ }
`);
let el = container.firstChild;
- expect(el.textContent).toBe('AXY');
+ expect(el.textContent).toBe('A1');
patch(`
- const FakeModuleSystem = global.FakeModuleSystem;
+ const {useReducer} = React;
- FakeModuleSystem.useFancyState = function(initialState) {
- return FakeModuleSystem.useIndirection(initialState);
- };
+ export default function App() {
+ const [foo, setFoo] = useReducer(x => x, 1);
+ return B{foo}
;
+ }
+ `);
+ // Same initial state, so it's preserved.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('B1');
- FakeModuleSystem.useIndirection = function(initialState) {
- return FakeModuleSystem.useOtherIndirection(initialState);
- };
+ patch(`
+ const {useReducer} = React;
- FakeModuleSystem.useOtherIndirection = function(initialState) {
- React.useEffect(() => {});
+ export default function App() {
+ const [foo, setFoo] = useReducer(x => x, 2);
+ return C{foo}
;
+ }
+ `);
+ // Different initial state, so state is reset.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('C2');
+ }
+ });
+
+ describe('with inline requires', () => {
+ beforeEach(() => {
+ global.FakeModuleSystem = {};
+ });
+
+ afterEach(() => {
+ delete global.FakeModuleSystem;
+ });
+
+ it('remounts component if custom hook it uses changes order on first edit', () => {
+ // This test verifies that remounting works even if calls to custom Hooks
+ // were transformed with an inline requires transform, like we have on RN.
+ // Inline requires make it harder to compare previous and next signatures
+ // because useFancyState inline require always resolves to the newest version.
+ // We're not actually using inline requires in the test, but it has similar semantics.
+ if (__DEV__) {
+ render(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return A{x}{y}
;
+ };
+
+ export default App;
+ `);
+ let el = container.firstChild;
+ expect(el.textContent).toBe('AXY');
+
+ patch(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return B{x}{y}
;
+ };
+
+ export default App;
+ `);
+ // The useFancyState Hook added an effect,
+ // so we had to remount the component.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('BXY');
+
+ patch(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return C{x}{y}
;
+ };
+
+ export default App;
+ `);
+ // We didn't change anything except the header text.
+ // So we don't expect a remount.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('CXY');
+ }
+ });
+
+ it('remounts component if custom hook it uses changes order on second edit', () => {
+ if (__DEV__) {
+ render(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return A{x}{y}
;
+ };
+
+ export default App;
+ `);
+ let el = container.firstChild;
+ expect(el.textContent).toBe('AXY');
+
+ patch(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return B{x}{y}
;
+ };
+
+ export default App;
+ `);
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('BXY');
+
+ patch(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return C{x}{y}
;
+ };
+
+ export default App;
+ `);
+ // The useFancyState Hook added an effect,
+ // so we had to remount the component.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('CXY');
+
+ patch(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return D{x}{y}
;
+ };
+
+ export default App;
+ `);
+ // We didn't change anything except the header text.
+ // So we don't expect a remount.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('DXY');
+ }
+ });
+
+ it('recovers if evaluating Hook list throws', () => {
+ if (__DEV__) {
+ render(`
+ let FakeModuleSystem = null;
+
+ global.FakeModuleSystem.useFancyState = function(initialState) {
return React.useState(initialState);
};
const App = () => {
+ FakeModuleSystem = global.FakeModuleSystem;
const [x, setX] = FakeModuleSystem.useFancyState('X');
const [y, setY] = FakeModuleSystem.useFancyState('Y');
- return B{x}{y}
;
+ return A{x}{y}
;
};
export default App;
`);
+ let el = container.firstChild;
+ expect(el.textContent).toBe('AXY');
- // The useFancyState Hook added an effect,
- // so we had to remount the component.
- expect(container.firstChild).not.toBe(el);
- el = container.firstChild;
- expect(el.textContent).toBe('BXY');
-
- patch(`
- const FakeModuleSystem = global.FakeModuleSystem;
-
- FakeModuleSystem.useFancyState = function(initialState) {
- return FakeModuleSystem.useIndirection(initialState);
- };
-
- FakeModuleSystem.useIndirection = function(initialState) {
- return FakeModuleSystem.useOtherIndirection(initialState);
- };
+ patch(`
+ let FakeModuleSystem = null;
- FakeModuleSystem.useOtherIndirection = function(initialState) {
+ global.FakeModuleSystem.useFancyState = function(initialState) {
React.useEffect(() => {});
return React.useState(initialState);
};
const App = () => {
+ FakeModuleSystem = global.FakeModuleSystem;
const [x, setX] = FakeModuleSystem.useFancyState('X');
const [y, setY] = FakeModuleSystem.useFancyState('Y');
- return C{x}{y}
;
+ return B{x}{y}
;
};
export default App;
`);
- // We didn't change anything except the header text.
- // So we don't expect a remount.
- expect(container.firstChild).toBe(el);
- expect(el.textContent).toBe('CXY');
- }
+ // We couldn't evaluate the Hook signatures
+ // so we had to remount the component.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('BXY');
+ }
+ });
+
+ it('remounts component if custom hook it uses changes order behind an indirection', () => {
+ if (__DEV__) {
+ render(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ return FakeModuleSystem.useIndirection(initialState);
+ };
+
+ FakeModuleSystem.useIndirection = function(initialState) {
+ return FakeModuleSystem.useOtherIndirection(initialState);
+ };
+
+ FakeModuleSystem.useOtherIndirection = function(initialState) {
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return A{x}{y}
;
+ };
+
+ export default App;
+ `);
+ let el = container.firstChild;
+ expect(el.textContent).toBe('AXY');
+
+ patch(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ return FakeModuleSystem.useIndirection(initialState);
+ };
+
+ FakeModuleSystem.useIndirection = function(initialState) {
+ return FakeModuleSystem.useOtherIndirection(initialState);
+ };
+
+ FakeModuleSystem.useOtherIndirection = function(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return B{x}{y}
;
+ };
+
+ export default App;
+ `);
+
+ // The useFancyState Hook added an effect,
+ // so we had to remount the component.
+ expect(container.firstChild).not.toBe(el);
+ el = container.firstChild;
+ expect(el.textContent).toBe('BXY');
+
+ patch(`
+ const FakeModuleSystem = global.FakeModuleSystem;
+
+ FakeModuleSystem.useFancyState = function(initialState) {
+ return FakeModuleSystem.useIndirection(initialState);
+ };
+
+ FakeModuleSystem.useIndirection = function(initialState) {
+ return FakeModuleSystem.useOtherIndirection(initialState);
+ };
+
+ FakeModuleSystem.useOtherIndirection = function(initialState) {
+ React.useEffect(() => {});
+ return React.useState(initialState);
+ };
+
+ const App = () => {
+ const [x, setX] = FakeModuleSystem.useFancyState('X');
+ const [y, setY] = FakeModuleSystem.useFancyState('Y');
+ return C{x}{y}
;
+ };
+
+ export default App;
+ `);
+ // We didn't change anything except the header text.
+ // So we don't expect a remount.
+ expect(container.firstChild).toBe(el);
+ expect(el.textContent).toBe('CXY');
+ }
+ });
});
- });
+ }
});