main
js 316 lines 9.88 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 let React;
14 let ReactNoop;
15 let Scheduler;
16 let act;
17 let waitForAll;
18 let waitFor;
19 let assertLog;
20 let waitForPaint;
21
22 describe('ReactIncrementalScheduling', () => {
23 beforeEach(() => {
24 jest.resetModules();
25
26 React = require('react');
27 ReactNoop = require('react-noop-renderer');
28 Scheduler = require('scheduler');
29 act = require('internal-test-utils').act;
30
31 const InternalTestUtils = require('internal-test-utils');
32 waitForAll = InternalTestUtils.waitForAll;
33 waitFor = InternalTestUtils.waitFor;
34 assertLog = InternalTestUtils.assertLog;
35 waitForPaint = InternalTestUtils.waitForPaint;
36 });
37
38 it('schedules and flushes deferred work', async () => {
39 ReactNoop.render(<span prop="1" />);
40 expect(ReactNoop).toMatchRenderedOutput(null);
41
42 await waitForAll([]);
43 expect(ReactNoop).toMatchRenderedOutput(<span prop="1" />);
44 });
45
46 it('searches for work on other roots once the current root completes', async () => {
47 ReactNoop.renderToRootWithID(<span prop="a:1" />, 'a');
48 ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
49 ReactNoop.renderToRootWithID(<span prop="c:1" />, 'c');
50
51 await waitForAll([]);
52
53 expect(ReactNoop.getChildrenAsJSX('a')).toEqual(<span prop="a:1" />);
54 expect(ReactNoop.getChildrenAsJSX('b')).toEqual(<span prop="b:1" />);
55 expect(ReactNoop.getChildrenAsJSX('c')).toEqual(<span prop="c:1" />);
56 });
57
58 it('schedules top-level updates in order of priority', async () => {
59 // Initial render.
60 ReactNoop.render(<span prop={1} />);
61 await waitForAll([]);
62 expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
63
64 ReactNoop.batchedUpdates(() => {
65 ReactNoop.render(<span prop={5} />);
66 ReactNoop.flushSync(() => {
67 ReactNoop.render(<span prop={2} />);
68 ReactNoop.render(<span prop={3} />);
69 ReactNoop.render(<span prop={4} />);
70 });
71 });
72 // The sync updates flush first.
73 expect(ReactNoop).toMatchRenderedOutput(<span prop={4} />);
74
75 // The terminal value should be the last update that was scheduled,
76 // regardless of priority. In this case, that's the last sync update.
77 await waitForAll([]);
78 expect(ReactNoop).toMatchRenderedOutput(<span prop={4} />);
79 });
80
81 it('schedules top-level updates with same priority in order of insertion', async () => {
82 // Initial render.
83 ReactNoop.render(<span prop={1} />);
84 await waitForAll([]);
85 expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
86
87 ReactNoop.render(<span prop={2} />);
88 ReactNoop.render(<span prop={3} />);
89 ReactNoop.render(<span prop={4} />);
90 ReactNoop.render(<span prop={5} />);
91
92 await waitForAll([]);
93 expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
94 });
95
96 it('works on deferred roots in the order they were scheduled', async () => {
97 const {useEffect} = React;
98 function Text({text}) {
99 useEffect(() => {
100 Scheduler.log(text);
101 }, [text]);
102 return text;
103 }
104
105 await act(() => {
106 ReactNoop.renderToRootWithID(<Text text="a:1" />, 'a');
107 ReactNoop.renderToRootWithID(<Text text="b:1" />, 'b');
108 ReactNoop.renderToRootWithID(<Text text="c:1" />, 'c');
109 });
110 assertLog(['a:1', 'b:1', 'c:1']);
111
112 expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
113 expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
114 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:1');
115
116 // Schedule deferred work in the reverse order
117 await act(async () => {
118 React.startTransition(() => {
119 ReactNoop.renderToRootWithID(<Text text="c:2" />, 'c');
120 ReactNoop.renderToRootWithID(<Text text="b:2" />, 'b');
121 });
122 // Ensure it starts in the order it was scheduled
123 await waitFor(['c:2']);
124
125 expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
126 expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:1');
127 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
128 // Schedule last bit of work, it will get processed the last
129
130 React.startTransition(() => {
131 ReactNoop.renderToRootWithID(<Text text="a:2" />, 'a');
132 });
133
134 // Keep performing work in the order it was scheduled
135 await waitFor(['b:2']);
136 expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:1');
137 expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
138 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
139
140 await waitFor(['a:2']);
141 expect(ReactNoop.getChildrenAsJSX('a')).toEqual('a:2');
142 expect(ReactNoop.getChildrenAsJSX('b')).toEqual('b:2');
143 expect(ReactNoop.getChildrenAsJSX('c')).toEqual('c:2');
144 });
145 });
146
147 it('schedules sync updates when inside componentDidMount/Update', async () => {
148 let instance;
149
150 class Foo extends React.Component {
151 state = {tick: 0};
152
153 componentDidMount() {
154 Scheduler.log(
155 'componentDidMount (before setState): ' + this.state.tick,
156 );
157 this.setState({tick: 1});
158 // We're in a batch. Update hasn't flushed yet.
159 Scheduler.log('componentDidMount (after setState): ' + this.state.tick);
160 }
161
162 componentDidUpdate() {
163 Scheduler.log('componentDidUpdate: ' + this.state.tick);
164 if (this.state.tick === 2) {
165 Scheduler.log(
166 'componentDidUpdate (before setState): ' + this.state.tick,
167 );
168 this.setState({tick: 3});
169 Scheduler.log(
170 'componentDidUpdate (after setState): ' + this.state.tick,
171 );
172 // We're in a batch. Update hasn't flushed yet.
173 }
174 }
175
176 render() {
177 Scheduler.log('render: ' + this.state.tick);
178 instance = this;
179 return <span prop={this.state.tick} />;
180 }
181 }
182
183 React.startTransition(() => {
184 ReactNoop.render(<Foo />);
185 });
186 // Render without committing
187 await waitFor(['render: 0']);
188
189 // Do one more unit of work to commit
190 expect(ReactNoop.flushNextYield()).toEqual([
191 'componentDidMount (before setState): 0',
192 'componentDidMount (after setState): 0',
193 // If the setState inside componentDidMount were deferred, there would be
194 // no more ops. Because it has Task priority, we get these ops, too:
195 'render: 1',
196 'componentDidUpdate: 1',
197 ]);
198
199 React.startTransition(() => {
200 instance.setState({tick: 2});
201 });
202 await waitFor(['render: 2']);
203 expect(ReactNoop.flushNextYield()).toEqual([
204 'componentDidUpdate: 2',
205 'componentDidUpdate (before setState): 2',
206 'componentDidUpdate (after setState): 2',
207 // If the setState inside componentDidUpdate were deferred, there would be
208 // no more ops. Because it has Task priority, we get these ops, too:
209 'render: 3',
210 'componentDidUpdate: 3',
211 ]);
212 });
213
214 it('can opt-in to async scheduling inside componentDidMount/Update', async () => {
215 let instance;
216 class Foo extends React.Component {
217 state = {tick: 0};
218
219 componentDidMount() {
220 React.startTransition(() => {
221 Scheduler.log(
222 'componentDidMount (before setState): ' + this.state.tick,
223 );
224 this.setState({tick: 1});
225 Scheduler.log(
226 'componentDidMount (after setState): ' + this.state.tick,
227 );
228 });
229 }
230
231 componentDidUpdate() {
232 React.startTransition(() => {
233 Scheduler.log('componentDidUpdate: ' + this.state.tick);
234 if (this.state.tick === 2) {
235 Scheduler.log(
236 'componentDidUpdate (before setState): ' + this.state.tick,
237 );
238 this.setState({tick: 3});
239 Scheduler.log(
240 'componentDidUpdate (after setState): ' + this.state.tick,
241 );
242 }
243 });
244 }
245
246 render() {
247 Scheduler.log('render: ' + this.state.tick);
248 instance = this;
249 return <span prop={this.state.tick} />;
250 }
251 }
252
253 ReactNoop.flushSync(() => {
254 ReactNoop.render(<Foo />);
255 });
256 // The cDM update should not have flushed yet because it has async priority.
257 assertLog([
258 'render: 0',
259 'componentDidMount (before setState): 0',
260 'componentDidMount (after setState): 0',
261 ]);
262 expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
263
264 // Now flush the cDM update.
265 await waitForAll(['render: 1', 'componentDidUpdate: 1']);
266 expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
267
268 React.startTransition(() => {
269 instance.setState({tick: 2});
270 });
271
272 await waitForPaint([
273 'render: 2',
274 'componentDidUpdate: 2',
275 'componentDidUpdate (before setState): 2',
276 'componentDidUpdate (after setState): 2',
277 ]);
278 expect(ReactNoop).toMatchRenderedOutput(<span prop={2} />);
279
280 // Now flush the cDU update.
281 await waitForAll(['render: 3', 'componentDidUpdate: 3']);
282 expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);
283 });
284
285 it('performs Task work even after time runs out', async () => {
286 class Foo extends React.Component {
287 state = {step: 1};
288 componentDidMount() {
289 this.setState({step: 2}, () => {
290 this.setState({step: 3}, () => {
291 this.setState({step: 4}, () => {
292 this.setState({step: 5});
293 });
294 });
295 });
296 }
297 render() {
298 Scheduler.log('Foo');
299 return <span prop={this.state.step} />;
300 }
301 }
302 React.startTransition(() => {
303 ReactNoop.render(<Foo />);
304 });
305
306 // This should be just enough to complete all the work, but not enough to
307 // commit it.
308 await waitFor(['Foo']);
309 expect(ReactNoop).toMatchRenderedOutput(null);
310
311 // Do one more unit of work.
312 ReactNoop.flushNextYield();
313 // The updates should all be flushed with Task priority
314 expect(ReactNoop).toMatchRenderedOutput(<span prop={5} />);
315 });
316 });