From 9e2d8ddadb44d5d7f78a52694c0ce7df684e4895 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 28 Oct 2021 10:04:40 -0400 Subject: [PATCH] Initial useId implementation --- .../src/__tests__/ReactDOMUseId-test.js | 134 +++++++++++++++ .../src/ReactFiberBeginWork.new.js | 60 ++++++- .../src/ReactFiberBeginWork.old.js | 60 ++++++- .../src/ReactFiberCompleteWork.new.js | 7 +- .../src/ReactFiberCompleteWork.old.js | 7 +- .../src/ReactFiberHooks.new.js | 47 +++++- .../src/ReactFiberHooks.old.js | 47 +++++- .../src/ReactFiberLane.new.js | 15 +- .../src/ReactFiberLane.old.js | 15 +- .../src/ReactFiberTreeContext.new.js | 158 ++++++++++++++++++ .../src/ReactFiberUnwindWork.new.js | 11 ++ .../src/ReactFiberUnwindWork.old.js | 11 ++ packages/react-reconciler/src/clz32.js | 25 +++ packages/react-server/src/ReactFizzHooks.js | 29 +++- packages/react-server/src/ReactFizzServer.js | 99 +++++++++-- .../react-server/src/ReactFizzTreeContext.js | 129 ++++++++++++++ 16 files changed, 799 insertions(+), 55 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactDOMUseId-test.js create mode 100644 packages/react-reconciler/src/ReactFiberTreeContext.new.js create mode 100644 packages/react-reconciler/src/clz32.js create mode 100644 packages/react-server/src/ReactFizzTreeContext.js diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js new file mode 100644 index 0000000000000..ba3de5b5c8f7f --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -0,0 +1,134 @@ +/** + * 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 clientAct; +let ReactDOMFizzServer; +let Stream; +let useId; +let useRef; +let useEffect; +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'); + clientAct = require('jest-react').act; + ReactDOMFizzServer = require('react-dom/server'); + Stream = require('stream'); + // TODO: Rename API + useId = React.unstable_useId; + useRef = React.useRef; + useEffect = React.useEffect; + + // Test Environment + const jsdom = new JSDOM( + '
', + { + 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 DivWithId({label, children}) { + const id = useId(); + const ref = useRef(null); + useEffect(() => { + const div = ref.current; + if (div !== null) { + if (div.id !== id) { + throw new Error('Server and client ids do not match'); + } + } + }, [id]); + return ( +
+ {children} +
+ ); + } + + test('basic usage', async () => { + function App() { + return ( +
+
+ + +
+ +
+ ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + + await clientAct(async () => { + ReactDOM.hydrateRoot(container, ); + }); + }); +}); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 4fe648bc3e767..1dd2b955047ae 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -174,7 +174,11 @@ import { prepareToReadContext, scheduleWorkOnParentPath, } from './ReactFiberNewContext.new'; -import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.new'; +import { + renderWithHooks, + checkDidRenderIdHook, + bailoutHooks, +} from './ReactFiberHooks.new'; import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.new'; import { getMaskedContext, @@ -186,6 +190,7 @@ import { invalidateContextProvider, } from './ReactFiberContext.new'; import { + getIsHydrating, enterHydrationState, reenterHydrationStateFromDehydratedSuspenseInstance, resetHydrationState, @@ -235,6 +240,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.new'; import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new'; import is from 'shared/objectIs'; import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new'; +import { + isForkedChild, + pushTreeContext, + getTreeIndex, +} from './ReactFiberTreeContext.new'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -359,6 +369,7 @@ function updateForwardRef( // The rest is a fork of updateFunctionComponent let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -374,6 +385,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -388,6 +400,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -402,6 +415,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -412,6 +426,13 @@ function updateForwardRef( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (hasId && getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const treeIndex = getTreeIndex(); + pushTreeContext(workInProgress, treeIndex); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -964,6 +985,7 @@ function updateFunctionComponent( } let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -979,6 +1001,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -993,6 +1016,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -1007,6 +1031,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1017,6 +1042,13 @@ function updateFunctionComponent( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (hasId && getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const treeIndex = getTreeIndex(); + pushTreeContext(workInProgress, treeIndex); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -1587,6 +1619,7 @@ function mountIndeterminateComponent( prepareToReadContext(workInProgress, renderLanes); let value; + let hasId; if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -1623,6 +1656,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); setIsRendering(false); } else { value = renderWithHooks( @@ -1633,6 +1667,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1752,11 +1787,20 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } } } + + if (hasId && getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const treeIndex = getTreeIndex(); + pushTreeContext(workInProgress, treeIndex); + } + reconcileChildren(null, workInProgress, value, renderLanes); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); @@ -3675,6 +3719,20 @@ 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 childIndex = workInProgress.index; + pushTreeContext(workInProgress, childIndex); + } } // Before entering the begin phase, clear pending update priority. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index f116897a8661b..6e9c2eedae2c4 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -174,7 +174,11 @@ import { prepareToReadContext, scheduleWorkOnParentPath, } from './ReactFiberNewContext.old'; -import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.old'; +import { + renderWithHooks, + checkDidRenderIdHook, + bailoutHooks, +} from './ReactFiberHooks.old'; import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.old'; import { getMaskedContext, @@ -186,6 +190,7 @@ import { invalidateContextProvider, } from './ReactFiberContext.old'; import { + getIsHydrating, enterHydrationState, reenterHydrationStateFromDehydratedSuspenseInstance, resetHydrationState, @@ -235,6 +240,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.old'; import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old'; import is from 'shared/objectIs'; import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old'; +import { + isForkedChild, + pushTreeContext, + getTreeIndex, +} from './ReactFiberTreeContext.old'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -359,6 +369,7 @@ function updateForwardRef( // The rest is a fork of updateFunctionComponent let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -374,6 +385,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -388,6 +400,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -402,6 +415,7 @@ function updateForwardRef( ref, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -412,6 +426,13 @@ function updateForwardRef( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (hasId && getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const treeIndex = getTreeIndex(); + pushTreeContext(workInProgress, treeIndex); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -964,6 +985,7 @@ function updateFunctionComponent( } let nextChildren; + let hasId; prepareToReadContext(workInProgress, renderLanes); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -979,6 +1001,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode @@ -993,6 +1016,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } @@ -1007,6 +1031,7 @@ function updateFunctionComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1017,6 +1042,13 @@ function updateFunctionComponent( return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } + if (hasId && getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const treeIndex = getTreeIndex(); + pushTreeContext(workInProgress, treeIndex); + } + // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren, renderLanes); @@ -1587,6 +1619,7 @@ function mountIndeterminateComponent( prepareToReadContext(workInProgress, renderLanes); let value; + let hasId; if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); @@ -1623,6 +1656,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); setIsRendering(false); } else { value = renderWithHooks( @@ -1633,6 +1667,7 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } if (enableSchedulingProfiler) { markComponentRenderStopped(); @@ -1752,11 +1787,20 @@ function mountIndeterminateComponent( context, renderLanes, ); + hasId = checkDidRenderIdHook(); } finally { setIsStrictModeForDevtools(false); } } } + + if (hasId && getIsHydrating()) { + // This component materialized an id. This will affect any ids that appear + // in its children. + const treeIndex = getTreeIndex(); + pushTreeContext(workInProgress, treeIndex); + } + reconcileChildren(null, workInProgress, value, renderLanes); if (__DEV__) { validateFunctionComponentInDev(workInProgress, Component); @@ -3675,6 +3719,20 @@ 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 childIndex = workInProgress.index; + pushTreeContext(workInProgress, childIndex); + } } // Before entering the begin phase, clear pending update priority. diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 20a7fc52db13a..feb38f00461a0 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -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: diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 305359aef206e..ea4d71e8ba371 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -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: diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 95707f0627e40..8dc28031e82cb 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -116,6 +116,7 @@ import { } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; +import {getTreeId} from './ReactFiberTreeContext.new'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -202,6 +203,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; @@ -395,6 +402,7 @@ export function renderWithHooks( // 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. @@ -545,6 +553,15 @@ export function renderWithHooks( return children; } +export function checkDidRenderIdHook() { + // This should be called immediately after every renderWithHooks call. + // Conceptually, it's part of the return value of renderWithHooks; it's only a + // separate function to avoid using an array tuple. + const didRenderIdHook = localIdCounter !== 0; + localIdCounter = 0; + return didRenderIdHook; +} + export function bailoutHooks( current: Fiber, workInProgress: Fiber, @@ -611,6 +628,7 @@ export function resetHooksAfterThrow(): void { } didScheduleRenderPhaseUpdateDuringThisPass = false; + localIdCounter = 0; } function mountWorkInProgressHook(): Hook { @@ -2097,11 +2115,36 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void { } function mountId(): string { - throw new Error('Not implemented.'); + 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 { - throw new Error('Not implemented.'); + const hook = updateWorkInProgressHook(); + const id: string = hook.memoizedState; + return id; } function mountRefresh() { diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 4b5bab4f90c6f..975c951022662 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -116,6 +116,7 @@ import { } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; +import {getTreeId} from './ReactFiberTreeContext.old'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -202,6 +203,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; @@ -395,6 +402,7 @@ export function renderWithHooks( // 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. @@ -545,6 +553,15 @@ export function renderWithHooks( return children; } +export function checkDidRenderIdHook() { + // This should be called immediately after every renderWithHooks call. + // Conceptually, it's part of the return value of renderWithHooks; it's only a + // separate function to avoid using an array tuple. + const didRenderIdHook = localIdCounter !== 0; + localIdCounter = 0; + return didRenderIdHook; +} + export function bailoutHooks( current: Fiber, workInProgress: Fiber, @@ -611,6 +628,7 @@ export function resetHooksAfterThrow(): void { } didScheduleRenderPhaseUpdateDuringThisPass = false; + localIdCounter = 0; } function mountWorkInProgressHook(): Hook { @@ -2097,11 +2115,36 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void { } function mountId(): string { - throw new Error('Not implemented.'); + 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 { - throw new Error('Not implemented.'); + const hook = updateWorkInProgressHook(); + const id: string = hook.memoizedState; + return id; } function mountRefresh() { diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index c1f34e1052fc4..7e1461c7a7226 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -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; -} diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index c81191f6a07e5..6b4be15e649f1 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -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; -} diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.new.js b/packages/react-reconciler/src/ReactFiberTreeContext.new.js new file mode 100644 index 0000000000000..34886527bb271 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberTreeContext.new.js @@ -0,0 +1,158 @@ +/** + * 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 + */ + +import {getIsHydrating} from './ReactFiberHydrationContext.new'; +import {clz32} from './clz32'; + +export type TreeIdContext = { + id: number, + overflow: string, + index: number, +}; + +let treeContextId: number = 0; +let treeContextOverflow: string = ''; +let treeContextIndex = 0; + +let treeIdProvider: Fiber | null = null; + +// 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 stack: Array = []; +let stackIndex: number = 0; + +function encodeLargeTreeId(baseId: number, treeIndex: number): string { + // This function is used when the number of bits needed to represent an id + // exceeds the bitwise safe range. + // + // 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 baseIdBitLength = 32 - clz32(baseId); + const numberOfBitsToRemove = baseIdBitLength - (baseIdBitLength % 5); + + // Then create a bitmask that selects only those bits. + const maskOfBitsToRemove = (1 << numberOfBitsToRemove) - 1; + + // Select the bits, and convert them to a base 32 string. + const overflow = (baseId & maskOfBitsToRemove).toString(32); + + // Now encode the rest of the bits using the normal algorithm. + const remainingBaseId = baseId >> numberOfBitsToRemove; + const remainingBaseIdBitLength = baseIdBitLength - numberOfBitsToRemove; + const truncatedId = + ((treeIndex + 1) << remainingBaseIdBitLength) | remainingBaseId; + + // Finally, combine all the parts together. + return truncatedId.toString(32) + overflow; +} + +export function getTreeId(): string { + warnIfNotHydrating(); + const baseId = treeContextId; + const baseOverflow = treeContextOverflow; + const baseTreeIndex = treeContextIndex; + + const baseIdBitLength = 32 - clz32(baseId); + const newTreeId = ((baseTreeIndex + 1) << baseIdBitLength) | baseId; + if (newTreeId < 0) { + // We overflowed the bitwise-safe range. Fall back to slower algorithm. + return encodeLargeTreeId(baseId, baseTreeIndex) + baseOverflow; + } else { + // Normal path. + return newTreeId.toString(32) + baseOverflow; + } +} + +export function getTreeIndex(): number { + warnIfNotHydrating(); + return treeContextIndex; +} + +export function isForkedChild(workInProgress: Fiber): boolean { + warnIfNotHydrating(); + // TODO: Temporary hack. Set a fiber flag instead. + const siblingFiber = workInProgress.sibling; + if (siblingFiber !== null) { + return true; + } + const returnFiber = workInProgress.return; + if (returnFiber !== null) { + return returnFiber.child !== workInProgress; + } + return false; +} + +export function pushTreeContext(workInProgress: Fiber, treeIndex: number) { + warnIfNotHydrating(); + + stack[stackIndex++] = treeContextId; + stack[stackIndex++] = treeContextOverflow; + stack[stackIndex++] = treeContextIndex; + stack[stackIndex++] = treeIdProvider; + + treeIdProvider = workInProgress; + + const baseId = treeContextId; + const baseOverflow = treeContextOverflow; + const baseTreeIndex = treeContextIndex; + + const baseIdBitLength = 32 - clz32(baseId); + const newTreeId = ((baseTreeIndex + 1) << baseIdBitLength) | baseId; + if (newTreeId < 0) { + // We overflowed the bitwise-safe range. To make more room, materialize + // the current id context and prepend it to the overflow variable. + treeContextOverflow = + encodeLargeTreeId(baseId, baseTreeIndex) + baseOverflow; + // Reset the id. + treeContextId = 0; + } else { + // Normal path. + treeContextId = newTreeId; + } + + treeContextIndex = treeIndex; +} + +export function popTreeContext(workInProgress: Fiber) { + // This is a loop because the same fiber could appear on the provider stack + // twice in a row. This happens when a component that includes a useId hook + // also forks into multiple children, because both scenarios affect the ids + // of its subtree. + while (workInProgress === treeIdProvider) { + // Restore the previous values. + treeIdProvider = stack[--stackIndex]; + stack[stackIndex] = null; + treeContextIndex = stack[--stackIndex]; + stack[stackIndex] = null; + treeContextOverflow = stack[--stackIndex]; + stack[stackIndex] = null; + treeContextId = stack[--stackIndex]; + stack[stackIndex] = null; + } +} + +function warnIfNotHydrating() { + if (__DEV__) { + if (!getIsHydrating()) { + console.error( + 'Expected to be hydrating. This is a bug in React. Please file ' + + 'an issue.', + ); + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index ba8bd4b573957..bb002b9e71b3a 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -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; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js index ad8e479700db0..7f161513a4afa 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js @@ -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; diff --git a/packages/react-reconciler/src/clz32.js b/packages/react-reconciler/src/clz32.js new file mode 100644 index 0000000000000..80a9cfb911482 --- /dev/null +++ b/packages/react-reconciler/src/clz32.js @@ -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; +} diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 00f585d4e371a..72bd0e96116c5 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -19,6 +19,7 @@ import type { import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig'; import {readContext as readContextImpl} from './ReactFizzNewContext'; +import {getTreeId} from './ReactFizzTreeContext'; import {makeServerID} from './ReactServerFormatConfig'; @@ -51,6 +52,8 @@ let workInProgressHook: Hook | null = null; 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, Update> | null = null; // Counter to prevent infinite loops. @@ -171,6 +174,7 @@ export function prepareToUseHooks(componentIdentity: Object): void { // The following should have already been reset // didScheduleRenderPhaseUpdate = false; + // localIdCounter = 0; // firstWorkInProgressHook = null; // numberOfReRenders = 0; // renderPhaseUpdates = null; @@ -203,6 +207,15 @@ 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; + localIdCounter = 0; + return didRenderIdHook; +} + // Reset the internal hooks state if an error occurs while rendering a component export function resetHooksState(): void { if (__DEV__) { @@ -211,6 +224,7 @@ export function resetHooksState(): void { currentlyRenderingComponent = null; didScheduleRenderPhaseUpdate = false; + localIdCounter = 0; firstWorkInProgressHook = null; numberOfReRenders = 0; renderPhaseUpdates = null; @@ -496,7 +510,20 @@ function useOpaqueIdentifier(): OpaqueIDType { } function useId(): string { - throw new Error('Not implemented.'); + const treeId = getTreeId(); + + // 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() { diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 6692b92648643..2c7436e4ec092 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -78,12 +78,18 @@ import { import { prepareToUseHooks, finishHooks, + checkDidRenderIdHook, resetHooksState, Dispatcher, currentResponseState, setCurrentResponseState, } from './ReactFizzHooks'; import {getStackByComponentStackNode} from './ReactFizzComponentStack'; +import { + pushTreeContext, + popTreeContext, + getTreeIndex, +} from './ReactFizzTreeContext'; import { getIteratorFn, @@ -671,6 +677,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 +749,19 @@ 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. This will affect any ids that appear + // in its children. + const index = getTreeIndex(); + const prevTreeIdContext = pushTreeContext(index); + try { + renderNodeDestructive(request, task, value); + } finally { + popTreeContext(prevTreeIdContext); + } + } else { + renderNodeDestructive(request, task, value); + } } popComponentStackInDEV(task); } @@ -827,7 +846,20 @@ 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. This will affect any ids that appear + // in its children. + const index = getTreeIndex(); + const prevTreeIdContext = pushTreeContext(index); + try { + renderNodeDestructive(request, task, children); + } finally { + popTreeContext(prevTreeIdContext); + } + } else { + renderNodeDestructive(request, task, children); + } popComponentStackInDEV(task); } @@ -1087,7 +1119,7 @@ function renderNodeDestructive( request: Request, task: Task, node: ReactNodeList, -): void { +): boolean { // Stash the node we're working on. We'll pick up from this task in case // something suspends. task.node = node; @@ -1101,7 +1133,7 @@ function renderNodeDestructive( const props = element.props; const ref = element.ref; renderElement(request, task, type, props, ref); - return; + return true; } case REACT_PORTAL_TYPE: throw new Error( @@ -1116,19 +1148,31 @@ function renderNodeDestructive( const init = lazyNode._init; const resolvedNode = init(payload); renderNodeDestructive(request, task, resolvedNode); - return; + return true; } } } if (isArray(node)) { + let childIndex = 0; 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]); + // 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. + const prevTreeIdContext = pushTreeContext(childIndex); + try { + const isNotEmpty = renderNode(request, task, node[i]); + if (isNotEmpty) { + // Disregard empty children when assigning ids. In the Fiber + // implementation, this corresponds to nodes for which no fiber + // is allocated. + childIndex++; + } + } finally { + popTreeContext(prevTreeIdContext); + } } - return; + return true; } const iteratorFn = getIteratorFn(node); @@ -1141,14 +1185,23 @@ function renderNodeDestructive( let step = iterator.next(); // If there are not entries, we need to push an empty so we start by checking that. if (!step.done) { + let childIndex = 0; 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); + const prevTreeIdContext = pushTreeContext(childIndex); + try { + const isNotEmpty = renderNode(request, task, step.value); + if (!isNotEmpty) { + // Disregard empty children when assigning ids. In the Fiber + // implementation, this corresponds to nodes for which no fiber + // is allocated. + childIndex++; + } + } finally { + popTreeContext(prevTreeIdContext); + } step = iterator.next(); } while (!step.done); - return; + return true; } } } @@ -1168,7 +1221,7 @@ function renderNodeDestructive( if (typeof node === 'string') { pushTextInstance(task.blockedSegment.chunks, node, request.responseState); - return; + return true; } if (typeof node === 'number') { @@ -1177,7 +1230,7 @@ function renderNodeDestructive( '' + node, request.responseState, ); - return; + return true; } if (__DEV__) { @@ -1189,6 +1242,11 @@ function renderNodeDestructive( ); } } + + // A return value of `value` indicates that this node is empty. This is + // important for the `useId` implementation — it must match the cases in Fiber + // where no child fiber is created. + return false; } function spawnNewSuspendedTask( @@ -1228,7 +1286,11 @@ function spawnNewSuspendedTask( // This is a non-destructive form of rendering a node. If it suspends it spawns // a new task and restores the context of this task to what it was before. -function renderNode(request: Request, task: Task, node: ReactNodeList): void { +function renderNode( + request: Request, + task: Task, + node: ReactNodeList, +): boolean { // TODO: Store segment.children.length here and reset it in case something // suspended partially through writing something. @@ -1257,6 +1319,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void { if (__DEV__) { task.componentStack = previousComponentStack; } + return true; } 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. diff --git a/packages/react-server/src/ReactFizzTreeContext.js b/packages/react-server/src/ReactFizzTreeContext.js new file mode 100644 index 0000000000000..1079d6265cb26 --- /dev/null +++ b/packages/react-server/src/ReactFizzTreeContext.js @@ -0,0 +1,129 @@ +/** + * 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 + */ + +export type TreeContext = { + id: number, + overflow: string, + index: number, +}; + +let treeContextId: number = 0; +let treeContextOverflow: string = ''; +let treeContextIndex = 0; + +function encodeLargeTreeId(baseId: number, treeIndex: number): string { + // This function is used when the number of bits needed to represent an id + // exceeds the bitwise safe range. + // + // 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 baseIdBitLength = 32 - clz32(baseId); + const numberOfBitsToRemove = baseIdBitLength - (baseIdBitLength % 5); + + // Then create a bitmask that selects only those bits. + const maskOfBitsToRemove = (1 << numberOfBitsToRemove) - 1; + + // Select the bits, and convert them to a base 32 string. + const overflow = (baseId & maskOfBitsToRemove).toString(32); + + // Now encode the rest of the bits using the normal algorithm. + const remainingBaseId = baseId >> numberOfBitsToRemove; + const remainingBaseIdBitLength = baseIdBitLength - numberOfBitsToRemove; + const truncatedId = + ((treeIndex + 1) << remainingBaseIdBitLength) | remainingBaseId; + + // Finally, combine all the parts together. + return truncatedId.toString(32) + overflow; +} + +export function getTreeId(): string { + const baseId = treeContextId; + const baseOverflow = treeContextOverflow; + const baseTreeIndex = treeContextIndex; + + const baseIdBitLength = 32 - clz32(baseId); + const newTreeId = ((baseTreeIndex + 1) << baseIdBitLength) | baseId; + if (newTreeId < 0) { + // We overflowed the bitwise-safe range. Fall back to slower algorithm. + return encodeLargeTreeId(baseId, baseTreeIndex) + baseOverflow; + } else { + // Normal path. + return newTreeId.toString(32) + baseOverflow; + } +} + +export function getTreeIndex(): number { + return treeContextIndex; +} + +export function pushTreeContext(treeIndex: number): TreeContext { + const prevTreeIdContext = { + id: treeContextId, + overflow: treeContextOverflow, + index: treeContextIndex, + }; + + const baseId = treeContextId; + const baseOverflow = treeContextOverflow; + const baseTreeIndex = treeContextIndex; + + const baseIdBitLength = 32 - clz32(baseId); + const newTreeId = ((baseTreeIndex + 1) << baseIdBitLength) | baseId; + if (newTreeId < 0) { + // We overflowed the bitwise-safe range. To make more room, materialize + // the current id context and prepend it to the overflow variable. + treeContextOverflow = + encodeLargeTreeId(baseId, baseTreeIndex) + baseOverflow; + // Reset the id. + treeContextId = 0; + } else { + // Normal path. + treeContextId = newTreeId; + } + + treeContextIndex = treeIndex; + + return prevTreeIdContext; +} + +export function popTreeContext(prevTreeIdContext: TreeContext | null) { + // Restore the previous values. + if (prevTreeIdContext !== null) { + treeContextId = prevTreeIdContext.id; + treeContextOverflow = prevTreeIdContext.overflow; + treeContextIndex = prevTreeIdContext.index; + } else { + treeContextId = 0; + treeContextOverflow = ''; + treeContextIndex = 0; + } +} + +// 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; +}