@samitouri / QOS-React-2 / commits / ebf9ae8579

useId (#22644)

* Add useId to dispatcher * Initial useId implementation Ids are base 32 strings whose binary representation corresponds to the position of a node in a tree. Every time the tree forks into multiple children, we add additional bits to the left of the sequence that represent the position of the child within the current level of children. 00101 00010001011010101 ╰─┬─╯ ╰───────┬───────╯ Fork 5 of 20 Parent id The leading 0s are important. In the above example, you only need 3 bits to represent slot 5. However, you need 5 bits to represent all the forks at the current level, so we must account for the empty bits at the end. For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise, the zeroth id at a level would be indistinguishable from its parent. If a node has only one child, and does not materialize an id (i.e. does not contain a useId hook), then we don't need to allocate any space in the sequence. It's treated as a transparent indirection. For example, these two trees produce the same ids: <> <> <Indirection> <A /> <A /> <B /> </Indirection> </> <B /> </> However, we cannot skip any materializes an id. Otherwise, a parent id that does not fork would be indistinguishable from its child id. For example, this tree does not fork, but the parent and child must have different ids. <Parent> <Child /> </Parent> To handle this scenario, every time we materialize an id, we allocate a new level with a single slot. You can think of this as a fork with only one prong, or an array of children with length 1. It's possible for the the size of the sequence to exceed 32 bits, the max size for bitwise operations. When this happens, we make more room by converting the right part of the id to a string and storing it in an overflow variable. We use a base 32 string representation, because 32 is the largest power of 2 that is supported by toString(). We want the base to be large so that the resulting ids are compact, and we want the base to be a power of 2 because every log2(base) bits corresponds to a single character, i.e. every log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without affecting the final result. * Incremental hydration Stores the tree context on the dehydrated Suspense boundary's state object so it resume where it left off. * Add useId to react-debug-tools * Add selective hydration test Demonstrates that selective hydration works and ids are preserved even after subsequent client updates.

Andrew Clark committed Nov 1, 2021 at 16:30 UTC ebf9ae8579230e7b1ed0b1d243e1cf802f56938b
38 files changed +1819 -77
packages/react-debug-tools/src/ReactDebugHooks.js
+12
@@ -341,6 +341,17 @@ function useOpaqueIdentifier(): OpaqueIDType | void {
341 return value;
342 }
343
344 +function useId(): string {
345 + const hook = nextHook();
346 + const id = hook !== null ? hook.memoizedState : '';
347 + hookLog.push({
348 + primitive: 'Id',
349 + stackError: new Error(),
350 + value: id,
351 + });
352 + return id;
353 +}
354 +
355 const Dispatcher: DispatcherType = {
356 getCacheForType,
357 readContext,
@@ -361,6 +372,7 @@ const Dispatcher: DispatcherType = {
372 useSyncExternalStore,
373 useDeferredValue,
374 useOpaqueIdentifier,
375 + useId,
376 };
377
378 // Inspect
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+28 -1
@@ -628,7 +628,7 @@ describe('ReactHooksInspectionIntegration', () => {
628 it('should support composite useOpaqueIdentifier hook in concurrent mode', () => {
629 function Foo(props) {
630 const id = React.unstable_useOpaqueIdentifier();
631 - const [state] = React.useState(() => 'hello', []);
631 + const [state] = React.useState('hello');
632 return <div id={id}>{state}</div>;
633 }
634
@@ -656,6 +656,33 @@ describe('ReactHooksInspectionIntegration', () => {
656 });
657 });
658
659 + it('should support useId hook', () => {
660 + function Foo(props) {
661 + const id = React.unstable_useId();
662 + const [state] = React.useState('hello');
663 + return <div id={id}>{state}</div>;
664 + }
665 +
666 + const renderer = ReactTestRenderer.create(<Foo />);
667 + const childFiber = renderer.root.findByType(Foo)._currentFiber();
668 + const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
669 +
670 + expect(tree.length).toEqual(2);
671 +
672 + expect(tree[0].id).toEqual(0);
673 + expect(tree[0].isStateEditable).toEqual(false);
674 + expect(tree[0].name).toEqual('Id');
675 + expect(String(tree[0].value).startsWith('r:')).toBe(true);
676 +
677 + expect(tree[1]).toEqual({
678 + id: 1,
679 + isStateEditable: true,
680 + name: 'State',
681 + value: 'hello',
682 + subHooks: [],
683 + });
684 + });
685 +
686 describe('useDebugValue', () => {
687 it('should support inspectable values for multiple custom hooks', () => {
688 function useLabeledValue(label) {
packages/react-dom/src/__tests__/ReactDOMUseId-test.js new
+515
@@ -0,0 +1,515 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 + */
9 +
10 +let JSDOM;
11 +let React;
12 +let ReactDOM;
13 +let Scheduler;
14 +let clientAct;
15 +let ReactDOMFizzServer;
16 +let Stream;
17 +let Suspense;
18 +let useId;
19 +let document;
20 +let writable;
21 +let container;
22 +let buffer = '';
23 +let hasErrored = false;
24 +let fatalError = undefined;
25 +
26 +describe('useId', () => {
27 + beforeEach(() => {
28 + jest.resetModules();
29 + JSDOM = require('jsdom').JSDOM;
30 + React = require('react');
31 + ReactDOM = require('react-dom');
32 + Scheduler = require('scheduler');
33 + clientAct = require('jest-react').act;
34 + ReactDOMFizzServer = require('react-dom/server');
35 + Stream = require('stream');
36 + Suspense = React.Suspense;
37 + useId = React.unstable_useId;
38 +
39 + // Test Environment
40 + const jsdom = new JSDOM(
41 + '<!DOCTYPE html><html><head></head><body><div id="container">',
42 + {
43 + runScripts: 'dangerously',
44 + },
45 + );
46 + document = jsdom.window.document;
47 + container = document.getElementById('container');
48 +
49 + buffer = '';
50 + hasErrored = false;
51 +
52 + writable = new Stream.PassThrough();
53 + writable.setEncoding('utf8');
54 + writable.on('data', chunk => {
55 + buffer += chunk;
56 + });
57 + writable.on('error', error => {
58 + hasErrored = true;
59 + fatalError = error;
60 + });
61 + });
62 +
63 + async function serverAct(callback) {
64 + await callback();
65 + // Await one turn around the event loop.
66 + // This assumes that we'll flush everything we have so far.
67 + await new Promise(resolve => {
68 + setImmediate(resolve);
69 + });
70 + if (hasErrored) {
71 + throw fatalError;
72 + }
73 + // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
74 + // We also want to execute any scripts that are embedded.
75 + // We assume that we have now received a proper fragment of HTML.
76 + const bufferedContent = buffer;
77 + buffer = '';
78 + const fakeBody = document.createElement('body');
79 + fakeBody.innerHTML = bufferedContent;
80 + while (fakeBody.firstChild) {
81 + const node = fakeBody.firstChild;
82 + if (node.nodeName === 'SCRIPT') {
83 + const script = document.createElement('script');
84 + script.textContent = node.textContent;
85 + fakeBody.removeChild(node);
86 + container.appendChild(script);
87 + } else {
88 + container.appendChild(node);
89 + }
90 + }
91 + }
92 +
93 + function normalizeTreeIdForTesting(id) {
94 + const [serverClientPrefix, base32, hookIndex] = id.split(':');
95 + if (serverClientPrefix === 'r') {
96 + // Client ids aren't stable. For testing purposes, strip out the counter.
97 + return (
98 + 'CLIENT_GENERATED_ID' +
99 + (hookIndex !== undefined ? ` (${hookIndex})` : '')
100 + );
101 + }
102 + // Formats the tree id as a binary sequence, so it's easier to visualize
103 + // the structure.
104 + return (
105 + parseInt(base32, 32).toString(2) +
106 + (hookIndex !== undefined ? ` (${hookIndex})` : '')
107 + );
108 + }
109 +
110 + function DivWithId({children}) {
111 + const id = normalizeTreeIdForTesting(useId());
112 + return <div id={id}>{children}</div>;
113 + }
114 +
115 + test('basic example', async () => {
116 + function App() {
117 + return (
118 + <div>
119 + <div>
120 + <DivWithId />
121 + <DivWithId />
122 + </div>
123 + <DivWithId />
124 + </div>
125 + );
126 + }
127 +
128 + await serverAct(async () => {
129 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
130 + pipe(writable);
131 + });
132 + await clientAct(async () => {
133 + ReactDOM.hydrateRoot(container, <App />);
134 + });
135 + expect(container).toMatchInlineSnapshot(`
136 + <div
137 + id="container"
138 + >
139 + <div>
140 + <div>
141 + <div
142 + id="101"
143 + />
144 + <div
145 + id="1001"
146 + />
147 + </div>
148 + <div
149 + id="10"
150 + />
151 + </div>
152 + </div>
153 + `);
154 + });
155 +
156 + test('indirections', async () => {
157 + function App() {
158 + // There are no forks in this tree, but the parent and the child should
159 + // have different ids.
160 + return (
161 + <DivWithId>
162 + <div>
163 + <div>
164 + <div>
165 + <DivWithId />
166 + </div>
167 + </div>
168 + </div>
169 + </DivWithId>
170 + );
171 + }
172 +
173 + await serverAct(async () => {
174 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
175 + pipe(writable);
176 + });
177 + await clientAct(async () => {
178 + ReactDOM.hydrateRoot(container, <App />);
179 + });
180 + expect(container).toMatchInlineSnapshot(`
181 + <div
182 + id="container"
183 + >
184 + <div
185 + id="0"
186 + >
187 + <div>
188 + <div>
189 + <div>
190 + <div
191 + id="1"
192 + />
193 + </div>
194 + </div>
195 + </div>
196 + </div>
197 + </div>
198 + `);
199 + });
200 +
201 + test('empty (null) children', async () => {
202 + // We don't treat empty children different from non-empty ones, which means
203 + // they get allocated a slot when generating ids. There's no inherent reason
204 + // to do this; Fiber happens to allocate a fiber for null children that
205 + // appear in a list, which is not ideal for performance. For the purposes
206 + // of id generation, though, what matters is that Fizz and Fiber
207 + // are consistent.
208 + function App() {
209 + return (
210 + <>
211 + {null}
212 + <DivWithId />
213 + {null}
214 + <DivWithId />
215 + </>
216 + );
217 + }
218 +
219 + await serverAct(async () => {
220 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
221 + pipe(writable);
222 + });
223 + await clientAct(async () => {
224 + ReactDOM.hydrateRoot(container, <App />);
225 + });
226 + expect(container).toMatchInlineSnapshot(`
227 + <div
228 + id="container"
229 + >
230 + <div
231 + id="10"
232 + />
233 + <div
234 + id="100"
235 + />
236 + </div>
237 + `);
238 + });
239 +
240 + test('large ids', async () => {
241 + // The component in this test outputs a recursive tree of nodes with ids,
242 + // where the underlying binary representation is an alternating series of 1s
243 + // and 0s. In other words, they are all of the form 101010101.
244 + //
245 + // Because we use base 32 encoding, the resulting id should consist of
246 + // alternating 'a' (01010) and 'l' (10101) characters, except for the the
247 + // 'R:' prefix, and the first character after that, which may not correspond
248 + // to a complete set of 5 bits.
249 + //
250 + // Example: R:clalalalalalalala...
251 + //
252 + // We can use this pattern to test large ids that exceed the bitwise
253 + // safe range (32 bits). The algorithm should theoretically support ids
254 + // of any size.
255 +
256 + function Child({children}) {
257 + const id = useId();
258 + return <div id={id}>{children}</div>;
259 + }
260 +
261 + function App() {
262 + let tree = <Child />;
263 + for (let i = 0; i < 50; i++) {
264 + tree = (
265 + <>
266 + <Child />
267 + {tree}
268 + </>
269 + );
270 + }
271 + return tree;
272 + }
273 +
274 + await serverAct(async () => {
275 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
276 + pipe(writable);
277 + });
278 + await clientAct(async () => {
279 + ReactDOM.hydrateRoot(container, <App />);
280 + });
281 + const divs = container.querySelectorAll('div');
282 +
283 + // Confirm that every id matches the expected pattern
284 + for (let i = 0; i < divs.length; i++) {
285 + // Example: R:clalalalalalalala...
286 + expect(divs[i].id).toMatch(/^R:.(((al)*a?)((la)*l?))*$/);
287 + }
288 + });
289 +
290 + test('multiple ids in a single component', async () => {
291 + function App() {
292 + const id1 = useId();
293 + const id2 = useId();
294 + const id3 = useId();
295 + return `${id1}, ${id2}, ${id3}`;
296 + }
297 +
298 + await serverAct(async () => {
299 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
300 + pipe(writable);
301 + });
302 + await clientAct(async () => {
303 + ReactDOM.hydrateRoot(container, <App />);
304 + });
305 + // We append a suffix to the end of the id to distinguish them
306 + expect(container).toMatchInlineSnapshot(`
307 + <div
308 + id="container"
309 + >
310 + R:0, R:0:1, R:0:2
311 + <!-- -->
312 + </div>
313 + `);
314 + });
315 +
316 + test('basic incremental hydration', async () => {
317 + function App() {
318 + return (
319 + <div>
320 + <Suspense fallback="Loading...">
321 + <DivWithId label="A" />
322 + <DivWithId label="B" />
323 + </Suspense>
324 + <DivWithId label="C" />
325 + </div>
326 + );
327 + }
328 +
329 + await serverAct(async () => {
330 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
331 + pipe(writable);
332 + });
333 + await clientAct(async () => {
334 + ReactDOM.hydrateRoot(container, <App />);
335 + });
336 + expect(container).toMatchInlineSnapshot(`
337 + <div
338 + id="container"
339 + >
340 + <div>
341 + <!--$-->
342 + <div
343 + id="101"
344 + />
345 + <div
346 + id="1001"
347 + />
348 + <!--/$-->
349 + <div
350 + id="10"
351 + />
352 + </div>
353 + </div>
354 + `);
355 + });
356 +
357 + test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => {
358 + const span = React.createRef(null);
359 + function App({swap}) {
360 + // Note: Using a dynamic array so these are treated as insertions and
361 + // deletions instead of updates, because Fiber currently allocates a node
362 + // even for empty children.
363 + const children = [
364 + <DivWithId key="A" />,
365 + swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
366 + <DivWithId key="D" />,
367 + ];
368 + return (
369 + <>
370 + {children}
371 + <Suspense key="boundary" fallback="Loading...">
372 + <DivWithId />
373 + <span ref={span} />
374 + </Suspense>
375 + </>
376 + );
377 + }
378 +
379 + await serverAct(async () => {
380 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
381 + pipe(writable);
382 + });
383 + const dehydratedSpan = container.getElementsByTagName('span')[0];
384 + await clientAct(async () => {
385 + const root = ReactDOM.hydrateRoot(container, <App />);
386 + expect(Scheduler).toFlushUntilNextPaint([]);
387 + expect(container).toMatchInlineSnapshot(`
388 + <div
389 + id="container"
390 + >
391 + <div
392 + id="101"
393 + />
394 + <div
395 + id="1001"
396 + />
397 + <div
398 + id="1101"
399 + />
400 + <!--$-->
401 + <div
402 + id="110"
403 + />
404 + <span />
405 + <!--/$-->
406 + </div>
407 + `);
408 +
409 + // The inner boundary hasn't hydrated yet
410 + expect(span.current).toBe(null);
411 +
412 + // Swap B for C
413 + root.render(<App swap={true} />);
414 + });
415 + // The swap should not have caused a mismatch.
416 + expect(container).toMatchInlineSnapshot(`
417 + <div
418 + id="container"
419 + >
420 + <div
421 + id="101"
422 + />
423 + <div
424 + id="CLIENT_GENERATED_ID"
425 + />
426 + <div
427 + id="1101"
428 + />
429 + <!--$-->
430 + <div
431 + id="110"
432 + />
433 + <span />
434 + <!--/$-->
435 + </div>
436 + `);
437 + // Should have hydrated successfully
438 + expect(span.current).toBe(dehydratedSpan);
439 + });
440 +
441 + test('inserting/deleting siblings inside a dehydrated Suspense boundary', async () => {
442 + const span = React.createRef(null);
443 + function App({swap}) {
444 + // Note: Using a dynamic array so these are treated as insertions and
445 + // deletions instead of updates, because Fiber currently allocates a node
446 + // even for empty children.
447 + const children = [
448 + <DivWithId key="A" />,
449 + swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
450 + <DivWithId key="D" />,
451 + ];
452 + return (
453 + <Suspense key="boundary" fallback="Loading...">
454 + {children}
455 + <span ref={span} />
456 + </Suspense>
457 + );
458 + }
459 +
460 + await serverAct(async () => {
461 + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
462 + pipe(writable);
463 + });
464 + const dehydratedSpan = container.getElementsByTagName('span')[0];
465 + await clientAct(async () => {
466 + const root = ReactDOM.hydrateRoot(container, <App />);
467 + expect(Scheduler).toFlushUntilNextPaint([]);
468 + expect(container).toMatchInlineSnapshot(`
469 + <div
470 + id="container"
471 + >
472 + <!--$-->
473 + <div
474 + id="101"
475 + />
476 + <div
477 + id="1001"
478 + />
479 + <div
480 + id="1101"
481 + />
482 + <span />
483 + <!--/$-->
484 + </div>
485 + `);
486 +
487 + // The inner boundary hasn't hydrated yet
488 + expect(span.current).toBe(null);
489 +
490 + // Swap B for C
491 + root.render(<App swap={true} />);
492 + });
493 + // The swap should not have caused a mismatch.
494 + expect(container).toMatchInlineSnapshot(`
495 + <div
496 + id="container"
497 + >
498 + <!--$-->
499 + <div
500 + id="101"
501 + />
502 + <div
503 + id="CLIENT_GENERATED_ID"
504 + />
505 + <div
506 + id="1101"
507 + />
508 + <span />
509 + <!--/$-->
510 + </div>
511 + `);
512 + // Should have hydrated successfully
513 + expect(span.current).toBe(dehydratedSpan);
514 + });
515 +});
packages/react-dom/src/server/ReactPartialRendererHooks.js
+5
@@ -519,6 +519,10 @@ function useOpaqueIdentifier(): OpaqueIDType {
519 );
520 }
521
522 +function useId(): OpaqueIDType {
523 + throw new Error('Not implemented.');
524 +}
525 +
526 function useCacheRefresh(): <T>(?() => T, ?T) => void {
527 throw new Error('Not implemented.');
528 }
@@ -549,6 +553,7 @@ export const Dispatcher: DispatcherType = {
553 useDeferredValue,
554 useTransition,
555 useOpaqueIdentifier,
556 + useId,
557 // Subscriptions are not setup in a server environment.
558 useMutableSource,
559 useSyncExternalStore,
packages/react-reconciler/src/ReactChildFiber.new.js
+30 -2
@@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
13 import type {Lanes} from './ReactFiberLane.new';
14
15 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
16 -import {Placement, ChildDeletion} from './ReactFiberFlags';
16 +import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
17 import {
18 getIteratorFn,
19 REACT_ELEMENT_TYPE,
@@ -40,6 +40,8 @@ import {
40 import {emptyRefsObject} from './ReactFiberClassComponent.new';
41 import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new';
42 import {StrictLegacyMode} from './ReactTypeOfMode';
43 +import {getIsHydrating} from './ReactFiberHydrationContext.new';
44 +import {pushTreeFork} from './ReactFiberTreeContext.new';
45
46 let didWarnAboutMaps;
47 let didWarnAboutGenerators;
@@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) {
336 ): number {
337 newFiber.index = newIndex;
338 if (!shouldTrackSideEffects) {
337 - // Noop.
339 + // During hydration, the useId algorithm needs to know which fibers are
340 + // part of a list of children (arrays, iterators).
341 + newFiber.flags |= Forked;
342 return lastPlacedIndex;
343 }
344 const current = newFiber.alternate;
@@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) {
827 if (newIdx === newChildren.length) {
828 // We've reached the end of the new children. We can delete the rest.
829 deleteRemainingChildren(returnFiber, oldFiber);
830 + if (getIsHydrating()) {
831 + const numberOfForks = newIdx;
832 + pushTreeFork(returnFiber, numberOfForks);
833 + }
834 return resultingFirstChild;
835 }
836
@@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) {
851 }
852 previousNewFiber = newFiber;
853 }
854 + if (getIsHydrating()) {
855 + const numberOfForks = newIdx;
856 + pushTreeFork(returnFiber, numberOfForks);
857 + }
858 return resultingFirstChild;
859 }
860
@@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) {
898 existingChildren.forEach(child => deleteChild(returnFiber, child));
899 }
900
901 + if (getIsHydrating()) {
902 + const numberOfForks = newIdx;
903 + pushTreeFork(returnFiber, numberOfForks);
904 + }
905 return resultingFirstChild;
906 }
907
@@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1029 if (step.done) {
1030 // We've reached the end of the new children. We can delete the rest.
1031 deleteRemainingChildren(returnFiber, oldFiber);
1032 + if (getIsHydrating()) {
1033 + const numberOfForks = newIdx;
1034 + pushTreeFork(returnFiber, numberOfForks);
1035 + }
1036 return resultingFirstChild;
1037 }
1038
@@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1053 }
1054 previousNewFiber = newFiber;
1055 }
1056 + if (getIsHydrating()) {
1057 + const numberOfForks = newIdx;
1058 + pushTreeFork(returnFiber, numberOfForks);
1059 + }
1060 return resultingFirstChild;
1061 }
1062
@@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1100 existingChildren.forEach(child => deleteChild(returnFiber, child));
1101 }
1102
1103 + if (getIsHydrating()) {
1104 + const numberOfForks = newIdx;
1105 + pushTreeFork(returnFiber, numberOfForks);
1106 + }
1107 return resultingFirstChild;
1108 }
1109
packages/react-reconciler/src/ReactChildFiber.old.js
+30 -2
@@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
13 import type {Lanes} from './ReactFiberLane.old';
14
15 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
16 -import {Placement, ChildDeletion} from './ReactFiberFlags';
16 +import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
17 import {
18 getIteratorFn,
19 REACT_ELEMENT_TYPE,
@@ -40,6 +40,8 @@ import {
40 import {emptyRefsObject} from './ReactFiberClassComponent.old';
41 import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old';
42 import {StrictLegacyMode} from './ReactTypeOfMode';
43 +import {getIsHydrating} from './ReactFiberHydrationContext.old';
44 +import {pushTreeFork} from './ReactFiberTreeContext.old';
45
46 let didWarnAboutMaps;
47 let didWarnAboutGenerators;
@@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) {
336 ): number {
337 newFiber.index = newIndex;
338 if (!shouldTrackSideEffects) {
337 - // Noop.
339 + // During hydration, the useId algorithm needs to know which fibers are
340 + // part of a list of children (arrays, iterators).
341 + newFiber.flags |= Forked;
342 return lastPlacedIndex;
343 }
344 const current = newFiber.alternate;
@@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) {
827 if (newIdx === newChildren.length) {
828 // We've reached the end of the new children. We can delete the rest.
829 deleteRemainingChildren(returnFiber, oldFiber);
830 + if (getIsHydrating()) {
831 + const numberOfForks = newIdx;
832 + pushTreeFork(returnFiber, numberOfForks);
833 + }
834 return resultingFirstChild;
835 }
836
@@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) {
851 }
852 previousNewFiber = newFiber;
853 }
854 + if (getIsHydrating()) {
855 + const numberOfForks = newIdx;
856 + pushTreeFork(returnFiber, numberOfForks);
857 + }
858 return resultingFirstChild;
859 }
860
@@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) {
898 existingChildren.forEach(child => deleteChild(returnFiber, child));
899 }
900
901 + if (getIsHydrating()) {
902 + const numberOfForks = newIdx;
903 + pushTreeFork(returnFiber, numberOfForks);
904 + }
905 return resultingFirstChild;
906 }
907
@@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1029 if (step.done) {
1030 // We've reached the end of the new children. We can delete the rest.
1031 deleteRemainingChildren(returnFiber, oldFiber);
1032 + if (getIsHydrating()) {
1033 + const numberOfForks = newIdx;
1034 + pushTreeFork(returnFiber, numberOfForks);
1035 + }
1036 return resultingFirstChild;
1037 }
1038
@@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1053 }
1054 previousNewFiber = newFiber;
1055 }
1056 + if (getIsHydrating()) {
1057 + const numberOfForks = newIdx;
1058 + pushTreeFork(returnFiber, numberOfForks);
1059 + }
1060 return resultingFirstChild;
1061 }
1062
@@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) {
1100 existingChildren.forEach(child => deleteChild(returnFiber, child));
1101 }
1102
1103 + if (getIsHydrating()) {
1104 + const numberOfForks = newIdx;
1105 + pushTreeFork(returnFiber, numberOfForks);
1106 + }
1107 return resultingFirstChild;
1108 }
1109
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+24
@@ -186,6 +186,7 @@ import {
186 invalidateContextProvider,
187 } from './ReactFiberContext.new';
188 import {
189 + getIsHydrating,
190 enterHydrationState,
191 reenterHydrationStateFromDehydratedSuspenseInstance,
192 resetHydrationState,
@@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.new';
236 import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new';
237 import is from 'shared/objectIs';
238 import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new';
239 +import {
240 + getForksAtLevel,
241 + isForkedChild,
242 + pushTreeId,
243 +} from './ReactFiberTreeContext.new';
244
245 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
246
@@ -1757,6 +1763,7 @@ function mountIndeterminateComponent(
1763 }
1764 }
1765 }
1766 +
1767 reconcileChildren(null, workInProgress, value, renderLanes);
1768 if (__DEV__) {
1769 validateFunctionComponentInDev(workInProgress, Component);
@@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
1852
1853 const SUSPENDED_MARKER: SuspenseState = {
1854 dehydrated: null,
1855 + treeContext: null,
1856 retryLane: NoLane,
1857 };
1858
@@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent(
2701 reenterHydrationStateFromDehydratedSuspenseInstance(
2702 workInProgress,
2703 suspenseInstance,
2704 + suspenseState.treeContext,
2705 );
2706 const nextProps = workInProgress.pendingProps;
2707 const primaryChildren = nextProps.children;
@@ -3675,6 +3684,21 @@ function beginWork(
3684 }
3685 } else {
3686 didReceiveUpdate = false;
3687 +
3688 + if (getIsHydrating() && isForkedChild(workInProgress)) {
3689 + // Check if this child belongs to a list of muliple children in
3690 + // its parent.
3691 + //
3692 + // In a true multi-threaded implementation, we would render children on
3693 + // parallel threads. This would represent the beginning of a new render
3694 + // thread for this subtree.
3695 + //
3696 + // We only use this for id generation during hydration, which is why the
3697 + // logic is located in this special branch.
3698 + const slotIndex = workInProgress.index;
3699 + const numberOfForks = getForksAtLevel(workInProgress);
3700 + pushTreeId(workInProgress, numberOfForks, slotIndex);
3701 + }
3702 }
3703
3704 // Before entering the begin phase, clear pending update priority.
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+24
@@ -186,6 +186,7 @@ import {
186 invalidateContextProvider,
187 } from './ReactFiberContext.old';
188 import {
189 + getIsHydrating,
190 enterHydrationState,
191 reenterHydrationStateFromDehydratedSuspenseInstance,
192 resetHydrationState,
@@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.old';
236 import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old';
237 import is from 'shared/objectIs';
238 import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old';
239 +import {
240 + getForksAtLevel,
241 + isForkedChild,
242 + pushTreeId,
243 +} from './ReactFiberTreeContext.old';
244
245 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
246
@@ -1757,6 +1763,7 @@ function mountIndeterminateComponent(
1763 }
1764 }
1765 }
1766 +
1767 reconcileChildren(null, workInProgress, value, renderLanes);
1768 if (__DEV__) {
1769 validateFunctionComponentInDev(workInProgress, Component);
@@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
1852
1853 const SUSPENDED_MARKER: SuspenseState = {
1854 dehydrated: null,
1855 + treeContext: null,
1856 retryLane: NoLane,
1857 };
1858
@@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent(
2701 reenterHydrationStateFromDehydratedSuspenseInstance(
2702 workInProgress,
2703 suspenseInstance,
2704 + suspenseState.treeContext,
2705 );
2706 const nextProps = workInProgress.pendingProps;
2707 const primaryChildren = nextProps.children;
@@ -3675,6 +3684,21 @@ function beginWork(
3684 }
3685 } else {
3686 didReceiveUpdate = false;
3687 +
3688 + if (getIsHydrating() && isForkedChild(workInProgress)) {
3689 + // Check if this child belongs to a list of muliple children in
3690 + // its parent.
3691 + //
3692 + // In a true multi-threaded implementation, we would render children on
3693 + // parallel threads. This would represent the beginning of a new render
3694 + // thread for this subtree.
3695 + //
3696 + // We only use this for id generation during hydration, which is why the
3697 + // logic is located in this special branch.
3698 + const slotIndex = workInProgress.index;
3699 + const numberOfForks = getForksAtLevel(workInProgress);
3700 + pushTreeId(workInProgress, numberOfForks, slotIndex);
3701 + }
3702 }
3703
3704 // Before entering the begin phase, clear pending update priority.
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+6 -1
@@ -155,6 +155,7 @@ import {
155 popRootCachePool,
156 popCachePool,
157 } from './ReactFiberCacheComponent.new';
158 +import {popTreeContext} from './ReactFiberTreeContext.new';
159
160 function markUpdate(workInProgress: Fiber) {
161 // Tag the fiber with an update effect. This turns a Placement into
@@ -822,7 +823,11 @@ function completeWork(
823 renderLanes: Lanes,
824 ): Fiber | null {
825 const newProps = workInProgress.pendingProps;
825 -
826 + // Note: This intentionally doesn't check if we're hydrating because comparing
827 + // to the current tree provider fiber is just as fast and less error-prone.
828 + // Ideally we would have a special version of the work loop only
829 + // for hydration.
830 + popTreeContext(workInProgress);
831 switch (workInProgress.tag) {
832 case IndeterminateComponent:
833 case LazyComponent:
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+6 -1
@@ -155,6 +155,7 @@ import {
155 popRootCachePool,
156 popCachePool,
157 } from './ReactFiberCacheComponent.old';
158 +import {popTreeContext} from './ReactFiberTreeContext.old';
159
160 function markUpdate(workInProgress: Fiber) {
161 // Tag the fiber with an update effect. This turns a Placement into
@@ -822,7 +823,11 @@ function completeWork(
823 renderLanes: Lanes,
824 ): Fiber | null {
825 const newProps = workInProgress.pendingProps;
825 -
826 + // Note: This intentionally doesn't check if we're hydrating because comparing
827 + // to the current tree provider fiber is just as fast and less error-prone.
828 + // Ideally we would have a special version of the work loop only
829 + // for hydration.
830 + popTreeContext(workInProgress);
831 switch (workInProgress.tag) {
832 case IndeterminateComponent:
833 case LazyComponent:
packages/react-reconciler/src/ReactFiberFlags.js
+28 -27
@@ -12,54 +12,55 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
12 export type Flags = number;
13
14 // Don't change these two values. They're used by React Dev Tools.
15 -export const NoFlags = /* */ 0b0000000000000000000000000;
16 -export const PerformedWork = /* */ 0b0000000000000000000000001;
15 +export const NoFlags = /* */ 0b00000000000000000000000000;
16 +export const PerformedWork = /* */ 0b00000000000000000000000001;
17
18 // You can change the rest (and add more).
19 -export const Placement = /* */ 0b0000000000000000000000010;
20 -export const Update = /* */ 0b0000000000000000000000100;
19 +export const Placement = /* */ 0b00000000000000000000000010;
20 +export const Update = /* */ 0b00000000000000000000000100;
21 export const PlacementAndUpdate = /* */ Placement | Update;
22 -export const Deletion = /* */ 0b0000000000000000000001000;
23 -export const ChildDeletion = /* */ 0b0000000000000000000010000;
24 -export const ContentReset = /* */ 0b0000000000000000000100000;
25 -export const Callback = /* */ 0b0000000000000000001000000;
26 -export const DidCapture = /* */ 0b0000000000000000010000000;
27 -export const ForceClientRender = /* */ 0b0000000000000000100000000;
28 -export const Ref = /* */ 0b0000000000000001000000000;
29 -export const Snapshot = /* */ 0b0000000000000010000000000;
30 -export const Passive = /* */ 0b0000000000000100000000000;
31 -export const Hydrating = /* */ 0b0000000000001000000000000;
22 +export const Deletion = /* */ 0b00000000000000000000001000;
23 +export const ChildDeletion = /* */ 0b00000000000000000000010000;
24 +export const ContentReset = /* */ 0b00000000000000000000100000;
25 +export const Callback = /* */ 0b00000000000000000001000000;
26 +export const DidCapture = /* */ 0b00000000000000000010000000;
27 +export const ForceClientRender = /* */ 0b00000000000000000100000000;
28 +export const Ref = /* */ 0b00000000000000001000000000;
29 +export const Snapshot = /* */ 0b00000000000000010000000000;
30 +export const Passive = /* */ 0b00000000000000100000000000;
31 +export const Hydrating = /* */ 0b00000000000001000000000000;
32 export const HydratingAndUpdate = /* */ Hydrating | Update;
33 -export const Visibility = /* */ 0b0000000000010000000000000;
34 -export const StoreConsistency = /* */ 0b0000000000100000000000000;
33 +export const Visibility = /* */ 0b00000000000010000000000000;
34 +export const StoreConsistency = /* */ 0b00000000000100000000000000;
35
36 export const LifecycleEffectMask =
37 Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
38
39 // Union of all commit flags (flags with the lifetime of a particular commit)
40 -export const HostEffectMask = /* */ 0b0000000000111111111111111;
40 +export const HostEffectMask = /* */ 0b00000000000111111111111111;
41
42 // These are not really side effects, but we still reuse this field.
43 -export const Incomplete = /* */ 0b0000000001000000000000000;
44 -export const ShouldCapture = /* */ 0b0000000010000000000000000;
45 -export const ForceUpdateForLegacySuspense = /* */ 0b0000000100000000000000000;
46 -export const DidPropagateContext = /* */ 0b0000001000000000000000000;
47 -export const NeedsPropagation = /* */ 0b0000010000000000000000000;
43 +export const Incomplete = /* */ 0b00000000001000000000000000;
44 +export const ShouldCapture = /* */ 0b00000000010000000000000000;
45 +export const ForceUpdateForLegacySuspense = /* */ 0b00000000100000000000000000;
46 +export const DidPropagateContext = /* */ 0b00000001000000000000000000;
47 +export const NeedsPropagation = /* */ 0b00000010000000000000000000;
48 +export const Forked = /* */ 0b00000100000000000000000000;
49
50 // Static tags describe aspects of a fiber that are not specific to a render,
51 // e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
52 // This enables us to defer more work in the unmount case,
53 // since we can defer traversing the tree during layout to look for Passive effects,
54 // and instead rely on the static flag as a signal that there may be cleanup work.
54 -export const RefStatic = /* */ 0b0000100000000000000000000;
55 -export const LayoutStatic = /* */ 0b0001000000000000000000000;
56 -export const PassiveStatic = /* */ 0b0010000000000000000000000;
55 +export const RefStatic = /* */ 0b00001000000000000000000000;
56 +export const LayoutStatic = /* */ 0b00010000000000000000000000;
57 +export const PassiveStatic = /* */ 0b00100000000000000000000000;
58
59 // These flags allow us to traverse to fibers that have effects on mount
60 // without traversing the entire tree after every commit for
61 // double invoking
61 -export const MountLayoutDev = /* */ 0b0100000000000000000000000;
62 -export const MountPassiveDev = /* */ 0b1000000000000000000000000;
62 +export const MountLayoutDev = /* */ 0b01000000000000000000000000;
63 +export const MountPassiveDev = /* */ 0b10000000000000000000000000;
64
65 // Groups of flags that are used in the commit phase to skip over trees that
66 // don't contain effects, by checking subtreeFlags.
packages/react-reconciler/src/ReactFiberHooks.new.js
+99
@@ -117,6 +117,7 @@ import {
117 } from './ReactUpdateQueue.new';
118 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
119 import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
120 +import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.new';
121
122 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
123
@@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false;
204 // TODO: Maybe there's some way to consolidate this with
205 // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
206 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
207 +// Counts the number of useId hooks in this component.
208 +let localIdCounter: number = 0;
209 +// Used for ids that are generated completely client-side (i.e. not during
210 +// hydration). This counter is global, so client ids are not stable across
211 +// render attempts.
212 +let globalClientIdCounter: number = 0;
213
214 const RE_RENDER_LIMIT = 25;
215
@@ -396,6 +403,7 @@ export function renderWithHooks<Props, SecondArg>(
403 // workInProgressHook = null;
404
405 // didScheduleRenderPhaseUpdate = false;
406 + // localIdCounter = 0;
407
408 // TODO Warn if no hooks are used at all during mount, then some are used during update.
409 // Currently we will identify the update render as a mount because memoizedState === null.
@@ -543,6 +551,21 @@ export function renderWithHooks<Props, SecondArg>(
551 }
552 }
553
554 + if (localIdCounter !== 0) {
555 + localIdCounter = 0;
556 + if (getIsHydrating()) {
557 + // This component materialized an id. This will affect any ids that appear
558 + // in its children.
559 + const returnFiber = workInProgress.return;
560 + if (returnFiber !== null) {
561 + const numberOfForks = 1;
562 + const slotIndex = 0;
563 + pushTreeFork(workInProgress, numberOfForks);
564 + pushTreeId(workInProgress, numberOfForks, slotIndex);
565 + }
566 + }
567 + }
568 +
569 return children;
570 }
571
@@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void {
635 }
636
637 didScheduleRenderPhaseUpdateDuringThisPass = false;
638 + localIdCounter = 0;
639 }
640
641 function mountWorkInProgressHook(): Hook {
@@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
2133 return id;
2134 }
2135
2136 +function mountId(): string {
2137 + const hook = mountWorkInProgressHook();
2138 +
2139 + let id;
2140 + if (getIsHydrating()) {
2141 + const treeId = getTreeId();
2142 +
2143 + // Use a captial R prefix for server-generated ids.
2144 + id = 'R:' + treeId;
2145 +
2146 + // Unless this is the first id at this level, append a number at the end
2147 + // that represents the position of this useId hook among all the useId
2148 + // hooks for this fiber.
2149 + const localId = localIdCounter++;
2150 + if (localId > 0) {
2151 + id += ':' + localId.toString(32);
2152 + }
2153 + } else {
2154 + // Use a lowercase r prefix for client-generated ids.
2155 + const globalClientId = globalClientIdCounter++;
2156 + id = 'r:' + globalClientId.toString(32);
2157 + }
2158 +
2159 + hook.memoizedState = id;
2160 + return id;
2161 +}
2162 +
2163 +function updateId(): string {
2164 + const hook = updateWorkInProgressHook();
2165 + const id: string = hook.memoizedState;
2166 + return id;
2167 +}
2168 +
2169 function mountRefresh() {
2170 const hook = mountWorkInProgressHook();
2171 const refresh = (hook.memoizedState = refreshCache.bind(
@@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2482 useMutableSource: throwInvalidHookError,
2483 useSyncExternalStore: throwInvalidHookError,
2484 useOpaqueIdentifier: throwInvalidHookError,
2485 + useId: throwInvalidHookError,
2486
2487 unstable_isNewReconciler: enableNewReconciler,
2488 };
@@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2511 useMutableSource: mountMutableSource,
2512 useSyncExternalStore: mountSyncExternalStore,
2513 useOpaqueIdentifier: mountOpaqueIdentifier,
2514 + useId: mountId,
2515
2516 unstable_isNewReconciler: enableNewReconciler,
2517 };
@@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2540 useMutableSource: updateMutableSource,
2541 useSyncExternalStore: updateSyncExternalStore,
2542 useOpaqueIdentifier: updateOpaqueIdentifier,
2543 + useId: updateId,
2544
2545 unstable_isNewReconciler: enableNewReconciler,
2546 };
@@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2569 useMutableSource: updateMutableSource,
2570 useSyncExternalStore: mountSyncExternalStore,
2571 useOpaqueIdentifier: rerenderOpaqueIdentifier,
2572 + useId: updateId,
2573
2574 unstable_isNewReconciler: enableNewReconciler,
2575 };
@@ -2680,6 +2741,11 @@ if (__DEV__) {
2741 mountHookTypesDev();
2742 return mountOpaqueIdentifier();
2743 },
2744 + useId(): string {
2745 + currentHookNameInDev = 'useId';
2746 + mountHookTypesDev();
2747 + return mountId();
2748 + },
2749
2750 unstable_isNewReconciler: enableNewReconciler,
2751 };
@@ -2822,6 +2888,11 @@ if (__DEV__) {
2888 updateHookTypesDev();
2889 return mountOpaqueIdentifier();
2890 },
2891 + useId(): string {
2892 + currentHookNameInDev = 'useId';
2893 + updateHookTypesDev();
2894 + return mountId();
2895 + },
2896
2897 unstable_isNewReconciler: enableNewReconciler,
2898 };
@@ -2964,6 +3035,11 @@ if (__DEV__) {
3035 updateHookTypesDev();
3036 return updateOpaqueIdentifier();
3037 },
3038 + useId(): string {
3039 + currentHookNameInDev = 'useId';
3040 + updateHookTypesDev();
3041 + return updateId();
3042 + },
3043
3044 unstable_isNewReconciler: enableNewReconciler,
3045 };
@@ -3107,6 +3183,11 @@ if (__DEV__) {
3183 updateHookTypesDev();
3184 return rerenderOpaqueIdentifier();
3185 },
3186 + useId(): string {
3187 + currentHookNameInDev = 'useId';
3188 + updateHookTypesDev();
3189 + return updateId();
3190 + },
3191
3192 unstable_isNewReconciler: enableNewReconciler,
3193 };
@@ -3266,6 +3347,12 @@ if (__DEV__) {
3347 mountHookTypesDev();
3348 return mountOpaqueIdentifier();
3349 },
3350 + useId(): string {
3351 + currentHookNameInDev = 'useId';
3352 + warnInvalidHookAccess();
3353 + mountHookTypesDev();
3354 + return mountId();
3355 + },
3356
3357 unstable_isNewReconciler: enableNewReconciler,
3358 };
@@ -3425,6 +3512,12 @@ if (__DEV__) {
3512 updateHookTypesDev();
3513 return updateOpaqueIdentifier();
3514 },
3515 + useId(): string {
3516 + currentHookNameInDev = 'useId';
3517 + warnInvalidHookAccess();
3518 + updateHookTypesDev();
3519 + return updateId();
3520 + },
3521
3522 unstable_isNewReconciler: enableNewReconciler,
3523 };
@@ -3585,6 +3678,12 @@ if (__DEV__) {
3678 updateHookTypesDev();
3679 return rerenderOpaqueIdentifier();
3680 },
3681 + useId(): string {
3682 + currentHookNameInDev = 'useId';
3683 + warnInvalidHookAccess();
3684 + updateHookTypesDev();
3685 + return updateId();
3686 + },
3687
3688 unstable_isNewReconciler: enableNewReconciler,
3689 };
packages/react-reconciler/src/ReactFiberHooks.old.js
+99
@@ -117,6 +117,7 @@ import {
117 } from './ReactUpdateQueue.old';
118 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
119 import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
120 +import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.old';
121
122 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
123
@@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false;
204 // TODO: Maybe there's some way to consolidate this with
205 // `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
206 let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
207 +// Counts the number of useId hooks in this component.
208 +let localIdCounter: number = 0;
209 +// Used for ids that are generated completely client-side (i.e. not during
210 +// hydration). This counter is global, so client ids are not stable across
211 +// render attempts.
212 +let globalClientIdCounter: number = 0;
213
214 const RE_RENDER_LIMIT = 25;
215
@@ -396,6 +403,7 @@ export function renderWithHooks<Props, SecondArg>(
403 // workInProgressHook = null;
404
405 // didScheduleRenderPhaseUpdate = false;
406 + // localIdCounter = 0;
407
408 // TODO Warn if no hooks are used at all during mount, then some are used during update.
409 // Currently we will identify the update render as a mount because memoizedState === null.
@@ -543,6 +551,21 @@ export function renderWithHooks<Props, SecondArg>(
551 }
552 }
553
554 + if (localIdCounter !== 0) {
555 + localIdCounter = 0;
556 + if (getIsHydrating()) {
557 + // This component materialized an id. This will affect any ids that appear
558 + // in its children.
559 + const returnFiber = workInProgress.return;
560 + if (returnFiber !== null) {
561 + const numberOfForks = 1;
562 + const slotIndex = 0;
563 + pushTreeFork(workInProgress, numberOfForks);
564 + pushTreeId(workInProgress, numberOfForks, slotIndex);
565 + }
566 + }
567 + }
568 +
569 return children;
570 }
571
@@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void {
635 }
636
637 didScheduleRenderPhaseUpdateDuringThisPass = false;
638 + localIdCounter = 0;
639 }
640
641 function mountWorkInProgressHook(): Hook {
@@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
2133 return id;
2134 }
2135
2136 +function mountId(): string {
2137 + const hook = mountWorkInProgressHook();
2138 +
2139 + let id;
2140 + if (getIsHydrating()) {
2141 + const treeId = getTreeId();
2142 +
2143 + // Use a captial R prefix for server-generated ids.
2144 + id = 'R:' + treeId;
2145 +
2146 + // Unless this is the first id at this level, append a number at the end
2147 + // that represents the position of this useId hook among all the useId
2148 + // hooks for this fiber.
2149 + const localId = localIdCounter++;
2150 + if (localId > 0) {
2151 + id += ':' + localId.toString(32);
2152 + }
2153 + } else {
2154 + // Use a lowercase r prefix for client-generated ids.
2155 + const globalClientId = globalClientIdCounter++;
2156 + id = 'r:' + globalClientId.toString(32);
2157 + }
2158 +
2159 + hook.memoizedState = id;
2160 + return id;
2161 +}
2162 +
2163 +function updateId(): string {
2164 + const hook = updateWorkInProgressHook();
2165 + const id: string = hook.memoizedState;
2166 + return id;
2167 +}
2168 +
2169 function mountRefresh() {
2170 const hook = mountWorkInProgressHook();
2171 const refresh = (hook.memoizedState = refreshCache.bind(
@@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2482 useMutableSource: throwInvalidHookError,
2483 useSyncExternalStore: throwInvalidHookError,
2484 useOpaqueIdentifier: throwInvalidHookError,
2485 + useId: throwInvalidHookError,
2486
2487 unstable_isNewReconciler: enableNewReconciler,
2488 };
@@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2511 useMutableSource: mountMutableSource,
2512 useSyncExternalStore: mountSyncExternalStore,
2513 useOpaqueIdentifier: mountOpaqueIdentifier,
2514 + useId: mountId,
2515
2516 unstable_isNewReconciler: enableNewReconciler,
2517 };
@@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2540 useMutableSource: updateMutableSource,
2541 useSyncExternalStore: updateSyncExternalStore,
2542 useOpaqueIdentifier: updateOpaqueIdentifier,
2543 + useId: updateId,
2544
2545 unstable_isNewReconciler: enableNewReconciler,
2546 };
@@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2569 useMutableSource: updateMutableSource,
2570 useSyncExternalStore: mountSyncExternalStore,
2571 useOpaqueIdentifier: rerenderOpaqueIdentifier,
2572 + useId: updateId,
2573
2574 unstable_isNewReconciler: enableNewReconciler,
2575 };
@@ -2680,6 +2741,11 @@ if (__DEV__) {
2741 mountHookTypesDev();
2742 return mountOpaqueIdentifier();
2743 },
2744 + useId(): string {
2745 + currentHookNameInDev = 'useId';
2746 + mountHookTypesDev();
2747 + return mountId();
2748 + },
2749
2750 unstable_isNewReconciler: enableNewReconciler,
2751 };
@@ -2822,6 +2888,11 @@ if (__DEV__) {
2888 updateHookTypesDev();
2889 return mountOpaqueIdentifier();
2890 },
2891 + useId(): string {
2892 + currentHookNameInDev = 'useId';
2893 + updateHookTypesDev();
2894 + return mountId();
2895 + },
2896
2897 unstable_isNewReconciler: enableNewReconciler,
2898 };
@@ -2964,6 +3035,11 @@ if (__DEV__) {
3035 updateHookTypesDev();
3036 return updateOpaqueIdentifier();
3037 },
3038 + useId(): string {
3039 + currentHookNameInDev = 'useId';
3040 + updateHookTypesDev();
3041 + return updateId();
3042 + },
3043
3044 unstable_isNewReconciler: enableNewReconciler,
3045 };
@@ -3107,6 +3183,11 @@ if (__DEV__) {
3183 updateHookTypesDev();
3184 return rerenderOpaqueIdentifier();
3185 },
3186 + useId(): string {
3187 + currentHookNameInDev = 'useId';
3188 + updateHookTypesDev();
3189 + return updateId();
3190 + },
3191
3192 unstable_isNewReconciler: enableNewReconciler,
3193 };
@@ -3266,6 +3347,12 @@ if (__DEV__) {
3347 mountHookTypesDev();
3348 return mountOpaqueIdentifier();
3349 },
3350 + useId(): string {
3351 + currentHookNameInDev = 'useId';
3352 + warnInvalidHookAccess();
3353 + mountHookTypesDev();
3354 + return mountId();
3355 + },
3356
3357 unstable_isNewReconciler: enableNewReconciler,
3358 };
@@ -3425,6 +3512,12 @@ if (__DEV__) {
3512 updateHookTypesDev();
3513 return updateOpaqueIdentifier();
3514 },
3515 + useId(): string {
3516 + currentHookNameInDev = 'useId';
3517 + warnInvalidHookAccess();
3518 + updateHookTypesDev();
3519 + return updateId();
3520 + },
3521
3522 unstable_isNewReconciler: enableNewReconciler,
3523 };
@@ -3585,6 +3678,12 @@ if (__DEV__) {
3678 updateHookTypesDev();
3679 return rerenderOpaqueIdentifier();
3680 },
3681 + useId(): string {
3682 + currentHookNameInDev = 'useId';
3683 + warnInvalidHookAccess();
3684 + updateHookTypesDev();
3685 + return updateId();
3686 + },
3687
3688 unstable_isNewReconciler: enableNewReconciler,
3689 };
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+10
@@ -17,6 +17,7 @@ import type {
17 HostContext,
18 } from './ReactFiberHostConfig';
19 import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
20 +import type {TreeContext} from './ReactFiberTreeContext.new';
21
22 import {
23 HostComponent,
@@ -62,6 +63,10 @@ import {
63 } from './ReactFiberHostConfig';
64 import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
65 import {OffscreenLane} from './ReactFiberLane.new';
66 +import {
67 + getSuspendedTreeContext,
68 + restoreSuspendedTreeContext,
69 +} from './ReactFiberTreeContext.new';
70
71 // The deepest Fiber on the stack involved in a hydration context.
72 // This may have been an insertion or a hydration.
@@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
101 function reenterHydrationStateFromDehydratedSuspenseInstance(
102 fiber: Fiber,
103 suspenseInstance: SuspenseInstance,
104 + treeContext: TreeContext | null,
105 ): boolean {
106 if (!supportsHydration) {
107 return false;
@@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
111 );
112 hydrationParentFiber = fiber;
113 isHydrating = true;
114 + if (treeContext !== null) {
115 + restoreSuspendedTreeContext(fiber, treeContext);
116 + }
117 return true;
118 }
119
@@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
296 if (suspenseInstance !== null) {
297 const suspenseState: SuspenseState = {
298 dehydrated: suspenseInstance,
299 + treeContext: getSuspendedTreeContext(),
300 retryLane: OffscreenLane,
301 };
302 fiber.memoizedState = suspenseState;
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+10
@@ -17,6 +17,7 @@ import type {
17 HostContext,
18 } from './ReactFiberHostConfig';
19 import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
20 +import type {TreeContext} from './ReactFiberTreeContext.old';
21
22 import {
23 HostComponent,
@@ -62,6 +63,10 @@ import {
63 } from './ReactFiberHostConfig';
64 import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
65 import {OffscreenLane} from './ReactFiberLane.old';
66 +import {
67 + getSuspendedTreeContext,
68 + restoreSuspendedTreeContext,
69 +} from './ReactFiberTreeContext.old';
70
71 // The deepest Fiber on the stack involved in a hydration context.
72 // This may have been an insertion or a hydration.
@@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
101 function reenterHydrationStateFromDehydratedSuspenseInstance(
102 fiber: Fiber,
103 suspenseInstance: SuspenseInstance,
104 + treeContext: TreeContext | null,
105 ): boolean {
106 if (!supportsHydration) {
107 return false;
@@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
111 );
112 hydrationParentFiber = fiber;
113 isHydrating = true;
114 + if (treeContext !== null) {
115 + restoreSuspendedTreeContext(fiber, treeContext);
116 + }
117 return true;
118 }
119
@@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
296 if (suspenseInstance !== null) {
297 const suspenseState: SuspenseState = {
298 dehydrated: suspenseInstance,
299 + treeContext: getSuspendedTreeContext(),
300 retryLane: OffscreenLane,
301 };
302 fiber.memoizedState = suspenseState;
packages/react-reconciler/src/ReactFiberLane.new.js
+1 -14
@@ -23,6 +23,7 @@ import {
23 } from 'shared/ReactFeatureFlags';
24 import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
25 import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
26 +import {clz32} from './clz32';
27
28 // Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler.
29 // If those values are changed that package should be rebuilt and redeployed.
@@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
792 lanes &= ~lane;
793 }
794 }
794 -
795 -const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
796 -
797 -// Count leading zeros. Only used on lanes, so assume input is an integer.
798 -// Based on:
799 -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
800 -const log = Math.log;
801 -const LN2 = Math.LN2;
802 -function clz32Fallback(lanes: Lanes | Lane) {
803 - if (lanes === 0) {
804 - return 32;
805 - }
806 - return (31 - ((log(lanes) / LN2) | 0)) | 0;
807 -}
packages/react-reconciler/src/ReactFiberLane.old.js
+1 -14
@@ -23,6 +23,7 @@ import {
23 } from 'shared/ReactFeatureFlags';
24 import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
25 import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
26 +import {clz32} from './clz32';
27
28 // Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler.
29 // If those values are changed that package should be rebuilt and redeployed.
@@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
792 lanes &= ~lane;
793 }
794 }
794 -
795 -const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
796 -
797 -// Count leading zeros. Only used on lanes, so assume input is an integer.
798 -// Based on:
799 -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
800 -const log = Math.log;
801 -const LN2 = Math.LN2;
802 -function clz32Fallback(lanes: Lanes | Lane) {
803 - if (lanes === 0) {
804 - return 32;
805 - }
806 - return (31 - ((log(lanes) / LN2) | 0)) | 0;
807 -}
packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js
+3
@@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
11 import type {Fiber} from './ReactInternalTypes';
12 import type {SuspenseInstance} from './ReactFiberHostConfig';
13 import type {Lane} from './ReactFiberLane.new';
14 +import type {TreeContext} from './ReactFiberTreeContext.new';
15 +
16 import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
17 import {NoFlags, DidCapture} from './ReactFiberFlags';
18 import {
@@ -40,6 +42,7 @@ export type SuspenseState = {|
42 // here to indicate that it is dehydrated (flag) and for quick access
43 // to check things like isSuspenseInstancePending.
44 dehydrated: null | SuspenseInstance,
45 + treeContext: null | TreeContext,
46 // Represents the lane we should attempt to hydrate a dehydrated boundary at.
47 // OffscreenLane is the default for dehydrated boundaries.
48 // NoLane is the default for normal boundaries, which turns into "normal" pri.
packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js
+3
@@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
11 import type {Fiber} from './ReactInternalTypes';
12 import type {SuspenseInstance} from './ReactFiberHostConfig';
13 import type {Lane} from './ReactFiberLane.old';
14 +import type {TreeContext} from './ReactFiberTreeContext.old';
15 +
16 import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
17 import {NoFlags, DidCapture} from './ReactFiberFlags';
18 import {
@@ -40,6 +42,7 @@ export type SuspenseState = {|
42 // here to indicate that it is dehydrated (flag) and for quick access
43 // to check things like isSuspenseInstancePending.
44 dehydrated: null | SuspenseInstance,
45 + treeContext: null | TreeContext,
46 // Represents the lane we should attempt to hydrate a dehydrated boundary at.
47 // OffscreenLane is the default for dehydrated boundaries.
48 // NoLane is the default for normal boundaries, which turns into "normal" pri.
packages/react-reconciler/src/ReactFiberTreeContext.new.js new
+273
@@ -0,0 +1,273 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 + * @flow
8 + */
9 +
10 +// Ids are base 32 strings whose binary representation corresponds to the
11 +// position of a node in a tree.
12 +
13 +// Every time the tree forks into multiple children, we add additional bits to
14 +// the left of the sequence that represent the position of the child within the
15 +// current level of children.
16 +//
17 +// 00101 00010001011010101
18 +// ╰─┬─╯ ╰───────┬───────╯
19 +// Fork 5 of 20 Parent id
20 +//
21 +// The leading 0s are important. In the above example, you only need 3 bits to
22 +// represent slot 5. However, you need 5 bits to represent all the forks at
23 +// the current level, so we must account for the empty bits at the end.
24 +//
25 +// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise,
26 +// the zeroth id at a level would be indistinguishable from its parent.
27 +//
28 +// If a node has only one child, and does not materialize an id (i.e. does not
29 +// contain a useId hook), then we don't need to allocate any space in the
30 +// sequence. It's treated as a transparent indirection. For example, these two
31 +// trees produce the same ids:
32 +//
33 +// <> <>
34 +// <Indirection> <A />
35 +// <A /> <B />
36 +// </Indirection> </>
37 +// <B />
38 +// </>
39 +//
40 +// However, we cannot skip any node that materializes an id. Otherwise, a parent
41 +// id that does not fork would be indistinguishable from its child id. For
42 +// example, this tree does not fork, but the parent and child must have
43 +// different ids.
44 +//
45 +// <Parent>
46 +// <Child />
47 +// </Parent>
48 +//
49 +// To handle this scenario, every time we materialize an id, we allocate a
50 +// new level with a single slot. You can think of this as a fork with only one
51 +// prong, or an array of children with length 1.
52 +//
53 +// It's possible for the the size of the sequence to exceed 32 bits, the max
54 +// size for bitwise operations. When this happens, we make more room by
55 +// converting the right part of the id to a string and storing it in an overflow
56 +// variable. We use a base 32 string representation, because 32 is the largest
57 +// power of 2 that is supported by toString(). We want the base to be large so
58 +// that the resulting ids are compact, and we want the base to be a power of 2
59 +// because every log2(base) bits corresponds to a single character, i.e. every
60 +// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without
61 +// affecting the final result.
62 +
63 +import {getIsHydrating} from './ReactFiberHydrationContext.new';
64 +import {clz32} from './clz32';
65 +import {Forked, NoFlags} from './ReactFiberFlags';
66 +
67 +export type TreeContext = {
68 + id: number,
69 + overflow: string,
70 +};
71 +
72 +// TODO: Use the unified fiber stack module instead of this local one?
73 +// Intentionally not using it yet to derisk the initial implementation, because
74 +// the way we push/pop these values is a bit unusual. If there's a mistake, I'd
75 +// rather the ids be wrong than crash the whole reconciler.
76 +const forkStack: Array<any> = [];
77 +let forkStackIndex: number = 0;
78 +let treeForkProvider: Fiber | null = null;
79 +let treeForkCount: number = 0;
80 +
81 +const idStack: Array<any> = [];
82 +let idStackIndex: number = 0;
83 +let treeContextProvider: Fiber | null = null;
84 +let treeContextId: number = 1;
85 +let treeContextOverflow: string = '';
86 +
87 +export function isForkedChild(workInProgress: Fiber): boolean {
88 + warnIfNotHydrating();
89 + return (workInProgress.flags & Forked) !== NoFlags;
90 +}
91 +
92 +export function getForksAtLevel(workInProgress: Fiber): number {
93 + warnIfNotHydrating();
94 + return treeForkCount;
95 +}
96 +
97 +export function getTreeId(): string {
98 + const overflow = treeContextOverflow;
99 + const idWithLeadingBit = treeContextId;
100 + const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
101 + return id.toString(32) + overflow;
102 +}
103 +
104 +export function pushTreeFork(
105 + workInProgress: Fiber,
106 + totalChildren: number,
107 +): void {
108 + // This is called right after we reconcile an array (or iterator) of child
109 + // fibers, because that's the only place where we know how many children in
110 + // the whole set without doing extra work later, or storing addtional
111 + // information on the fiber.
112 + //
113 + // That's why this function is separate from pushTreeId — it's called during
114 + // the render phase of the fork parent, not the child, which is where we push
115 + // the other context values.
116 + //
117 + // In the Fizz implementation this is much simpler because the child is
118 + // rendered in the same callstack as the parent.
119 + //
120 + // It might be better to just add a `forks` field to the Fiber type. It would
121 + // make this module simpler.
122 +
123 + warnIfNotHydrating();
124 +
125 + forkStack[forkStackIndex++] = treeForkCount;
126 + forkStack[forkStackIndex++] = treeForkProvider;
127 +
128 + treeForkProvider = workInProgress;
129 + treeForkCount = totalChildren;
130 +}
131 +
132 +export function pushTreeId(
133 + workInProgress: Fiber,
134 + totalChildren: number,
135 + index: number,
136 +) {
137 + warnIfNotHydrating();
138 +
139 + idStack[idStackIndex++] = treeContextId;
140 + idStack[idStackIndex++] = treeContextOverflow;
141 + idStack[idStackIndex++] = treeContextProvider;
142 +
143 + treeContextProvider = workInProgress;
144 +
145 + const baseIdWithLeadingBit = treeContextId;
146 + const baseOverflow = treeContextOverflow;
147 +
148 + // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
149 + // of the id; we use it to account for leading 0s.
150 + const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
151 + const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
152 +
153 + const slot = index + 1;
154 + const length = getBitLength(totalChildren) + baseLength;
155 +
156 + // 30 is the max length we can store without overflowing, taking into
157 + // consideration the leading 1 we use to mark the end of the sequence.
158 + if (length > 30) {
159 + // We overflowed the bitwise-safe range. Fall back to slower algorithm.
160 + // This branch assumes the length of the base id is greater than 5; it won't
161 + // work for smaller ids, because you need 5 bits per character.
162 + //
163 + // We encode the id in multiple steps: first the base id, then the
164 + // remaining digits.
165 + //
166 + // Each 5 bit sequence corresponds to a single base 32 character. So for
167 + // example, if the current id is 23 bits long, we can convert 20 of those
168 + // bits into a string of 4 characters, with 3 bits left over.
169 + //
170 + // First calculate how many bits in the base id represent a complete
171 + // sequence of characters.
172 + const numberOfOverflowBits = baseLength - (baseLength % 5);
173 +
174 + // Then create a bitmask that selects only those bits.
175 + const newOverflowBits = (1 << numberOfOverflowBits) - 1;
176 +
177 + // Select the bits, and convert them to a base 32 string.
178 + const newOverflow = (baseId & newOverflowBits).toString(32);
179 +
180 + // Now we can remove those bits from the base id.
181 + const restOfBaseId = baseId >> numberOfOverflowBits;
182 + const restOfBaseLength = baseLength - numberOfOverflowBits;
183 +
184 + // Finally, encode the rest of the bits using the normal algorithm. Because
185 + // we made more room, this time it won't overflow.
186 + const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
187 + const restOfNewBits = slot << restOfBaseLength;
188 + const id = restOfNewBits | restOfBaseId;
189 + const overflow = newOverflow + baseOverflow;
190 +
191 + treeContextId = (1 << restOfLength) | id;
192 + treeContextOverflow = overflow;
193 + } else {
194 + // Normal path
195 + const newBits = slot << baseLength;
196 + const id = newBits | baseId;
197 + const overflow = baseOverflow;
198 +
199 + treeContextId = (1 << length) | id;
200 + treeContextOverflow = overflow;
201 + }
202 +}
203 +
204 +function getBitLength(number: number): number {
205 + return 32 - clz32(number);
206 +}
207 +
208 +function getLeadingBit(id: number) {
209 + return 1 << (getBitLength(id) - 1);
210 +}
211 +
212 +export function popTreeContext(workInProgress: Fiber) {
213 + // Restore the previous values.
214 +
215 + // This is a bit more complicated than other context-like modules in Fiber
216 + // because the same Fiber may appear on the stack multiple times and for
217 + // different reasons. We have to keep popping until the work-in-progress is
218 + // no longer at the top of the stack.
219 +
220 + while (workInProgress === treeForkProvider) {
221 + treeForkProvider = forkStack[--forkStackIndex];
222 + forkStack[forkStackIndex] = null;
223 + treeForkCount = forkStack[--forkStackIndex];
224 + forkStack[forkStackIndex] = null;
225 + }
226 +
227 + while (workInProgress === treeContextProvider) {
228 + treeContextProvider = idStack[--idStackIndex];
229 + idStack[idStackIndex] = null;
230 + treeContextOverflow = idStack[--idStackIndex];
231 + idStack[idStackIndex] = null;
232 + treeContextId = idStack[--idStackIndex];
233 + idStack[idStackIndex] = null;
234 + }
235 +}
236 +
237 +export function getSuspendedTreeContext(): TreeContext | null {
238 + warnIfNotHydrating();
239 + if (treeContextProvider !== null) {
240 + return {
241 + id: treeContextId,
242 + overflow: treeContextOverflow,
243 + };
244 + } else {
245 + return null;
246 + }
247 +}
248 +
249 +export function restoreSuspendedTreeContext(
250 + workInProgress: Fiber,
251 + suspendedContext: TreeContext,
252 +) {
253 + warnIfNotHydrating();
254 +
255 + idStack[idStackIndex++] = treeContextId;
256 + idStack[idStackIndex++] = treeContextOverflow;
257 + idStack[idStackIndex++] = treeContextProvider;
258 +
259 + treeContextId = suspendedContext.id;
260 + treeContextOverflow = suspendedContext.overflow;
261 + treeContextProvider = workInProgress;
262 +}
263 +
264 +function warnIfNotHydrating() {
265 + if (__DEV__) {
266 + if (!getIsHydrating()) {
267 + console.error(
268 + 'Expected to be hydrating. This is a bug in React. Please file ' +
269 + 'an issue.',
270 + );
271 + }
272 + }
273 +}
packages/react-reconciler/src/ReactFiberTreeContext.old.js new
+273
@@ -0,0 +1,273 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 + * @flow
8 + */
9 +
10 +// Ids are base 32 strings whose binary representation corresponds to the
11 +// position of a node in a tree.
12 +
13 +// Every time the tree forks into multiple children, we add additional bits to
14 +// the left of the sequence that represent the position of the child within the
15 +// current level of children.
16 +//
17 +// 00101 00010001011010101
18 +// ╰─┬─╯ ╰───────┬───────╯
19 +// Fork 5 of 20 Parent id
20 +//
21 +// The leading 0s are important. In the above example, you only need 3 bits to
22 +// represent slot 5. However, you need 5 bits to represent all the forks at
23 +// the current level, so we must account for the empty bits at the end.
24 +//
25 +// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise,
26 +// the zeroth id at a level would be indistinguishable from its parent.
27 +//
28 +// If a node has only one child, and does not materialize an id (i.e. does not
29 +// contain a useId hook), then we don't need to allocate any space in the
30 +// sequence. It's treated as a transparent indirection. For example, these two
31 +// trees produce the same ids:
32 +//
33 +// <> <>
34 +// <Indirection> <A />
35 +// <A /> <B />
36 +// </Indirection> </>
37 +// <B />
38 +// </>
39 +//
40 +// However, we cannot skip any node that materializes an id. Otherwise, a parent
41 +// id that does not fork would be indistinguishable from its child id. For
42 +// example, this tree does not fork, but the parent and child must have
43 +// different ids.
44 +//
45 +// <Parent>
46 +// <Child />
47 +// </Parent>
48 +//
49 +// To handle this scenario, every time we materialize an id, we allocate a
50 +// new level with a single slot. You can think of this as a fork with only one
51 +// prong, or an array of children with length 1.
52 +//
53 +// It's possible for the the size of the sequence to exceed 32 bits, the max
54 +// size for bitwise operations. When this happens, we make more room by
55 +// converting the right part of the id to a string and storing it in an overflow
56 +// variable. We use a base 32 string representation, because 32 is the largest
57 +// power of 2 that is supported by toString(). We want the base to be large so
58 +// that the resulting ids are compact, and we want the base to be a power of 2
59 +// because every log2(base) bits corresponds to a single character, i.e. every
60 +// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without
61 +// affecting the final result.
62 +
63 +import {getIsHydrating} from './ReactFiberHydrationContext.old';
64 +import {clz32} from './clz32';
65 +import {Forked, NoFlags} from './ReactFiberFlags';
66 +
67 +export type TreeContext = {
68 + id: number,
69 + overflow: string,
70 +};
71 +
72 +// TODO: Use the unified fiber stack module instead of this local one?
73 +// Intentionally not using it yet to derisk the initial implementation, because
74 +// the way we push/pop these values is a bit unusual. If there's a mistake, I'd
75 +// rather the ids be wrong than crash the whole reconciler.
76 +const forkStack: Array<any> = [];
77 +let forkStackIndex: number = 0;
78 +let treeForkProvider: Fiber | null = null;
79 +let treeForkCount: number = 0;
80 +
81 +const idStack: Array<any> = [];
82 +let idStackIndex: number = 0;
83 +let treeContextProvider: Fiber | null = null;
84 +let treeContextId: number = 1;
85 +let treeContextOverflow: string = '';
86 +
87 +export function isForkedChild(workInProgress: Fiber): boolean {
88 + warnIfNotHydrating();
89 + return (workInProgress.flags & Forked) !== NoFlags;
90 +}
91 +
92 +export function getForksAtLevel(workInProgress: Fiber): number {
93 + warnIfNotHydrating();
94 + return treeForkCount;
95 +}
96 +
97 +export function getTreeId(): string {
98 + const overflow = treeContextOverflow;
99 + const idWithLeadingBit = treeContextId;
100 + const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
101 + return id.toString(32) + overflow;
102 +}
103 +
104 +export function pushTreeFork(
105 + workInProgress: Fiber,
106 + totalChildren: number,
107 +): void {
108 + // This is called right after we reconcile an array (or iterator) of child
109 + // fibers, because that's the only place where we know how many children in
110 + // the whole set without doing extra work later, or storing addtional
111 + // information on the fiber.
112 + //
113 + // That's why this function is separate from pushTreeId — it's called during
114 + // the render phase of the fork parent, not the child, which is where we push
115 + // the other context values.
116 + //
117 + // In the Fizz implementation this is much simpler because the child is
118 + // rendered in the same callstack as the parent.
119 + //
120 + // It might be better to just add a `forks` field to the Fiber type. It would
121 + // make this module simpler.
122 +
123 + warnIfNotHydrating();
124 +
125 + forkStack[forkStackIndex++] = treeForkCount;
126 + forkStack[forkStackIndex++] = treeForkProvider;
127 +
128 + treeForkProvider = workInProgress;
129 + treeForkCount = totalChildren;
130 +}
131 +
132 +export function pushTreeId(
133 + workInProgress: Fiber,
134 + totalChildren: number,
135 + index: number,
136 +) {
137 + warnIfNotHydrating();
138 +
139 + idStack[idStackIndex++] = treeContextId;
140 + idStack[idStackIndex++] = treeContextOverflow;
141 + idStack[idStackIndex++] = treeContextProvider;
142 +
143 + treeContextProvider = workInProgress;
144 +
145 + const baseIdWithLeadingBit = treeContextId;
146 + const baseOverflow = treeContextOverflow;
147 +
148 + // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
149 + // of the id; we use it to account for leading 0s.
150 + const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
151 + const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
152 +
153 + const slot = index + 1;
154 + const length = getBitLength(totalChildren) + baseLength;
155 +
156 + // 30 is the max length we can store without overflowing, taking into
157 + // consideration the leading 1 we use to mark the end of the sequence.
158 + if (length > 30) {
159 + // We overflowed the bitwise-safe range. Fall back to slower algorithm.
160 + // This branch assumes the length of the base id is greater than 5; it won't
161 + // work for smaller ids, because you need 5 bits per character.
162 + //
163 + // We encode the id in multiple steps: first the base id, then the
164 + // remaining digits.
165 + //
166 + // Each 5 bit sequence corresponds to a single base 32 character. So for
167 + // example, if the current id is 23 bits long, we can convert 20 of those
168 + // bits into a string of 4 characters, with 3 bits left over.
169 + //
170 + // First calculate how many bits in the base id represent a complete
171 + // sequence of characters.
172 + const numberOfOverflowBits = baseLength - (baseLength % 5);
173 +
174 + // Then create a bitmask that selects only those bits.
175 + const newOverflowBits = (1 << numberOfOverflowBits) - 1;
176 +
177 + // Select the bits, and convert them to a base 32 string.
178 + const newOverflow = (baseId & newOverflowBits).toString(32);
179 +
180 + // Now we can remove those bits from the base id.
181 + const restOfBaseId = baseId >> numberOfOverflowBits;
182 + const restOfBaseLength = baseLength - numberOfOverflowBits;
183 +
184 + // Finally, encode the rest of the bits using the normal algorithm. Because
185 + // we made more room, this time it won't overflow.
186 + const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
187 + const restOfNewBits = slot << restOfBaseLength;
188 + const id = restOfNewBits | restOfBaseId;
189 + const overflow = newOverflow + baseOverflow;
190 +
191 + treeContextId = (1 << restOfLength) | id;
192 + treeContextOverflow = overflow;
193 + } else {
194 + // Normal path
195 + const newBits = slot << baseLength;
196 + const id = newBits | baseId;
197 + const overflow = baseOverflow;
198 +
199 + treeContextId = (1 << length) | id;
200 + treeContextOverflow = overflow;
201 + }
202 +}
203 +
204 +function getBitLength(number: number): number {
205 + return 32 - clz32(number);
206 +}
207 +
208 +function getLeadingBit(id: number) {
209 + return 1 << (getBitLength(id) - 1);
210 +}
211 +
212 +export function popTreeContext(workInProgress: Fiber) {
213 + // Restore the previous values.
214 +
215 + // This is a bit more complicated than other context-like modules in Fiber
216 + // because the same Fiber may appear on the stack multiple times and for
217 + // different reasons. We have to keep popping until the work-in-progress is
218 + // no longer at the top of the stack.
219 +
220 + while (workInProgress === treeForkProvider) {
221 + treeForkProvider = forkStack[--forkStackIndex];
222 + forkStack[forkStackIndex] = null;
223 + treeForkCount = forkStack[--forkStackIndex];
224 + forkStack[forkStackIndex] = null;
225 + }
226 +
227 + while (workInProgress === treeContextProvider) {
228 + treeContextProvider = idStack[--idStackIndex];
229 + idStack[idStackIndex] = null;
230 + treeContextOverflow = idStack[--idStackIndex];
231 + idStack[idStackIndex] = null;
232 + treeContextId = idStack[--idStackIndex];
233 + idStack[idStackIndex] = null;
234 + }
235 +}
236 +
237 +export function getSuspendedTreeContext(): TreeContext | null {
238 + warnIfNotHydrating();
239 + if (treeContextProvider !== null) {
240 + return {
241 + id: treeContextId,
242 + overflow: treeContextOverflow,
243 + };
244 + } else {
245 + return null;
246 + }
247 +}
248 +
249 +export function restoreSuspendedTreeContext(
250 + workInProgress: Fiber,
251 + suspendedContext: TreeContext,
252 +) {
253 + warnIfNotHydrating();
254 +
255 + idStack[idStackIndex++] = treeContextId;
256 + idStack[idStackIndex++] = treeContextOverflow;
257 + idStack[idStackIndex++] = treeContextProvider;
258 +
259 + treeContextId = suspendedContext.id;
260 + treeContextOverflow = suspendedContext.overflow;
261 + treeContextProvider = workInProgress;
262 +}
263 +
264 +function warnIfNotHydrating() {
265 + if (__DEV__) {
266 + if (!getIsHydrating()) {
267 + console.error(
268 + 'Expected to be hydrating. This is a bug in React. Please file ' +
269 + 'an issue.',
270 + );
271 + }
272 + }
273 +}
packages/react-reconciler/src/ReactFiberUnwindWork.new.js
+11
@@ -50,8 +50,14 @@ import {
50 popCachePool,
51 } from './ReactFiberCacheComponent.new';
52 import {transferActualDuration} from './ReactProfilerTimer.new';
53 +import {popTreeContext} from './ReactFiberTreeContext.new';
54
55 function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
56 + // Note: This intentionally doesn't check if we're hydrating because comparing
57 + // to the current tree provider fiber is just as fast and less error-prone.
58 + // Ideally we would have a special version of the work loop only
59 + // for hydration.
60 + popTreeContext(workInProgress);
61 switch (workInProgress.tag) {
62 case ClassComponent: {
63 const Component = workInProgress.type;
@@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
170 }
171
172 function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
173 + // Note: This intentionally doesn't check if we're hydrating because comparing
174 + // to the current tree provider fiber is just as fast and less error-prone.
175 + // Ideally we would have a special version of the work loop only
176 + // for hydration.
177 + popTreeContext(interruptedWork);
178 switch (interruptedWork.tag) {
179 case ClassComponent: {
180 const childContextTypes = interruptedWork.type.childContextTypes;
packages/react-reconciler/src/ReactFiberUnwindWork.old.js
+11
@@ -50,8 +50,14 @@ import {
50 popCachePool,
51 } from './ReactFiberCacheComponent.old';
52 import {transferActualDuration} from './ReactProfilerTimer.old';
53 +import {popTreeContext} from './ReactFiberTreeContext.old';
54
55 function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
56 + // Note: This intentionally doesn't check if we're hydrating because comparing
57 + // to the current tree provider fiber is just as fast and less error-prone.
58 + // Ideally we would have a special version of the work loop only
59 + // for hydration.
60 + popTreeContext(workInProgress);
61 switch (workInProgress.tag) {
62 case ClassComponent: {
63 const Component = workInProgress.type;
@@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
170 }
171
172 function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
173 + // Note: This intentionally doesn't check if we're hydrating because comparing
174 + // to the current tree provider fiber is just as fast and less error-prone.
175 + // Ideally we would have a special version of the work loop only
176 + // for hydration.
177 + popTreeContext(interruptedWork);
178 switch (interruptedWork.tag) {
179 case ClassComponent: {
180 const childContextTypes = interruptedWork.type.childContextTypes;
packages/react-reconciler/src/ReactInternalTypes.js
+2
@@ -44,6 +44,7 @@ export type HookType =
44 | 'useMutableSource'
45 | 'useSyncExternalStore'
46 | 'useOpaqueIdentifier'
47 + | 'useId'
48 | 'useCacheRefresh';
49
50 export type ContextDependency<T> = {
@@ -317,6 +318,7 @@ export type Dispatcher = {|
318 getServerSnapshot?: () => T,
319 ): T,
320 useOpaqueIdentifier(): any,
321 + useId(): string,
322 useCacheRefresh?: () => <T>(?() => T, ?T) => void,
323
324 unstable_isNewReconciler?: boolean,
packages/react-reconciler/src/clz32.js new
+25
@@ -0,0 +1,25 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 + * @flow
8 + */
9 +
10 +// TODO: This is pretty well supported by browsers. Maybe we can drop it.
11 +
12 +export const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
13 +
14 +// Count leading zeros.
15 +// Based on:
16 +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
17 +const log = Math.log;
18 +const LN2 = Math.LN2;
19 +function clz32Fallback(x: number): number {
20 + const asUint = x >>> 0;
21 + if (asUint === 0) {
22 + return 32;
23 + }
24 + return (31 - ((log(asUint) / LN2) | 0)) | 0;
25 +}
packages/react-server/src/ReactFizzHooks.js
+38 -1
@@ -17,8 +17,10 @@ import type {
17 } from 'shared/ReactTypes';
18
19 import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig';
20 +import type {Task} from './ReactFizzServer';
21
22 import {readContext as readContextImpl} from './ReactFizzNewContext';
23 +import {getTreeId} from './ReactFizzTreeContext';
24
25 import {makeServerID} from './ReactServerFormatConfig';
26
@@ -45,12 +47,15 @@ type Hook = {|
47 |};
48
49 let currentlyRenderingComponent: Object | null = null;
50 +let currentlyRenderingTask: Task | null = null;
51 let firstWorkInProgressHook: Hook | null = null;
52 let workInProgressHook: Hook | null = null;
53 // Whether the work-in-progress hook is a re-rendered hook
54 let isReRender: boolean = false;
55 // Whether an update was scheduled during the currently executing render pass.
56 let didScheduleRenderPhaseUpdate: boolean = false;
57 +// Counts the number of useId hooks in this component
58 +let localIdCounter: number = 0;
59 // Lazily created map of render-phase updates
60 let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
61 // Counter to prevent infinite loops.
@@ -163,18 +168,22 @@ function createWorkInProgressHook(): Hook {
168 return workInProgressHook;
169 }
170
166 -export function prepareToUseHooks(componentIdentity: Object): void {
171 +export function prepareToUseHooks(task: Task, componentIdentity: Object): void {
172 currentlyRenderingComponent = componentIdentity;
173 + currentlyRenderingTask = task;
174 if (__DEV__) {
175 isInHookUserCodeInDev = false;
176 }
177
178 // The following should have already been reset
179 // didScheduleRenderPhaseUpdate = false;
180 + // localIdCounter = 0;
181 // firstWorkInProgressHook = null;
182 // numberOfReRenders = 0;
183 // renderPhaseUpdates = null;
184 // workInProgressHook = null;
185 +
186 + localIdCounter = 0;
187 }
188
189 export function finishHooks(
@@ -203,6 +212,14 @@ export function finishHooks(
212 return children;
213 }
214
215 +export function checkDidRenderIdHook() {
216 + // This should be called immediately after every finishHooks call.
217 + // Conceptually, it's part of the return value of finishHooks; it's only a
218 + // separate function to avoid using an array tuple.
219 + const didRenderIdHook = localIdCounter !== 0;
220 + return didRenderIdHook;
221 +}
222 +
223 // Reset the internal hooks state if an error occurs while rendering a component
224 export function resetHooksState(): void {
225 if (__DEV__) {
@@ -210,6 +227,7 @@ export function resetHooksState(): void {
227 }
228
229 currentlyRenderingComponent = null;
230 + currentlyRenderingTask = null;
231 didScheduleRenderPhaseUpdate = false;
232 firstWorkInProgressHook = null;
233 numberOfReRenders = 0;
@@ -495,6 +513,24 @@ function useOpaqueIdentifier(): OpaqueIDType {
513 return makeServerID(currentResponseState);
514 }
515
516 +function useId(): string {
517 + const task: Task = (currentlyRenderingTask: any);
518 + const treeId = getTreeId(task.treeContext);
519 +
520 + // Use a captial R prefix for server-generated ids.
521 + let id = 'R:' + treeId;
522 +
523 + // Unless this is the first id at this level, append a number at the end
524 + // that represents the position of this useId hook among all the useId
525 + // hooks for this fiber.
526 + const localId = localIdCounter++;
527 + if (localId > 0) {
528 + id += ':' + localId.toString(32);
529 + }
530 +
531 + return id;
532 +}
533 +
534 function unsupportedRefresh() {
535 throw new Error('Cache cannot be refreshed during server rendering.');
536 }
@@ -524,6 +560,7 @@ export const Dispatcher: DispatcherType = {
560 useDeferredValue,
561 useTransition,
562 useOpaqueIdentifier,
563 + useId,
564 // Subscriptions are not setup in a server environment.
565 useMutableSource,
566 useSyncExternalStore,
packages/react-server/src/ReactFizzServer.js
+69 -14
@@ -25,6 +25,7 @@ import type {
25 } from './ReactServerFormatConfig';
26 import type {ContextSnapshot} from './ReactFizzNewContext';
27 import type {ComponentStackNode} from './ReactFizzComponentStack';
28 +import type {TreeContext} from './ReactFizzTreeContext';
29
30 import {
31 scheduleWork,
@@ -78,12 +79,14 @@ import {
79 import {
80 prepareToUseHooks,
81 finishHooks,
82 + checkDidRenderIdHook,
83 resetHooksState,
84 Dispatcher,
85 currentResponseState,
86 setCurrentResponseState,
87 } from './ReactFizzHooks';
88 import {getStackByComponentStackNode} from './ReactFizzComponentStack';
89 +import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';
90
91 import {
92 getIteratorFn,
@@ -134,7 +137,7 @@ type SuspenseBoundary = {
137 fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
138 };
139
137 -type Task = {
140 +export type Task = {
141 node: ReactNodeList,
142 ping: () => void,
143 blockedBoundary: Root | SuspenseBoundary,
@@ -142,6 +145,7 @@ type Task = {
145 abortSet: Set<Task>, // the abortable set that this task belongs to
146 legacyContext: LegacyContext, // the current legacy context that this task is executing in
147 context: ContextSnapshot, // the current new context that this task is executing in
148 + treeContext: TreeContext, // the current tree context that this task is executing in
149 componentStack: null | ComponentStackNode, // DEV-only component stack
150 };
151
@@ -265,6 +269,7 @@ export function createRequest(
269 abortSet,
270 emptyContextObject,
271 rootContextSnapshot,
272 + emptyTreeContext,
273 );
274 pingedTasks.push(rootTask);
275 return request;
@@ -302,6 +307,7 @@ function createTask(
307 abortSet: Set<Task>,
308 legacyContext: LegacyContext,
309 context: ContextSnapshot,
310 + treeContext: TreeContext,
311 ): Task {
312 request.allPendingTasks++;
313 if (blockedBoundary === null) {
@@ -317,6 +323,7 @@ function createTask(
323 abortSet,
324 legacyContext,
325 context,
326 + treeContext,
327 }: any);
328 if (__DEV__) {
329 task.componentStack = null;
@@ -497,6 +504,7 @@ function renderSuspenseBoundary(
504 fallbackAbortSet,
505 task.legacyContext,
506 task.context,
507 + task.treeContext,
508 );
509 if (__DEV__) {
510 suspendedFallbackTask.componentStack = task.componentStack;
@@ -564,7 +572,7 @@ function renderWithHooks<Props, SecondArg>(
572 secondArg: SecondArg,
573 ): any {
574 const componentIdentity = {};
567 - prepareToUseHooks(componentIdentity);
575 + prepareToUseHooks(task, componentIdentity);
576 const result = Component(props, secondArg);
577 return finishHooks(Component, props, result, secondArg);
578 }
@@ -671,6 +679,7 @@ function renderIndeterminateComponent(
679 }
680
681 const value = renderWithHooks(request, task, Component, props, legacyContext);
682 + const hasId = checkDidRenderIdHook();
683
684 if (__DEV__) {
685 // Support for module components is deprecated and is removed behind a flag.
@@ -742,7 +751,21 @@ function renderIndeterminateComponent(
751 }
752 // We're now successfully past this task, and we don't have to pop back to
753 // the previous task every again, so we can use the destructive recursive form.
745 - renderNodeDestructive(request, task, value);
754 + if (hasId) {
755 + // This component materialized an id. We treat this as its own level, with
756 + // a single "child" slot.
757 + const prevTreeContext = task.treeContext;
758 + const totalChildren = 1;
759 + const index = 0;
760 + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
761 + try {
762 + renderNodeDestructive(request, task, value);
763 + } finally {
764 + task.treeContext = prevTreeContext;
765 + }
766 + } else {
767 + renderNodeDestructive(request, task, value);
768 + }
769 }
770 popComponentStackInDEV(task);
771 }
@@ -827,7 +850,22 @@ function renderForwardRef(
850 ): void {
851 pushFunctionComponentStackInDEV(task, type.render);
852 const children = renderWithHooks(request, task, type.render, props, ref);
830 - renderNodeDestructive(request, task, children);
853 + const hasId = checkDidRenderIdHook();
854 + if (hasId) {
855 + // This component materialized an id. We treat this as its own level, with
856 + // a single "child" slot.
857 + const prevTreeContext = task.treeContext;
858 + const totalChildren = 1;
859 + const index = 0;
860 + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
861 + try {
862 + renderNodeDestructive(request, task, children);
863 + } finally {
864 + task.treeContext = prevTreeContext;
865 + }
866 + } else {
867 + renderNodeDestructive(request, task, children);
868 + }
869 popComponentStackInDEV(task);
870 }
871
@@ -1122,12 +1160,7 @@ function renderNodeDestructive(
1160 }
1161
1162 if (isArray(node)) {
1125 - for (let i = 0; i < node.length; i++) {
1126 - // Recursively render the rest. We need to use the non-destructive form
1127 - // so that we can safely pop back up and render the sibling if something
1128 - // suspends.
1129 - renderNode(request, task, node[i]);
1130 - }
1163 + renderChildrenArray(request, task, node);
1164 return;
1165 }
1166
@@ -1138,18 +1171,23 @@ function renderNodeDestructive(
1171 }
1172 const iterator = iteratorFn.call(node);
1173 if (iterator) {
1174 + // We need to know how many total children are in this set, so that we
1175 + // can allocate enough id slots to acommodate them. So we must exhaust
1176 + // the iterator before we start recursively rendering the children.
1177 + // TODO: This is not great but I think it's inherent to the id
1178 + // generation algorithm.
1179 let step = iterator.next();
1180 // If there are not entries, we need to push an empty so we start by checking that.
1181 if (!step.done) {
1182 + const children = [];
1183 do {
1145 - // Recursively render the rest. We need to use the non-destructive form
1146 - // so that we can safely pop back up and render the sibling if something
1147 - // suspends.
1148 - renderNode(request, task, step.value);
1184 + children.push(step.value);
1185 step = iterator.next();
1186 } while (!step.done);
1187 + renderChildrenArray(request, task, children);
1188 return;
1189 }
1190 + return;
1191 }
1192 }
1193
@@ -1191,6 +1229,21 @@ function renderNodeDestructive(
1229 }
1230 }
1231
1232 +function renderChildrenArray(request, task, children) {
1233 + const totalChildren = children.length;
1234 + for (let i = 0; i < totalChildren; i++) {
1235 + const prevTreeContext = task.treeContext;
1236 + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
1237 + try {
1238 + // We need to use the non-destructive form so that we can safely pop back
1239 + // up and render the sibling if something suspends.
1240 + renderNode(request, task, children[i]);
1241 + } finally {
1242 + task.treeContext = prevTreeContext;
1243 + }
1244 + }
1245 +}
1246 +
1247 function spawnNewSuspendedTask(
1248 request: Request,
1249 task: Task,
@@ -1214,6 +1267,7 @@ function spawnNewSuspendedTask(
1267 task.abortSet,
1268 task.legacyContext,
1269 task.context,
1270 + task.treeContext,
1271 );
1272 if (__DEV__) {
1273 if (task.componentStack !== null) {
@@ -1257,6 +1311,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
1311 if (__DEV__) {
1312 task.componentStack = previousComponentStack;
1313 }
1314 + return;
1315 } else {
1316 // Restore the context. We assume that this will be restored by the inner
1317 // functions in case nothing throws so we don't use "finally" here.
packages/react-server/src/ReactFizzTreeContext.js new
+168
@@ -0,0 +1,168 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 + * @flow
8 + */
9 +
10 +// Ids are base 32 strings whose binary representation corresponds to the
11 +// position of a node in a tree.
12 +
13 +// Every time the tree forks into multiple children, we add additional bits to
14 +// the left of the sequence that represent the position of the child within the
15 +// current level of children.
16 +//
17 +// 00101 00010001011010101
18 +// ╰─┬─╯ ╰───────┬───────╯
19 +// Fork 5 of 20 Parent id
20 +//
21 +// The leading 0s are important. In the above example, you only need 3 bits to
22 +// represent slot 5. However, you need 5 bits to represent all the forks at
23 +// the current level, so we must account for the empty bits at the end.
24 +//
25 +// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise,
26 +// the zeroth id at a level would be indistinguishable from its parent.
27 +//
28 +// If a node has only one child, and does not materialize an id (i.e. does not
29 +// contain a useId hook), then we don't need to allocate any space in the
30 +// sequence. It's treated as a transparent indirection. For example, these two
31 +// trees produce the same ids:
32 +//
33 +// <> <>
34 +// <Indirection> <A />
35 +// <A /> <B />
36 +// </Indirection> </>
37 +// <B />
38 +// </>
39 +//
40 +// However, we cannot skip any node that materializes an id. Otherwise, a parent
41 +// id that does not fork would be indistinguishable from its child id. For
42 +// example, this tree does not fork, but the parent and child must have
43 +// different ids.
44 +//
45 +// <Parent>
46 +// <Child />
47 +// </Parent>
48 +//
49 +// To handle this scenario, every time we materialize an id, we allocate a
50 +// new level with a single slot. You can think of this as a fork with only one
51 +// prong, or an array of children with length 1.
52 +//
53 +// It's possible for the the size of the sequence to exceed 32 bits, the max
54 +// size for bitwise operations. When this happens, we make more room by
55 +// converting the right part of the id to a string and storing it in an overflow
56 +// variable. We use a base 32 string representation, because 32 is the largest
57 +// power of 2 that is supported by toString(). We want the base to be large so
58 +// that the resulting ids are compact, and we want the base to be a power of 2
59 +// because every log2(base) bits corresponds to a single character, i.e. every
60 +// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without
61 +// affecting the final result.
62 +
63 +export type TreeContext = {
64 + +id: number,
65 + +overflow: string,
66 +};
67 +
68 +export const emptyTreeContext = {
69 + id: 1,
70 + overflow: '',
71 +};
72 +
73 +export function getTreeId(context: TreeContext): string {
74 + const overflow = context.overflow;
75 + const idWithLeadingBit = context.id;
76 + const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
77 + return id.toString(32) + overflow;
78 +}
79 +
80 +export function pushTreeContext(
81 + baseContext: TreeContext,
82 + totalChildren: number,
83 + index: number,
84 +): TreeContext {
85 + const baseIdWithLeadingBit = baseContext.id;
86 + const baseOverflow = baseContext.overflow;
87 +
88 + // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
89 + // of the id; we use it to account for leading 0s.
90 + const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
91 + const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
92 +
93 + const slot = index + 1;
94 + const length = getBitLength(totalChildren) + baseLength;
95 +
96 + // 30 is the max length we can store without overflowing, taking into
97 + // consideration the leading 1 we use to mark the end of the sequence.
98 + if (length > 30) {
99 + // We overflowed the bitwise-safe range. Fall back to slower algorithm.
100 + // This branch assumes the length of the base id is greater than 5; it won't
101 + // work for smaller ids, because you need 5 bits per character.
102 + //
103 + // We encode the id in multiple steps: first the base id, then the
104 + // remaining digits.
105 + //
106 + // Each 5 bit sequence corresponds to a single base 32 character. So for
107 + // example, if the current id is 23 bits long, we can convert 20 of those
108 + // bits into a string of 4 characters, with 3 bits left over.
109 + //
110 + // First calculate how many bits in the base id represent a complete
111 + // sequence of characters.
112 + const numberOfOverflowBits = baseLength - (baseLength % 5);
113 +
114 + // Then create a bitmask that selects only those bits.
115 + const newOverflowBits = (1 << numberOfOverflowBits) - 1;
116 +
117 + // Select the bits, and convert them to a base 32 string.
118 + const newOverflow = (baseId & newOverflowBits).toString(32);
119 +
120 + // Now we can remove those bits from the base id.
121 + const restOfBaseId = baseId >> numberOfOverflowBits;
122 + const restOfBaseLength = baseLength - numberOfOverflowBits;
123 +
124 + // Finally, encode the rest of the bits using the normal algorithm. Because
125 + // we made more room, this time it won't overflow.
126 + const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
127 + const restOfNewBits = slot << restOfBaseLength;
128 + const id = restOfNewBits | restOfBaseId;
129 + const overflow = newOverflow + baseOverflow;
130 + return {
131 + id: (1 << restOfLength) | id,
132 + overflow,
133 + };
134 + } else {
135 + // Normal path
136 + const newBits = slot << baseLength;
137 + const id = newBits | baseId;
138 + const overflow = baseOverflow;
139 + return {
140 + id: (1 << length) | id,
141 + overflow,
142 + };
143 + }
144 +}
145 +
146 +function getBitLength(number: number): number {
147 + return 32 - clz32(number);
148 +}
149 +
150 +function getLeadingBit(id: number) {
151 + return 1 << (getBitLength(id) - 1);
152 +}
153 +
154 +// TODO: Math.clz32 is supported in Node 12+. Maybe we can drop the fallback.
155 +const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
156 +
157 +// Count leading zeros.
158 +// Based on:
159 +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
160 +const log = Math.log;
161 +const LN2 = Math.LN2;
162 +function clz32Fallback(x: number): number {
163 + const asUint = x >>> 0;
164 + if (asUint === 0) {
165 + return 32;
166 + }
167 + return (31 - ((log(asUint) / LN2) | 0)) | 0;
168 +}
packages/react-server/src/ReactFlightServer.js
+1
@@ -846,6 +846,7 @@ const Dispatcher: DispatcherType = {
846 useImperativeHandle: (unsupportedHook: any),
847 useEffect: (unsupportedHook: any),
848 useOpaqueIdentifier: (unsupportedHook: any),
849 + useId: (unsupportedHook: any),
850 useMutableSource: (unsupportedHook: any),
851 useSyncExternalStore: (unsupportedHook: any),
852 useCacheRefresh(): <T>(?() => T, ?T) => void {
packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
+1
@@ -43,6 +43,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
43 useDeferredValue: unsupported,
44 useTransition: unsupported,
45 useOpaqueIdentifier: unsupported,
46 + useId: unsupported,
47 useMutableSource: unsupported,
48 useSyncExternalStore: unsupported,
49 useCacheRefresh: unsupported,
packages/react/index.classic.fb.js
+1
@@ -41,6 +41,7 @@ export {
41 unstable_getCacheForType,
42 unstable_useCacheRefresh,
43 unstable_useOpaqueIdentifier,
44 + unstable_useId,
45 useCallback,
46 useContext,
47 useDebugValue,
packages/react/index.experimental.js
+1
@@ -37,6 +37,7 @@ export {
37 unstable_getCacheForType,
38 unstable_useCacheRefresh,
39 unstable_useOpaqueIdentifier,
40 + unstable_useId,
41 useCallback,
42 useContext,
43 useDebugValue,
packages/react/index.js
+1
@@ -62,6 +62,7 @@ export {
62 unstable_getCacheForType,
63 unstable_useCacheRefresh,
64 unstable_useOpaqueIdentifier,
65 + unstable_useId,
66 useCallback,
67 useContext,
68 useDebugValue,
packages/react/index.modern.fb.js
+1
@@ -40,6 +40,7 @@ export {
40 unstable_getCacheForType,
41 unstable_useCacheRefresh,
42 unstable_useOpaqueIdentifier,
43 + unstable_useId,
44 useCallback,
45 useContext,
46 useDebugValue,
packages/react/index.stable.js
+1
@@ -30,6 +30,7 @@ export {
30 memo,
31 startTransition,
32 unstable_useOpaqueIdentifier,
33 + unstable_useId,
34 useCallback,
35 useContext,
36 useDebugValue,
packages/react/src/React.js
+2
@@ -53,6 +53,7 @@ import {
53 useTransition,
54 useDeferredValue,
55 useOpaqueIdentifier,
56 + useId,
57 useCacheRefresh,
58 } from './ReactHooks';
59 import {
@@ -127,5 +128,6 @@ export {
128 // enableScopeAPI
129 REACT_SCOPE_TYPE as unstable_Scope,
130 useOpaqueIdentifier as unstable_useOpaqueIdentifier,
131 + useId as unstable_useId,
132 act,
133 };
packages/react/src/ReactHooks.js
+5
@@ -174,6 +174,11 @@ export function useOpaqueIdentifier(): OpaqueIDType | void {
174 return dispatcher.useOpaqueIdentifier();
175 }
176
177 +export function useId(): string {
178 + const dispatcher = resolveDispatcher();
179 + return dispatcher.useId();
180 +}
181 +
182 export function useMutableSource<Source, Snapshot>(
183 source: MutableSource<Source>,
184 getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
packages/react/unstable-shared-subset.experimental.js
+1
@@ -28,6 +28,7 @@ export {
28 unstable_getCacheSignal,
29 unstable_getCacheForType,
30 unstable_useOpaqueIdentifier,
31 + unstable_useId,
32 useCallback,
33 useContext,
34 useDebugValue,