/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
/* eslint-disable no-func-assign */
'use strict';
describe('useRef', () => {
let React;
let ReactNoop;
let Scheduler;
let act;
let useCallback;
let useEffect;
let useLayoutEffect;
let useRef;
let useState;
let waitForAll;
let assertLog;
beforeEach(() => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
act = require('internal-test-utils').act;
useCallback = React.useCallback;
useEffect = React.useEffect;
useLayoutEffect = React.useLayoutEffect;
useRef = React.useRef;
useState = React.useState;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
});
function Text(props) {
Scheduler.log(props.text);
return <span prop={props.text} />;
}
it('creates a ref object initialized with the provided value', async () => {
jest.useFakeTimers();
function useDebouncedCallback(callback, ms, inputs) {
const timeoutID = useRef(-1);
useEffect(() => {
return function unmount() {
clearTimeout(timeoutID.current);
};
}, []);
const debouncedCallback = useCallback(
(...args) => {
clearTimeout(timeoutID.current);
timeoutID.current = setTimeout(callback, ms, ...args);
},
[callback, ms],
);
return useCallback(debouncedCallback, inputs);
}
let ping;
function App() {
ping = useDebouncedCallback(
value => {
Scheduler.log('ping: ' + value);
},
100,
[],
);
return null;
}
await act(() => {
ReactNoop.render(<App />);
});
assertLog([]);
ping(1);
ping(2);
ping(3);
assertLog([]);
jest.advanceTimersByTime(100);
assertLog(['ping: 3']);
ping(4);
jest.advanceTimersByTime(20);
ping(5);
ping(6);
jest.advanceTimersByTime(80);
assertLog([]);
jest.advanceTimersByTime(20);
assertLog(['ping: 6']);
});
it('should return the same ref during re-renders', async () => {
function Counter() {
const ref = useRef('val');
const [count, setCount] = useState(0);
const [firstRef] = useState(ref);
if (firstRef !== ref) {
throw new Error('should never change');
}
if (count < 3) {
setCount(count + 1);
}
return <Text text={count} />;
}
ReactNoop.render(<Counter />);
await waitForAll([3]);
ReactNoop.render(<Counter />);
await waitForAll([3]);
});
if (__DEV__) {
it('should never warn when attaching to children', async () => {
class Component extends React.Component {
render() {
return null;
}
}
function Example({phase}) {
const hostRef = useRef();
const classRef = useRef();
return (
<>
<div key={`host-${phase}`} ref={hostRef} />
<Component key={`class-${phase}`} ref={classRef} />
</>
);
}
await act(() => {
ReactNoop.render(<Example phase="mount" />);
});
await act(() => {
ReactNoop.render(<Example phase="update" />);
});
});
// @gate enableUseRefAccessWarning
it('should warn about reads during render', async () => {
function Example() {
const ref = useRef(123);
let value;
expect(() => {
value = ref.current;
}).toWarnDev([
'Example: Unsafe read of a mutable value during render.',
]);
return value;
}
await act(() => {
ReactNoop.render(<Example />);
});
});
it('should not warn about lazy init during render', async () => {
function Example() {
const ref1 = useRef(null);
const ref2 = useRef(undefined);
// Read: safe because lazy init:
if (ref1.current === null) {
ref1.current = 123;
}
if (ref2.current === undefined) {
ref2.current = 123;
}
return null;
}
await act(() => {
ReactNoop.render(<Example />);
});
// Should not warn after an update either.
await act(() => {
ReactNoop.render(<Example />);
});
});
it('should not warn about lazy init outside of render', async () => {
function Example() {
// eslint-disable-next-line no-unused-vars
const [didMount, setDidMount] = useState(false);
const ref1 = useRef(null);
const ref2 = useRef(undefined);
useLayoutEffect(() => {
ref1.current = 123;
ref2.current = 123;
setDidMount(true);
}, []);
return null;
}
await act(() => {
ReactNoop.render(<Example />);
});
});
// @gate enableUseRefAccessWarning
it('should warn about unconditional lazy init during render', async () => {
function Example() {
const ref1 = useRef(null);
const ref2 = useRef(undefined);
if (shouldExpectWarning) {
expect(() => {
ref1.current = 123;
}).toWarnDev([
'Example: Unsafe write of a mutable value during render',
]);
expect(() => {
ref2.current = 123;
}).toWarnDev([
'Example: Unsafe write of a mutable value during render',
]);
} else {
ref1.current = 123;
ref1.current = 123;
}
// But only warn once
ref1.current = 345;
ref1.current = 345;
return null;
}
let shouldExpectWarning = true;
await act(() => {
ReactNoop.render(<Example />);
});
// Should not warn again on update.
shouldExpectWarning = false;
await act(() => {
ReactNoop.render(<Example />);
});
});
// @gate enableUseRefAccessWarning
it('should warn about reads to ref after lazy init pattern', async () => {
function Example() {
const ref1 = useRef(null);
const ref2 = useRef(undefined);
// Read 1: safe because lazy init:
if (ref1.current === null) {
ref1.current = 123;
}
if (ref2.current === undefined) {
ref2.current = 123;
}
let value;
expect(() => {
value = ref1.current;
}).toWarnDev(['Example: Unsafe read of a mutable value during render']);
expect(() => {
value = ref2.current;
}).toWarnDev(['Example: Unsafe read of a mutable value during render']);
// But it should only warn once.
value = ref1.current;
value = ref2.current;
return value;
}
await act(() => {
ReactNoop.render(<Example />);
});
});
// @gate enableUseRefAccessWarning
it('should warn about writes to ref after lazy init pattern', async () => {
function Example() {
const ref1 = useRef(null);
const ref2 = useRef(undefined);
// Read: safe because lazy init:
if (ref1.current === null) {
ref1.current = 123;
}
if (ref2.current === undefined) {
ref2.current = 123;
}
expect(() => {
ref1.current = 456;
}).toWarnDev([
'Example: Unsafe write of a mutable value during render',
]);
expect(() => {
ref2.current = 456;
}).toWarnDev([
'Example: Unsafe write of a mutable value during render',
]);
return null;
}
await act(() => {
ReactNoop.render(<Example />);
});
});
it('should not warn about reads or writes within effect', async () => {
function Example() {
const ref = useRef(123);
useLayoutEffect(() => {
expect(ref.current).toBe(123);
ref.current = 456;
expect(ref.current).toBe(456);
}, []);
useEffect(() => {
expect(ref.current).toBe(456);
ref.current = 789;
expect(ref.current).toBe(789);
}, []);
return null;
}
await act(() => {
ReactNoop.render(<Example />);
});
ReactNoop.flushPassiveEffects();
});
it('should not warn about reads or writes outside of render phase (e.g. event handler)', async () => {
let ref;
function Example() {
ref = useRef(123);
return null;
}
await act(() => {
ReactNoop.render(<Example />);
});
expect(ref.current).toBe(123);
ref.current = 456;
expect(ref.current).toBe(456);
});
}
});