main
js 240 lines 5.51 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 * @jest-environment node
9 */
10
11 'use strict';
12
13 describe('useRef', () => {
14 let React;
15 let ReactNoop;
16 let Scheduler;
17 let act;
18 let useCallback;
19 let useEffect;
20 let useLayoutEffect;
21 let useRef;
22 let useState;
23 let waitForAll;
24 let assertLog;
25
26 beforeEach(() => {
27 React = require('react');
28 ReactNoop = require('react-noop-renderer');
29 Scheduler = require('scheduler');
30
31 act = require('internal-test-utils').act;
32 useCallback = React.useCallback;
33 useEffect = React.useEffect;
34 useLayoutEffect = React.useLayoutEffect;
35 useRef = React.useRef;
36 useState = React.useState;
37
38 const InternalTestUtils = require('internal-test-utils');
39 waitForAll = InternalTestUtils.waitForAll;
40 assertLog = InternalTestUtils.assertLog;
41 });
42
43 function Text(props) {
44 Scheduler.log(props.text);
45 return <span prop={props.text} />;
46 }
47
48 it('creates a ref object initialized with the provided value', async () => {
49 jest.useFakeTimers();
50
51 function useDebouncedCallback(callback, ms, inputs) {
52 const timeoutID = useRef(-1);
53 useEffect(() => {
54 return function unmount() {
55 clearTimeout(timeoutID.current);
56 };
57 }, []);
58 const debouncedCallback = useCallback(
59 (...args) => {
60 clearTimeout(timeoutID.current);
61 timeoutID.current = setTimeout(callback, ms, ...args);
62 },
63 [callback, ms],
64 );
65 return useCallback(debouncedCallback, inputs);
66 }
67
68 let ping;
69 function App() {
70 ping = useDebouncedCallback(
71 value => {
72 Scheduler.log('ping: ' + value);
73 },
74 100,
75 [],
76 );
77 return null;
78 }
79
80 await act(() => {
81 ReactNoop.render(<App />);
82 });
83 assertLog([]);
84
85 ping(1);
86 ping(2);
87 ping(3);
88
89 assertLog([]);
90
91 jest.advanceTimersByTime(100);
92
93 assertLog(['ping: 3']);
94
95 ping(4);
96 jest.advanceTimersByTime(20);
97 ping(5);
98 ping(6);
99 jest.advanceTimersByTime(80);
100
101 assertLog([]);
102
103 jest.advanceTimersByTime(20);
104 assertLog(['ping: 6']);
105 });
106
107 it('should return the same ref during re-renders', async () => {
108 function Counter() {
109 const ref = useRef('val');
110 const [count, setCount] = useState(0);
111 const [firstRef] = useState(ref);
112
113 if (firstRef !== ref) {
114 throw new Error('should never change');
115 }
116
117 if (count < 3) {
118 setCount(count + 1);
119 }
120
121 return <Text text={count} />;
122 }
123
124 ReactNoop.render(<Counter />);
125 await waitForAll([3]);
126
127 ReactNoop.render(<Counter />);
128 await waitForAll([3]);
129 });
130
131 if (__DEV__) {
132 it('should never warn when attaching to children', async () => {
133 class Component extends React.Component {
134 render() {
135 return null;
136 }
137 }
138
139 function Example({phase}) {
140 const hostRef = useRef();
141 const classRef = useRef();
142 return (
143 <>
144 <div key={`host-${phase}`} ref={hostRef} />
145 <Component key={`class-${phase}`} ref={classRef} />
146 </>
147 );
148 }
149
150 await act(() => {
151 ReactNoop.render(<Example phase="mount" />);
152 });
153 await act(() => {
154 ReactNoop.render(<Example phase="update" />);
155 });
156 });
157
158 it('should not warn about lazy init during render', async () => {
159 function Example() {
160 const ref1 = useRef(null);
161 const ref2 = useRef(undefined);
162 // Read: safe because lazy init:
163 if (ref1.current === null) {
164 ref1.current = 123;
165 }
166 if (ref2.current === undefined) {
167 ref2.current = 123;
168 }
169 return null;
170 }
171
172 await act(() => {
173 ReactNoop.render(<Example />);
174 });
175
176 // Should not warn after an update either.
177 await act(() => {
178 ReactNoop.render(<Example />);
179 });
180 });
181
182 it('should not warn about lazy init outside of render', async () => {
183 function Example() {
184 // eslint-disable-next-line no-unused-vars
185 const [didMount, setDidMount] = useState(false);
186 const ref1 = useRef(null);
187 const ref2 = useRef(undefined);
188 useLayoutEffect(() => {
189 ref1.current = 123;
190 ref2.current = 123;
191 setDidMount(true);
192 }, []);
193 return null;
194 }
195
196 await act(() => {
197 ReactNoop.render(<Example />);
198 });
199 });
200
201 it('should not warn about reads or writes within effect', async () => {
202 function Example() {
203 const ref = useRef(123);
204 useLayoutEffect(() => {
205 expect(ref.current).toBe(123);
206 ref.current = 456;
207 expect(ref.current).toBe(456);
208 }, []);
209 useEffect(() => {
210 expect(ref.current).toBe(456);
211 ref.current = 789;
212 expect(ref.current).toBe(789);
213 }, []);
214 return null;
215 }
216
217 await act(() => {
218 ReactNoop.render(<Example />);
219 });
220
221 ReactNoop.flushPassiveEffects();
222 });
223
224 it('should not warn about reads or writes outside of render phase (e.g. event handler)', async () => {
225 let ref;
226 function Example() {
227 ref = useRef(123);
228 return null;
229 }
230
231 await act(() => {
232 ReactNoop.render(<Example />);
233 });
234
235 expect(ref.current).toBe(123);
236 ref.current = 456;
237 expect(ref.current).toBe(456);
238 });
239 }
240 });