* 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.
This commit is contained in:
Andrew Clark 2021-11-01 16:30:44 -04:00 committed by GitHub
parent a0d991fe65
commit ebf9ae8579
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1819 additions and 77 deletions

View File

@ -341,6 +341,17 @@ function useOpaqueIdentifier(): OpaqueIDType | void {
return value;
}
function useId(): string {
const hook = nextHook();
const id = hook !== null ? hook.memoizedState : '';
hookLog.push({
primitive: 'Id',
stackError: new Error(),
value: id,
});
return id;
}
const Dispatcher: DispatcherType = {
getCacheForType,
readContext,
@ -361,6 +372,7 @@ const Dispatcher: DispatcherType = {
useSyncExternalStore,
useDeferredValue,
useOpaqueIdentifier,
useId,
};
// Inspect

View File

@ -628,7 +628,7 @@ describe('ReactHooksInspectionIntegration', () => {
it('should support composite useOpaqueIdentifier hook in concurrent mode', () => {
function Foo(props) {
const id = React.unstable_useOpaqueIdentifier();
const [state] = React.useState(() => 'hello', []);
const [state] = React.useState('hello');
return <div id={id}>{state}</div>;
}
@ -656,6 +656,33 @@ describe('ReactHooksInspectionIntegration', () => {
});
});
it('should support useId hook', () => {
function Foo(props) {
const id = React.unstable_useId();
const [state] = React.useState('hello');
return <div id={id}>{state}</div>;
}
const renderer = ReactTestRenderer.create(<Foo />);
const childFiber = renderer.root.findByType(Foo)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree.length).toEqual(2);
expect(tree[0].id).toEqual(0);
expect(tree[0].isStateEditable).toEqual(false);
expect(tree[0].name).toEqual('Id');
expect(String(tree[0].value).startsWith('r:')).toBe(true);
expect(tree[1]).toEqual({
id: 1,
isStateEditable: true,
name: 'State',
value: 'hello',
subHooks: [],
});
});
describe('useDebugValue', () => {
it('should support inspectable values for multiple custom hooks', () => {
function useLabeledValue(label) {

View File

@ -0,0 +1,515 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
let JSDOM;
let React;
let ReactDOM;
let Scheduler;
let clientAct;
let ReactDOMFizzServer;
let Stream;
let Suspense;
let useId;
let document;
let writable;
let container;
let buffer = '';
let hasErrored = false;
let fatalError = undefined;
describe('useId', () => {
beforeEach(() => {
jest.resetModules();
JSDOM = require('jsdom').JSDOM;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
clientAct = require('jest-react').act;
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
Suspense = React.Suspense;
useId = React.unstable_useId;
// Test Environment
const jsdom = new JSDOM(
'<!DOCTYPE html><html><head></head><body><div id="container">',
{
runScripts: 'dangerously',
},
);
document = jsdom.window.document;
container = document.getElementById('container');
buffer = '';
hasErrored = false;
writable = new Stream.PassThrough();
writable.setEncoding('utf8');
writable.on('data', chunk => {
buffer += chunk;
});
writable.on('error', error => {
hasErrored = true;
fatalError = error;
});
});
async function serverAct(callback) {
await callback();
// Await one turn around the event loop.
// This assumes that we'll flush everything we have so far.
await new Promise(resolve => {
setImmediate(resolve);
});
if (hasErrored) {
throw fatalError;
}
// JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
// We also want to execute any scripts that are embedded.
// We assume that we have now received a proper fragment of HTML.
const bufferedContent = buffer;
buffer = '';
const fakeBody = document.createElement('body');
fakeBody.innerHTML = bufferedContent;
while (fakeBody.firstChild) {
const node = fakeBody.firstChild;
if (node.nodeName === 'SCRIPT') {
const script = document.createElement('script');
script.textContent = node.textContent;
fakeBody.removeChild(node);
container.appendChild(script);
} else {
container.appendChild(node);
}
}
}
function normalizeTreeIdForTesting(id) {
const [serverClientPrefix, base32, hookIndex] = id.split(':');
if (serverClientPrefix === 'r') {
// Client ids aren't stable. For testing purposes, strip out the counter.
return (
'CLIENT_GENERATED_ID' +
(hookIndex !== undefined ? ` (${hookIndex})` : '')
);
}
// Formats the tree id as a binary sequence, so it's easier to visualize
// the structure.
return (
parseInt(base32, 32).toString(2) +
(hookIndex !== undefined ? ` (${hookIndex})` : '')
);
}
function DivWithId({children}) {
const id = normalizeTreeIdForTesting(useId());
return <div id={id}>{children}</div>;
}
test('basic example', async () => {
function App() {
return (
<div>
<div>
<DivWithId />
<DivWithId />
</div>
<DivWithId />
</div>
);
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
<div>
<div
id="101"
/>
<div
id="1001"
/>
</div>
<div
id="10"
/>
</div>
</div>
`);
});
test('indirections', async () => {
function App() {
// There are no forks in this tree, but the parent and the child should
// have different ids.
return (
<DivWithId>
<div>
<div>
<div>
<DivWithId />
</div>
</div>
</div>
</DivWithId>
);
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="0"
>
<div>
<div>
<div>
<div
id="1"
/>
</div>
</div>
</div>
</div>
</div>
`);
});
test('empty (null) children', async () => {
// We don't treat empty children different from non-empty ones, which means
// they get allocated a slot when generating ids. There's no inherent reason
// to do this; Fiber happens to allocate a fiber for null children that
// appear in a list, which is not ideal for performance. For the purposes
// of id generation, though, what matters is that Fizz and Fiber
// are consistent.
function App() {
return (
<>
{null}
<DivWithId />
{null}
<DivWithId />
</>
);
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="10"
/>
<div
id="100"
/>
</div>
`);
});
test('large ids', async () => {
// The component in this test outputs a recursive tree of nodes with ids,
// where the underlying binary representation is an alternating series of 1s
// and 0s. In other words, they are all of the form 101010101.
//
// Because we use base 32 encoding, the resulting id should consist of
// alternating 'a' (01010) and 'l' (10101) characters, except for the the
// 'R:' prefix, and the first character after that, which may not correspond
// to a complete set of 5 bits.
//
// Example: R:clalalalalalalala...
//
// We can use this pattern to test large ids that exceed the bitwise
// safe range (32 bits). The algorithm should theoretically support ids
// of any size.
function Child({children}) {
const id = useId();
return <div id={id}>{children}</div>;
}
function App() {
let tree = <Child />;
for (let i = 0; i < 50; i++) {
tree = (
<>
<Child />
{tree}
</>
);
}
return tree;
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
const divs = container.querySelectorAll('div');
// Confirm that every id matches the expected pattern
for (let i = 0; i < divs.length; i++) {
// Example: R:clalalalalalalala...
expect(divs[i].id).toMatch(/^R:.(((al)*a?)((la)*l?))*$/);
}
});
test('multiple ids in a single component', async () => {
function App() {
const id1 = useId();
const id2 = useId();
const id3 = useId();
return `${id1}, ${id2}, ${id3}`;
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
// We append a suffix to the end of the id to distinguish them
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
R:0, R:0:1, R:0:2
<!-- -->
</div>
`);
});
test('basic incremental hydration', async () => {
function App() {
return (
<div>
<Suspense fallback="Loading...">
<DivWithId label="A" />
<DivWithId label="B" />
</Suspense>
<DivWithId label="C" />
</div>
);
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div>
<!--$-->
<div
id="101"
/>
<div
id="1001"
/>
<!--/$-->
<div
id="10"
/>
</div>
</div>
`);
});
test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => {
const span = React.createRef(null);
function App({swap}) {
// Note: Using a dynamic array so these are treated as insertions and
// deletions instead of updates, because Fiber currently allocates a node
// even for empty children.
const children = [
<DivWithId key="A" />,
swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
<DivWithId key="D" />,
];
return (
<>
{children}
<Suspense key="boundary" fallback="Loading...">
<DivWithId />
<span ref={span} />
</Suspense>
</>
);
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
const dehydratedSpan = container.getElementsByTagName('span')[0];
await clientAct(async () => {
const root = ReactDOM.hydrateRoot(container, <App />);
expect(Scheduler).toFlushUntilNextPaint([]);
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="101"
/>
<div
id="1001"
/>
<div
id="1101"
/>
<!--$-->
<div
id="110"
/>
<span />
<!--/$-->
</div>
`);
// The inner boundary hasn't hydrated yet
expect(span.current).toBe(null);
// Swap B for C
root.render(<App swap={true} />);
});
// The swap should not have caused a mismatch.
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<div
id="101"
/>
<div
id="CLIENT_GENERATED_ID"
/>
<div
id="1101"
/>
<!--$-->
<div
id="110"
/>
<span />
<!--/$-->
</div>
`);
// Should have hydrated successfully
expect(span.current).toBe(dehydratedSpan);
});
test('inserting/deleting siblings inside a dehydrated Suspense boundary', async () => {
const span = React.createRef(null);
function App({swap}) {
// Note: Using a dynamic array so these are treated as insertions and
// deletions instead of updates, because Fiber currently allocates a node
// even for empty children.
const children = [
<DivWithId key="A" />,
swap ? <DivWithId key="C" /> : <DivWithId key="B" />,
<DivWithId key="D" />,
];
return (
<Suspense key="boundary" fallback="Loading...">
{children}
<span ref={span} />
</Suspense>
);
}
await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});
const dehydratedSpan = container.getElementsByTagName('span')[0];
await clientAct(async () => {
const root = ReactDOM.hydrateRoot(container, <App />);
expect(Scheduler).toFlushUntilNextPaint([]);
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<!--$-->
<div
id="101"
/>
<div
id="1001"
/>
<div
id="1101"
/>
<span />
<!--/$-->
</div>
`);
// The inner boundary hasn't hydrated yet
expect(span.current).toBe(null);
// Swap B for C
root.render(<App swap={true} />);
});
// The swap should not have caused a mismatch.
expect(container).toMatchInlineSnapshot(`
<div
id="container"
>
<!--$-->
<div
id="101"
/>
<div
id="CLIENT_GENERATED_ID"
/>
<div
id="1101"
/>
<span />
<!--/$-->
</div>
`);
// Should have hydrated successfully
expect(span.current).toBe(dehydratedSpan);
});
});

View File

@ -519,6 +519,10 @@ function useOpaqueIdentifier(): OpaqueIDType {
);
}
function useId(): OpaqueIDType {
throw new Error('Not implemented.');
}
function useCacheRefresh(): <T>(?() => T, ?T) => void {
throw new Error('Not implemented.');
}
@ -549,6 +553,7 @@ export const Dispatcher: DispatcherType = {
useDeferredValue,
useTransition,
useOpaqueIdentifier,
useId,
// Subscriptions are not setup in a server environment.
useMutableSource,
useSyncExternalStore,

View File

@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.new';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import {Placement, ChildDeletion} from './ReactFiberFlags';
import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
@ -40,6 +40,8 @@ import {
import {emptyRefsObject} from './ReactFiberClassComponent.new';
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new';
import {StrictLegacyMode} from './ReactTypeOfMode';
import {getIsHydrating} from './ReactFiberHydrationContext.new';
import {pushTreeFork} from './ReactFiberTreeContext.new';
let didWarnAboutMaps;
let didWarnAboutGenerators;
@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) {
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// Noop.
// During hydration, the useId algorithm needs to know which fibers are
// part of a list of children (arrays, iterators).
newFiber.flags |= Forked;
return lastPlacedIndex;
}
const current = newFiber.alternate;
@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}

View File

@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import {Placement, ChildDeletion} from './ReactFiberFlags';
import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
@ -40,6 +40,8 @@ import {
import {emptyRefsObject} from './ReactFiberClassComponent.old';
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old';
import {StrictLegacyMode} from './ReactTypeOfMode';
import {getIsHydrating} from './ReactFiberHydrationContext.old';
import {pushTreeFork} from './ReactFiberTreeContext.old';
let didWarnAboutMaps;
let didWarnAboutGenerators;
@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) {
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// Noop.
// During hydration, the useId algorithm needs to know which fibers are
// part of a list of children (arrays, iterators).
newFiber.flags |= Forked;
return lastPlacedIndex;
}
const current = newFiber.alternate;
@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}

View File

@ -186,6 +186,7 @@ import {
invalidateContextProvider,
} from './ReactFiberContext.new';
import {
getIsHydrating,
enterHydrationState,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.new';
import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new';
import is from 'shared/objectIs';
import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new';
import {
getForksAtLevel,
isForkedChild,
pushTreeId,
} from './ReactFiberTreeContext.new';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@ -1757,6 +1763,7 @@ function mountIndeterminateComponent(
}
}
}
reconcileChildren(null, workInProgress, value, renderLanes);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
treeContext: null,
retryLane: NoLane,
};
@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent(
reenterHydrationStateFromDehydratedSuspenseInstance(
workInProgress,
suspenseInstance,
suspenseState.treeContext,
);
const nextProps = workInProgress.pendingProps;
const primaryChildren = nextProps.children;
@ -3675,6 +3684,21 @@ function beginWork(
}
} else {
didReceiveUpdate = false;
if (getIsHydrating() && isForkedChild(workInProgress)) {
// Check if this child belongs to a list of muliple children in
// its parent.
//
// In a true multi-threaded implementation, we would render children on
// parallel threads. This would represent the beginning of a new render
// thread for this subtree.
//
// We only use this for id generation during hydration, which is why the
// logic is located in this special branch.
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
// Before entering the begin phase, clear pending update priority.

View File

@ -186,6 +186,7 @@ import {
invalidateContextProvider,
} from './ReactFiberContext.old';
import {
getIsHydrating,
enterHydrationState,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.old';
import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old';
import is from 'shared/objectIs';
import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old';
import {
getForksAtLevel,
isForkedChild,
pushTreeId,
} from './ReactFiberTreeContext.old';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@ -1757,6 +1763,7 @@ function mountIndeterminateComponent(
}
}
}
reconcileChildren(null, workInProgress, value, renderLanes);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
treeContext: null,
retryLane: NoLane,
};
@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent(
reenterHydrationStateFromDehydratedSuspenseInstance(
workInProgress,
suspenseInstance,
suspenseState.treeContext,
);
const nextProps = workInProgress.pendingProps;
const primaryChildren = nextProps.children;
@ -3675,6 +3684,21 @@ function beginWork(
}
} else {
didReceiveUpdate = false;
if (getIsHydrating() && isForkedChild(workInProgress)) {
// Check if this child belongs to a list of muliple children in
// its parent.
//
// In a true multi-threaded implementation, we would render children on
// parallel threads. This would represent the beginning of a new render
// thread for this subtree.
//
// We only use this for id generation during hydration, which is why the
// logic is located in this special branch.
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
// Before entering the begin phase, clear pending update priority.

View File

@ -155,6 +155,7 @@ import {
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.new';
import {popTreeContext} from './ReactFiberTreeContext.new';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@ -822,7 +823,11 @@ function completeWork(
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:

View File

@ -155,6 +155,7 @@ import {
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.old';
import {popTreeContext} from './ReactFiberTreeContext.old';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@ -822,7 +823,11 @@ function completeWork(
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:

View File

@ -12,54 +12,55 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b0000000000000000000000000;
export const PerformedWork = /* */ 0b0000000000000000000000001;
export const NoFlags = /* */ 0b00000000000000000000000000;
export const PerformedWork = /* */ 0b00000000000000000000000001;
// You can change the rest (and add more).
export const Placement = /* */ 0b0000000000000000000000010;
export const Update = /* */ 0b0000000000000000000000100;
export const Placement = /* */ 0b00000000000000000000000010;
export const Update = /* */ 0b00000000000000000000000100;
export const PlacementAndUpdate = /* */ Placement | Update;
export const Deletion = /* */ 0b0000000000000000000001000;
export const ChildDeletion = /* */ 0b0000000000000000000010000;
export const ContentReset = /* */ 0b0000000000000000000100000;
export const Callback = /* */ 0b0000000000000000001000000;
export const DidCapture = /* */ 0b0000000000000000010000000;
export const ForceClientRender = /* */ 0b0000000000000000100000000;
export const Ref = /* */ 0b0000000000000001000000000;
export const Snapshot = /* */ 0b0000000000000010000000000;
export const Passive = /* */ 0b0000000000000100000000000;
export const Hydrating = /* */ 0b0000000000001000000000000;
export const Deletion = /* */ 0b00000000000000000000001000;
export const ChildDeletion = /* */ 0b00000000000000000000010000;
export const ContentReset = /* */ 0b00000000000000000000100000;
export const Callback = /* */ 0b00000000000000000001000000;
export const DidCapture = /* */ 0b00000000000000000010000000;
export const ForceClientRender = /* */ 0b00000000000000000100000000;
export const Ref = /* */ 0b00000000000000001000000000;
export const Snapshot = /* */ 0b00000000000000010000000000;
export const Passive = /* */ 0b00000000000000100000000000;
export const Hydrating = /* */ 0b00000000000001000000000000;
export const HydratingAndUpdate = /* */ Hydrating | Update;
export const Visibility = /* */ 0b0000000000010000000000000;
export const StoreConsistency = /* */ 0b0000000000100000000000000;
export const Visibility = /* */ 0b00000000000010000000000000;
export const StoreConsistency = /* */ 0b00000000000100000000000000;
export const LifecycleEffectMask =
Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
// Union of all commit flags (flags with the lifetime of a particular commit)
export const HostEffectMask = /* */ 0b0000000000111111111111111;
export const HostEffectMask = /* */ 0b00000000000111111111111111;
// These are not really side effects, but we still reuse this field.
export const Incomplete = /* */ 0b0000000001000000000000000;
export const ShouldCapture = /* */ 0b0000000010000000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b0000000100000000000000000;
export const DidPropagateContext = /* */ 0b0000001000000000000000000;
export const NeedsPropagation = /* */ 0b0000010000000000000000000;
export const Incomplete = /* */ 0b00000000001000000000000000;
export const ShouldCapture = /* */ 0b00000000010000000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b00000000100000000000000000;
export const DidPropagateContext = /* */ 0b00000001000000000000000000;
export const NeedsPropagation = /* */ 0b00000010000000000000000000;
export const Forked = /* */ 0b00000100000000000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const RefStatic = /* */ 0b0000100000000000000000000;
export const LayoutStatic = /* */ 0b0001000000000000000000000;
export const PassiveStatic = /* */ 0b0010000000000000000000000;
export const RefStatic = /* */ 0b00001000000000000000000000;
export const LayoutStatic = /* */ 0b00010000000000000000000000;
export const PassiveStatic = /* */ 0b00100000000000000000000000;
// These flags allow us to traverse to fibers that have effects on mount
// without traversing the entire tree after every commit for
// double invoking
export const MountLayoutDev = /* */ 0b0100000000000000000000000;
export const MountPassiveDev = /* */ 0b1000000000000000000000000;
export const MountLayoutDev = /* */ 0b01000000000000000000000000;
export const MountPassiveDev = /* */ 0b10000000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.

View File

@ -117,6 +117,7 @@ import {
} from './ReactUpdateQueue.new';
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.new';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false;
// TODO: Maybe there's some way to consolidate this with
// `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Counts the number of useId hooks in this component.
let localIdCounter: number = 0;
// Used for ids that are generated completely client-side (i.e. not during
// hydration). This counter is global, so client ids are not stable across
// render attempts.
let globalClientIdCounter: number = 0;
const RE_RENDER_LIMIT = 25;
@ -396,6 +403,7 @@ export function renderWithHooks<Props, SecondArg>(
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@ -543,6 +551,21 @@ export function renderWithHooks<Props, SecondArg>(
}
}
if (localIdCounter !== 0) {
localIdCounter = 0;
if (getIsHydrating()) {
// This component materialized an id. This will affect any ids that appear
// in its children.
const returnFiber = workInProgress.return;
if (returnFiber !== null) {
const numberOfForks = 1;
const slotIndex = 0;
pushTreeFork(workInProgress, numberOfForks);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
}
return children;
}
@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void {
}
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
return id;
}
function mountId(): string {
const hook = mountWorkInProgressHook();
let id;
if (getIsHydrating()) {
const treeId = getTreeId();
// Use a captial R prefix for server-generated ids.
id = 'R:' + treeId;
// Unless this is the first id at this level, append a number at the end
// that represents the position of this useId hook among all the useId
// hooks for this fiber.
const localId = localIdCounter++;
if (localId > 0) {
id += ':' + localId.toString(32);
}
} else {
// Use a lowercase r prefix for client-generated ids.
const globalClientId = globalClientIdCounter++;
id = 'r:' + globalClientId.toString(32);
}
hook.memoizedState = id;
return id;
}
function updateId(): string {
const hook = updateWorkInProgressHook();
const id: string = hook.memoizedState;
return id;
}
function mountRefresh() {
const hook = mountWorkInProgressHook();
const refresh = (hook.memoizedState = refreshCache.bind(
@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
useId: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: mountOpaqueIdentifier,
useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useOpaqueIdentifier: updateOpaqueIdentifier,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: rerenderOpaqueIdentifier,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2680,6 +2741,11 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
mountHookTypesDev();
return mountId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -2822,6 +2888,11 @@ if (__DEV__) {
updateHookTypesDev();
return mountOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
return mountId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -2964,6 +3035,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3107,6 +3183,11 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3266,6 +3347,12 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
mountHookTypesDev();
return mountId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3425,6 +3512,12 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3585,6 +3678,12 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};

View File

@ -117,6 +117,7 @@ import {
} from './ReactUpdateQueue.old';
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.old';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false;
// TODO: Maybe there's some way to consolidate this with
// `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Counts the number of useId hooks in this component.
let localIdCounter: number = 0;
// Used for ids that are generated completely client-side (i.e. not during
// hydration). This counter is global, so client ids are not stable across
// render attempts.
let globalClientIdCounter: number = 0;
const RE_RENDER_LIMIT = 25;
@ -396,6 +403,7 @@ export function renderWithHooks<Props, SecondArg>(
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@ -543,6 +551,21 @@ export function renderWithHooks<Props, SecondArg>(
}
}
if (localIdCounter !== 0) {
localIdCounter = 0;
if (getIsHydrating()) {
// This component materialized an id. This will affect any ids that appear
// in its children.
const returnFiber = workInProgress.return;
if (returnFiber !== null) {
const numberOfForks = 1;
const slotIndex = 0;
pushTreeFork(workInProgress, numberOfForks);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
}
return children;
}
@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void {
}
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
return id;
}
function mountId(): string {
const hook = mountWorkInProgressHook();
let id;
if (getIsHydrating()) {
const treeId = getTreeId();
// Use a captial R prefix for server-generated ids.
id = 'R:' + treeId;
// Unless this is the first id at this level, append a number at the end
// that represents the position of this useId hook among all the useId
// hooks for this fiber.
const localId = localIdCounter++;
if (localId > 0) {
id += ':' + localId.toString(32);
}
} else {
// Use a lowercase r prefix for client-generated ids.
const globalClientId = globalClientIdCounter++;
id = 'r:' + globalClientId.toString(32);
}
hook.memoizedState = id;
return id;
}
function updateId(): string {
const hook = updateWorkInProgressHook();
const id: string = hook.memoizedState;
return id;
}
function mountRefresh() {
const hook = mountWorkInProgressHook();
const refresh = (hook.memoizedState = refreshCache.bind(
@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
useId: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: mountOpaqueIdentifier,
useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useOpaqueIdentifier: updateOpaqueIdentifier,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: rerenderOpaqueIdentifier,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@ -2680,6 +2741,11 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
mountHookTypesDev();
return mountId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -2822,6 +2888,11 @@ if (__DEV__) {
updateHookTypesDev();
return mountOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
return mountId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -2964,6 +3035,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3107,6 +3183,11 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3266,6 +3347,12 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
mountHookTypesDev();
return mountId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3425,6 +3512,12 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};
@ -3585,6 +3678,12 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
useId(): string {
currentHookNameInDev = 'useId';
warnInvalidHookAccess();
updateHookTypesDev();
return updateId();
},
unstable_isNewReconciler: enableNewReconciler,
};

View File

@ -17,6 +17,7 @@ import type {
HostContext,
} from './ReactFiberHostConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {TreeContext} from './ReactFiberTreeContext.new';
import {
HostComponent,
@ -62,6 +63,10 @@ import {
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.new';
import {
getSuspendedTreeContext,
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.new';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
function reenterHydrationStateFromDehydratedSuspenseInstance(
fiber: Fiber,
suspenseInstance: SuspenseInstance,
treeContext: TreeContext | null,
): boolean {
if (!supportsHydration) {
return false;
@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
return true;
}
@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
treeContext: getSuspendedTreeContext(),
retryLane: OffscreenLane,
};
fiber.memoizedState = suspenseState;

View File

@ -17,6 +17,7 @@ import type {
HostContext,
} from './ReactFiberHostConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import type {TreeContext} from './ReactFiberTreeContext.old';
import {
HostComponent,
@ -62,6 +63,10 @@ import {
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.old';
import {
getSuspendedTreeContext,
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.old';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
function reenterHydrationStateFromDehydratedSuspenseInstance(
fiber: Fiber,
suspenseInstance: SuspenseInstance,
treeContext: TreeContext | null,
): boolean {
if (!supportsHydration) {
return false;
@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
return true;
}
@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
treeContext: getSuspendedTreeContext(),
retryLane: OffscreenLane,
};
fiber.memoizedState = suspenseState;

View File

@ -23,6 +23,7 @@ import {
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
import {clz32} from './clz32';
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler.
// If those values are changed that package should be rebuilt and redeployed.
@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
lanes &= ~lane;
}
}
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Count leading zeros. Only used on lanes, so assume input is an integer.
// Based on:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
const log = Math.log;
const LN2 = Math.LN2;
function clz32Fallback(lanes: Lanes | Lane) {
if (lanes === 0) {
return 32;
}
return (31 - ((log(lanes) / LN2) | 0)) | 0;
}

View File

@ -23,6 +23,7 @@ import {
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
import {clz32} from './clz32';
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler.
// If those values are changed that package should be rebuilt and redeployed.
@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
lanes &= ~lane;
}
}
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Count leading zeros. Only used on lanes, so assume input is an integer.
// Based on:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
const log = Math.log;
const LN2 = Math.LN2;
function clz32Fallback(lanes: Lanes | Lane) {
if (lanes === 0) {
return 32;
}
return (31 - ((log(lanes) / LN2) | 0)) | 0;
}

View File

@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {Lane} from './ReactFiberLane.new';
import type {TreeContext} from './ReactFiberTreeContext.new';
import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoFlags, DidCapture} from './ReactFiberFlags';
import {
@ -40,6 +42,7 @@ export type SuspenseState = {|
// here to indicate that it is dehydrated (flag) and for quick access
// to check things like isSuspenseInstancePending.
dehydrated: null | SuspenseInstance,
treeContext: null | TreeContext,
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.

View File

@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {Lane} from './ReactFiberLane.old';
import type {TreeContext} from './ReactFiberTreeContext.old';
import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoFlags, DidCapture} from './ReactFiberFlags';
import {
@ -40,6 +42,7 @@ export type SuspenseState = {|
// here to indicate that it is dehydrated (flag) and for quick access
// to check things like isSuspenseInstancePending.
dehydrated: null | SuspenseInstance,
treeContext: null | TreeContext,
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.

View File

@ -0,0 +1,273 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// 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 node that 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.
import {getIsHydrating} from './ReactFiberHydrationContext.new';
import {clz32} from './clz32';
import {Forked, NoFlags} from './ReactFiberFlags';
export type TreeContext = {
id: number,
overflow: string,
};
// TODO: Use the unified fiber stack module instead of this local one?
// Intentionally not using it yet to derisk the initial implementation, because
// the way we push/pop these values is a bit unusual. If there's a mistake, I'd
// rather the ids be wrong than crash the whole reconciler.
const forkStack: Array<any> = [];
let forkStackIndex: number = 0;
let treeForkProvider: Fiber | null = null;
let treeForkCount: number = 0;
const idStack: Array<any> = [];
let idStackIndex: number = 0;
let treeContextProvider: Fiber | null = null;
let treeContextId: number = 1;
let treeContextOverflow: string = '';
export function isForkedChild(workInProgress: Fiber): boolean {
warnIfNotHydrating();
return (workInProgress.flags & Forked) !== NoFlags;
}
export function getForksAtLevel(workInProgress: Fiber): number {
warnIfNotHydrating();
return treeForkCount;
}
export function getTreeId(): string {
const overflow = treeContextOverflow;
const idWithLeadingBit = treeContextId;
const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
return id.toString(32) + overflow;
}
export function pushTreeFork(
workInProgress: Fiber,
totalChildren: number,
): void {
// This is called right after we reconcile an array (or iterator) of child
// fibers, because that's the only place where we know how many children in
// the whole set without doing extra work later, or storing addtional
// information on the fiber.
//
// That's why this function is separate from pushTreeId — it's called during
// the render phase of the fork parent, not the child, which is where we push
// the other context values.
//
// In the Fizz implementation this is much simpler because the child is
// rendered in the same callstack as the parent.
//
// It might be better to just add a `forks` field to the Fiber type. It would
// make this module simpler.
warnIfNotHydrating();
forkStack[forkStackIndex++] = treeForkCount;
forkStack[forkStackIndex++] = treeForkProvider;
treeForkProvider = workInProgress;
treeForkCount = totalChildren;
}
export function pushTreeId(
workInProgress: Fiber,
totalChildren: number,
index: number,
) {
warnIfNotHydrating();
idStack[idStackIndex++] = treeContextId;
idStack[idStackIndex++] = treeContextOverflow;
idStack[idStackIndex++] = treeContextProvider;
treeContextProvider = workInProgress;
const baseIdWithLeadingBit = treeContextId;
const baseOverflow = treeContextOverflow;
// The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
// of the id; we use it to account for leading 0s.
const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
const slot = index + 1;
const length = getBitLength(totalChildren) + baseLength;
// 30 is the max length we can store without overflowing, taking into
// consideration the leading 1 we use to mark the end of the sequence.
if (length > 30) {
// We overflowed the bitwise-safe range. Fall back to slower algorithm.
// This branch assumes the length of the base id is greater than 5; it won't
// work for smaller ids, because you need 5 bits per character.
//
// We encode the id in multiple steps: first the base id, then the
// remaining digits.
//
// Each 5 bit sequence corresponds to a single base 32 character. So for
// example, if the current id is 23 bits long, we can convert 20 of those
// bits into a string of 4 characters, with 3 bits left over.
//
// First calculate how many bits in the base id represent a complete
// sequence of characters.
const numberOfOverflowBits = baseLength - (baseLength % 5);
// Then create a bitmask that selects only those bits.
const newOverflowBits = (1 << numberOfOverflowBits) - 1;
// Select the bits, and convert them to a base 32 string.
const newOverflow = (baseId & newOverflowBits).toString(32);
// Now we can remove those bits from the base id.
const restOfBaseId = baseId >> numberOfOverflowBits;
const restOfBaseLength = baseLength - numberOfOverflowBits;
// Finally, encode the rest of the bits using the normal algorithm. Because
// we made more room, this time it won't overflow.
const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
const restOfNewBits = slot << restOfBaseLength;
const id = restOfNewBits | restOfBaseId;
const overflow = newOverflow + baseOverflow;
treeContextId = (1 << restOfLength) | id;
treeContextOverflow = overflow;
} else {
// Normal path
const newBits = slot << baseLength;
const id = newBits | baseId;
const overflow = baseOverflow;
treeContextId = (1 << length) | id;
treeContextOverflow = overflow;
}
}
function getBitLength(number: number): number {
return 32 - clz32(number);
}
function getLeadingBit(id: number) {
return 1 << (getBitLength(id) - 1);
}
export function popTreeContext(workInProgress: Fiber) {
// Restore the previous values.
// This is a bit more complicated than other context-like modules in Fiber
// because the same Fiber may appear on the stack multiple times and for
// different reasons. We have to keep popping until the work-in-progress is
// no longer at the top of the stack.
while (workInProgress === treeForkProvider) {
treeForkProvider = forkStack[--forkStackIndex];
forkStack[forkStackIndex] = null;
treeForkCount = forkStack[--forkStackIndex];
forkStack[forkStackIndex] = null;
}
while (workInProgress === treeContextProvider) {
treeContextProvider = idStack[--idStackIndex];
idStack[idStackIndex] = null;
treeContextOverflow = idStack[--idStackIndex];
idStack[idStackIndex] = null;
treeContextId = idStack[--idStackIndex];
idStack[idStackIndex] = null;
}
}
export function getSuspendedTreeContext(): TreeContext | null {
warnIfNotHydrating();
if (treeContextProvider !== null) {
return {
id: treeContextId,
overflow: treeContextOverflow,
};
} else {
return null;
}
}
export function restoreSuspendedTreeContext(
workInProgress: Fiber,
suspendedContext: TreeContext,
) {
warnIfNotHydrating();
idStack[idStackIndex++] = treeContextId;
idStack[idStackIndex++] = treeContextOverflow;
idStack[idStackIndex++] = treeContextProvider;
treeContextId = suspendedContext.id;
treeContextOverflow = suspendedContext.overflow;
treeContextProvider = workInProgress;
}
function warnIfNotHydrating() {
if (__DEV__) {
if (!getIsHydrating()) {
console.error(
'Expected to be hydrating. This is a bug in React. Please file ' +
'an issue.',
);
}
}
}

View File

@ -0,0 +1,273 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// 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 node that 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.
import {getIsHydrating} from './ReactFiberHydrationContext.old';
import {clz32} from './clz32';
import {Forked, NoFlags} from './ReactFiberFlags';
export type TreeContext = {
id: number,
overflow: string,
};
// TODO: Use the unified fiber stack module instead of this local one?
// Intentionally not using it yet to derisk the initial implementation, because
// the way we push/pop these values is a bit unusual. If there's a mistake, I'd
// rather the ids be wrong than crash the whole reconciler.
const forkStack: Array<any> = [];
let forkStackIndex: number = 0;
let treeForkProvider: Fiber | null = null;
let treeForkCount: number = 0;
const idStack: Array<any> = [];
let idStackIndex: number = 0;
let treeContextProvider: Fiber | null = null;
let treeContextId: number = 1;
let treeContextOverflow: string = '';
export function isForkedChild(workInProgress: Fiber): boolean {
warnIfNotHydrating();
return (workInProgress.flags & Forked) !== NoFlags;
}
export function getForksAtLevel(workInProgress: Fiber): number {
warnIfNotHydrating();
return treeForkCount;
}
export function getTreeId(): string {
const overflow = treeContextOverflow;
const idWithLeadingBit = treeContextId;
const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
return id.toString(32) + overflow;
}
export function pushTreeFork(
workInProgress: Fiber,
totalChildren: number,
): void {
// This is called right after we reconcile an array (or iterator) of child
// fibers, because that's the only place where we know how many children in
// the whole set without doing extra work later, or storing addtional
// information on the fiber.
//
// That's why this function is separate from pushTreeId — it's called during
// the render phase of the fork parent, not the child, which is where we push
// the other context values.
//
// In the Fizz implementation this is much simpler because the child is
// rendered in the same callstack as the parent.
//
// It might be better to just add a `forks` field to the Fiber type. It would
// make this module simpler.
warnIfNotHydrating();
forkStack[forkStackIndex++] = treeForkCount;
forkStack[forkStackIndex++] = treeForkProvider;
treeForkProvider = workInProgress;
treeForkCount = totalChildren;
}
export function pushTreeId(
workInProgress: Fiber,
totalChildren: number,
index: number,
) {
warnIfNotHydrating();
idStack[idStackIndex++] = treeContextId;
idStack[idStackIndex++] = treeContextOverflow;
idStack[idStackIndex++] = treeContextProvider;
treeContextProvider = workInProgress;
const baseIdWithLeadingBit = treeContextId;
const baseOverflow = treeContextOverflow;
// The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
// of the id; we use it to account for leading 0s.
const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
const slot = index + 1;
const length = getBitLength(totalChildren) + baseLength;
// 30 is the max length we can store without overflowing, taking into
// consideration the leading 1 we use to mark the end of the sequence.
if (length > 30) {
// We overflowed the bitwise-safe range. Fall back to slower algorithm.
// This branch assumes the length of the base id is greater than 5; it won't
// work for smaller ids, because you need 5 bits per character.
//
// We encode the id in multiple steps: first the base id, then the
// remaining digits.
//
// Each 5 bit sequence corresponds to a single base 32 character. So for
// example, if the current id is 23 bits long, we can convert 20 of those
// bits into a string of 4 characters, with 3 bits left over.
//
// First calculate how many bits in the base id represent a complete
// sequence of characters.
const numberOfOverflowBits = baseLength - (baseLength % 5);
// Then create a bitmask that selects only those bits.
const newOverflowBits = (1 << numberOfOverflowBits) - 1;
// Select the bits, and convert them to a base 32 string.
const newOverflow = (baseId & newOverflowBits).toString(32);
// Now we can remove those bits from the base id.
const restOfBaseId = baseId >> numberOfOverflowBits;
const restOfBaseLength = baseLength - numberOfOverflowBits;
// Finally, encode the rest of the bits using the normal algorithm. Because
// we made more room, this time it won't overflow.
const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
const restOfNewBits = slot << restOfBaseLength;
const id = restOfNewBits | restOfBaseId;
const overflow = newOverflow + baseOverflow;
treeContextId = (1 << restOfLength) | id;
treeContextOverflow = overflow;
} else {
// Normal path
const newBits = slot << baseLength;
const id = newBits | baseId;
const overflow = baseOverflow;
treeContextId = (1 << length) | id;
treeContextOverflow = overflow;
}
}
function getBitLength(number: number): number {
return 32 - clz32(number);
}
function getLeadingBit(id: number) {
return 1 << (getBitLength(id) - 1);
}
export function popTreeContext(workInProgress: Fiber) {
// Restore the previous values.
// This is a bit more complicated than other context-like modules in Fiber
// because the same Fiber may appear on the stack multiple times and for
// different reasons. We have to keep popping until the work-in-progress is
// no longer at the top of the stack.
while (workInProgress === treeForkProvider) {
treeForkProvider = forkStack[--forkStackIndex];
forkStack[forkStackIndex] = null;
treeForkCount = forkStack[--forkStackIndex];
forkStack[forkStackIndex] = null;
}
while (workInProgress === treeContextProvider) {
treeContextProvider = idStack[--idStackIndex];
idStack[idStackIndex] = null;
treeContextOverflow = idStack[--idStackIndex];
idStack[idStackIndex] = null;
treeContextId = idStack[--idStackIndex];
idStack[idStackIndex] = null;
}
}
export function getSuspendedTreeContext(): TreeContext | null {
warnIfNotHydrating();
if (treeContextProvider !== null) {
return {
id: treeContextId,
overflow: treeContextOverflow,
};
} else {
return null;
}
}
export function restoreSuspendedTreeContext(
workInProgress: Fiber,
suspendedContext: TreeContext,
) {
warnIfNotHydrating();
idStack[idStackIndex++] = treeContextId;
idStack[idStackIndex++] = treeContextOverflow;
idStack[idStackIndex++] = treeContextProvider;
treeContextId = suspendedContext.id;
treeContextOverflow = suspendedContext.overflow;
treeContextProvider = workInProgress;
}
function warnIfNotHydrating() {
if (__DEV__) {
if (!getIsHydrating()) {
console.error(
'Expected to be hydrating. This is a bug in React. Please file ' +
'an issue.',
);
}
}
}

View File

@ -50,8 +50,14 @@ import {
popCachePool,
} from './ReactFiberCacheComponent.new';
import {transferActualDuration} from './ReactProfilerTimer.new';
import {popTreeContext} from './ReactFiberTreeContext.new';
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
}
function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(interruptedWork);
switch (interruptedWork.tag) {
case ClassComponent: {
const childContextTypes = interruptedWork.type.childContextTypes;

View File

@ -50,8 +50,14 @@ import {
popCachePool,
} from './ReactFiberCacheComponent.old';
import {transferActualDuration} from './ReactProfilerTimer.old';
import {popTreeContext} from './ReactFiberTreeContext.old';
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
}
function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(interruptedWork);
switch (interruptedWork.tag) {
case ClassComponent: {
const childContextTypes = interruptedWork.type.childContextTypes;

View File

@ -44,6 +44,7 @@ export type HookType =
| 'useMutableSource'
| 'useSyncExternalStore'
| 'useOpaqueIdentifier'
| 'useId'
| 'useCacheRefresh';
export type ContextDependency<T> = {
@ -317,6 +318,7 @@ export type Dispatcher = {|
getServerSnapshot?: () => T,
): T,
useOpaqueIdentifier(): any,
useId(): string,
useCacheRefresh?: () => <T>(?() => T, ?T) => void,
unstable_isNewReconciler?: boolean,

25
packages/react-reconciler/src/clz32.js vendored Normal file
View File

@ -0,0 +1,25 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// TODO: This is pretty well supported by browsers. Maybe we can drop it.
export const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Count leading zeros.
// Based on:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
const log = Math.log;
const LN2 = Math.LN2;
function clz32Fallback(x: number): number {
const asUint = x >>> 0;
if (asUint === 0) {
return 32;
}
return (31 - ((log(asUint) / LN2) | 0)) | 0;
}

View File

@ -17,8 +17,10 @@ import type {
} from 'shared/ReactTypes';
import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig';
import type {Task} from './ReactFizzServer';
import {readContext as readContextImpl} from './ReactFizzNewContext';
import {getTreeId} from './ReactFizzTreeContext';
import {makeServerID} from './ReactServerFormatConfig';
@ -45,12 +47,15 @@ type Hook = {|
|};
let currentlyRenderingComponent: Object | null = null;
let currentlyRenderingTask: Task | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
// Whether the work-in-progress hook is a re-rendered hook
let isReRender: boolean = false;
// Whether an update was scheduled during the currently executing render pass.
let didScheduleRenderPhaseUpdate: boolean = false;
// Counts the number of useId hooks in this component
let localIdCounter: number = 0;
// Lazily created map of render-phase updates
let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null;
// Counter to prevent infinite loops.
@ -163,18 +168,22 @@ function createWorkInProgressHook(): Hook {
return workInProgressHook;
}
export function prepareToUseHooks(componentIdentity: Object): void {
export function prepareToUseHooks(task: Task, componentIdentity: Object): void {
currentlyRenderingComponent = componentIdentity;
currentlyRenderingTask = task;
if (__DEV__) {
isInHookUserCodeInDev = false;
}
// The following should have already been reset
// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// firstWorkInProgressHook = null;
// numberOfReRenders = 0;
// renderPhaseUpdates = null;
// workInProgressHook = null;
localIdCounter = 0;
}
export function finishHooks(
@ -203,6 +212,14 @@ export function finishHooks(
return children;
}
export function checkDidRenderIdHook() {
// This should be called immediately after every finishHooks call.
// Conceptually, it's part of the return value of finishHooks; it's only a
// separate function to avoid using an array tuple.
const didRenderIdHook = localIdCounter !== 0;
return didRenderIdHook;
}
// Reset the internal hooks state if an error occurs while rendering a component
export function resetHooksState(): void {
if (__DEV__) {
@ -210,6 +227,7 @@ export function resetHooksState(): void {
}
currentlyRenderingComponent = null;
currentlyRenderingTask = null;
didScheduleRenderPhaseUpdate = false;
firstWorkInProgressHook = null;
numberOfReRenders = 0;
@ -495,6 +513,24 @@ function useOpaqueIdentifier(): OpaqueIDType {
return makeServerID(currentResponseState);
}
function useId(): string {
const task: Task = (currentlyRenderingTask: any);
const treeId = getTreeId(task.treeContext);
// Use a captial R prefix for server-generated ids.
let id = 'R:' + treeId;
// Unless this is the first id at this level, append a number at the end
// that represents the position of this useId hook among all the useId
// hooks for this fiber.
const localId = localIdCounter++;
if (localId > 0) {
id += ':' + localId.toString(32);
}
return id;
}
function unsupportedRefresh() {
throw new Error('Cache cannot be refreshed during server rendering.');
}
@ -524,6 +560,7 @@ export const Dispatcher: DispatcherType = {
useDeferredValue,
useTransition,
useOpaqueIdentifier,
useId,
// Subscriptions are not setup in a server environment.
useMutableSource,
useSyncExternalStore,

View File

@ -25,6 +25,7 @@ import type {
} from './ReactServerFormatConfig';
import type {ContextSnapshot} from './ReactFizzNewContext';
import type {ComponentStackNode} from './ReactFizzComponentStack';
import type {TreeContext} from './ReactFizzTreeContext';
import {
scheduleWork,
@ -78,12 +79,14 @@ import {
import {
prepareToUseHooks,
finishHooks,
checkDidRenderIdHook,
resetHooksState,
Dispatcher,
currentResponseState,
setCurrentResponseState,
} from './ReactFizzHooks';
import {getStackByComponentStackNode} from './ReactFizzComponentStack';
import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';
import {
getIteratorFn,
@ -134,7 +137,7 @@ type SuspenseBoundary = {
fallbackAbortableTasks: Set<Task>, // used to cancel task on the fallback if the boundary completes or gets canceled.
};
type Task = {
export type Task = {
node: ReactNodeList,
ping: () => void,
blockedBoundary: Root | SuspenseBoundary,
@ -142,6 +145,7 @@ type Task = {
abortSet: Set<Task>, // the abortable set that this task belongs to
legacyContext: LegacyContext, // the current legacy context that this task is executing in
context: ContextSnapshot, // the current new context that this task is executing in
treeContext: TreeContext, // the current tree context that this task is executing in
componentStack: null | ComponentStackNode, // DEV-only component stack
};
@ -265,6 +269,7 @@ export function createRequest(
abortSet,
emptyContextObject,
rootContextSnapshot,
emptyTreeContext,
);
pingedTasks.push(rootTask);
return request;
@ -302,6 +307,7 @@ function createTask(
abortSet: Set<Task>,
legacyContext: LegacyContext,
context: ContextSnapshot,
treeContext: TreeContext,
): Task {
request.allPendingTasks++;
if (blockedBoundary === null) {
@ -317,6 +323,7 @@ function createTask(
abortSet,
legacyContext,
context,
treeContext,
}: any);
if (__DEV__) {
task.componentStack = null;
@ -497,6 +504,7 @@ function renderSuspenseBoundary(
fallbackAbortSet,
task.legacyContext,
task.context,
task.treeContext,
);
if (__DEV__) {
suspendedFallbackTask.componentStack = task.componentStack;
@ -564,7 +572,7 @@ function renderWithHooks<Props, SecondArg>(
secondArg: SecondArg,
): any {
const componentIdentity = {};
prepareToUseHooks(componentIdentity);
prepareToUseHooks(task, componentIdentity);
const result = Component(props, secondArg);
return finishHooks(Component, props, result, secondArg);
}
@ -671,6 +679,7 @@ function renderIndeterminateComponent(
}
const value = renderWithHooks(request, task, Component, props, legacyContext);
const hasId = checkDidRenderIdHook();
if (__DEV__) {
// Support for module components is deprecated and is removed behind a flag.
@ -742,7 +751,21 @@ function renderIndeterminateComponent(
}
// We're now successfully past this task, and we don't have to pop back to
// the previous task every again, so we can use the destructive recursive form.
renderNodeDestructive(request, task, value);
if (hasId) {
// This component materialized an id. We treat this as its own level, with
// a single "child" slot.
const prevTreeContext = task.treeContext;
const totalChildren = 1;
const index = 0;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
try {
renderNodeDestructive(request, task, value);
} finally {
task.treeContext = prevTreeContext;
}
} else {
renderNodeDestructive(request, task, value);
}
}
popComponentStackInDEV(task);
}
@ -827,7 +850,22 @@ function renderForwardRef(
): void {
pushFunctionComponentStackInDEV(task, type.render);
const children = renderWithHooks(request, task, type.render, props, ref);
renderNodeDestructive(request, task, children);
const hasId = checkDidRenderIdHook();
if (hasId) {
// This component materialized an id. We treat this as its own level, with
// a single "child" slot.
const prevTreeContext = task.treeContext;
const totalChildren = 1;
const index = 0;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
try {
renderNodeDestructive(request, task, children);
} finally {
task.treeContext = prevTreeContext;
}
} else {
renderNodeDestructive(request, task, children);
}
popComponentStackInDEV(task);
}
@ -1122,12 +1160,7 @@ function renderNodeDestructive(
}
if (isArray(node)) {
for (let i = 0; i < node.length; i++) {
// Recursively render the rest. We need to use the non-destructive form
// so that we can safely pop back up and render the sibling if something
// suspends.
renderNode(request, task, node[i]);
}
renderChildrenArray(request, task, node);
return;
}
@ -1138,18 +1171,23 @@ function renderNodeDestructive(
}
const iterator = iteratorFn.call(node);
if (iterator) {
// We need to know how many total children are in this set, so that we
// can allocate enough id slots to acommodate them. So we must exhaust
// the iterator before we start recursively rendering the children.
// TODO: This is not great but I think it's inherent to the id
// generation algorithm.
let step = iterator.next();
// If there are not entries, we need to push an empty so we start by checking that.
if (!step.done) {
const children = [];
do {
// Recursively render the rest. We need to use the non-destructive form
// so that we can safely pop back up and render the sibling if something
// suspends.
renderNode(request, task, step.value);
children.push(step.value);
step = iterator.next();
} while (!step.done);
renderChildrenArray(request, task, children);
return;
}
return;
}
}
@ -1191,6 +1229,21 @@ function renderNodeDestructive(
}
}
function renderChildrenArray(request, task, children) {
const totalChildren = children.length;
for (let i = 0; i < totalChildren; i++) {
const prevTreeContext = task.treeContext;
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
try {
// We need to use the non-destructive form so that we can safely pop back
// up and render the sibling if something suspends.
renderNode(request, task, children[i]);
} finally {
task.treeContext = prevTreeContext;
}
}
}
function spawnNewSuspendedTask(
request: Request,
task: Task,
@ -1214,6 +1267,7 @@ function spawnNewSuspendedTask(
task.abortSet,
task.legacyContext,
task.context,
task.treeContext,
);
if (__DEV__) {
if (task.componentStack !== null) {
@ -1257,6 +1311,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
if (__DEV__) {
task.componentStack = previousComponentStack;
}
return;
} else {
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.

View File

@ -0,0 +1,168 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// 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 node that 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.
export type TreeContext = {
+id: number,
+overflow: string,
};
export const emptyTreeContext = {
id: 1,
overflow: '',
};
export function getTreeId(context: TreeContext): string {
const overflow = context.overflow;
const idWithLeadingBit = context.id;
const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
return id.toString(32) + overflow;
}
export function pushTreeContext(
baseContext: TreeContext,
totalChildren: number,
index: number,
): TreeContext {
const baseIdWithLeadingBit = baseContext.id;
const baseOverflow = baseContext.overflow;
// The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
// of the id; we use it to account for leading 0s.
const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
const slot = index + 1;
const length = getBitLength(totalChildren) + baseLength;
// 30 is the max length we can store without overflowing, taking into
// consideration the leading 1 we use to mark the end of the sequence.
if (length > 30) {
// We overflowed the bitwise-safe range. Fall back to slower algorithm.
// This branch assumes the length of the base id is greater than 5; it won't
// work for smaller ids, because you need 5 bits per character.
//
// We encode the id in multiple steps: first the base id, then the
// remaining digits.
//
// Each 5 bit sequence corresponds to a single base 32 character. So for
// example, if the current id is 23 bits long, we can convert 20 of those
// bits into a string of 4 characters, with 3 bits left over.
//
// First calculate how many bits in the base id represent a complete
// sequence of characters.
const numberOfOverflowBits = baseLength - (baseLength % 5);
// Then create a bitmask that selects only those bits.
const newOverflowBits = (1 << numberOfOverflowBits) - 1;
// Select the bits, and convert them to a base 32 string.
const newOverflow = (baseId & newOverflowBits).toString(32);
// Now we can remove those bits from the base id.
const restOfBaseId = baseId >> numberOfOverflowBits;
const restOfBaseLength = baseLength - numberOfOverflowBits;
// Finally, encode the rest of the bits using the normal algorithm. Because
// we made more room, this time it won't overflow.
const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
const restOfNewBits = slot << restOfBaseLength;
const id = restOfNewBits | restOfBaseId;
const overflow = newOverflow + baseOverflow;
return {
id: (1 << restOfLength) | id,
overflow,
};
} else {
// Normal path
const newBits = slot << baseLength;
const id = newBits | baseId;
const overflow = baseOverflow;
return {
id: (1 << length) | id,
overflow,
};
}
}
function getBitLength(number: number): number {
return 32 - clz32(number);
}
function getLeadingBit(id: number) {
return 1 << (getBitLength(id) - 1);
}
// TODO: Math.clz32 is supported in Node 12+. Maybe we can drop the fallback.
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Count leading zeros.
// Based on:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
const log = Math.log;
const LN2 = Math.LN2;
function clz32Fallback(x: number): number {
const asUint = x >>> 0;
if (asUint === 0) {
return 32;
}
return (31 - ((log(asUint) / LN2) | 0)) | 0;
}

View File

@ -846,6 +846,7 @@ const Dispatcher: DispatcherType = {
useImperativeHandle: (unsupportedHook: any),
useEffect: (unsupportedHook: any),
useOpaqueIdentifier: (unsupportedHook: any),
useId: (unsupportedHook: any),
useMutableSource: (unsupportedHook: any),
useSyncExternalStore: (unsupportedHook: any),
useCacheRefresh(): <T>(?() => T, ?T) => void {

View File

@ -43,6 +43,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
useDeferredValue: unsupported,
useTransition: unsupported,
useOpaqueIdentifier: unsupported,
useId: unsupported,
useMutableSource: unsupported,
useSyncExternalStore: unsupported,
useCacheRefresh: unsupported,

View File

@ -41,6 +41,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
unstable_useId,
useCallback,
useContext,
useDebugValue,

View File

@ -37,6 +37,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
unstable_useId,
useCallback,
useContext,
useDebugValue,

View File

@ -62,6 +62,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
unstable_useId,
useCallback,
useContext,
useDebugValue,

View File

@ -40,6 +40,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
unstable_useId,
useCallback,
useContext,
useDebugValue,

View File

@ -30,6 +30,7 @@ export {
memo,
startTransition,
unstable_useOpaqueIdentifier,
unstable_useId,
useCallback,
useContext,
useDebugValue,

View File

@ -53,6 +53,7 @@ import {
useTransition,
useDeferredValue,
useOpaqueIdentifier,
useId,
useCacheRefresh,
} from './ReactHooks';
import {
@ -127,5 +128,6 @@ export {
// enableScopeAPI
REACT_SCOPE_TYPE as unstable_Scope,
useOpaqueIdentifier as unstable_useOpaqueIdentifier,
useId as unstable_useId,
act,
};

View File

@ -174,6 +174,11 @@ export function useOpaqueIdentifier(): OpaqueIDType | void {
return dispatcher.useOpaqueIdentifier();
}
export function useId(): string {
const dispatcher = resolveDispatcher();
return dispatcher.useId();
}
export function useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,

View File

@ -28,6 +28,7 @@ export {
unstable_getCacheSignal,
unstable_getCacheForType,
unstable_useOpaqueIdentifier,
unstable_useId,
useCallback,
useContext,
useDebugValue,