Skip to content

Commit

Permalink
Initial useId implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
acdlite committed Oct 28, 2021
1 parent 8d25434 commit 9e2d8dd
Show file tree
Hide file tree
Showing 16 changed files with 799 additions and 55 deletions.
134 changes: 134 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMUseId-test.js
Original file line number Diff line number Diff line change
@@ -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(
'<!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 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 (
<div ref={ref} id={id}>
{children}
</div>
);
}

test('basic usage', async () => {
function App() {
return (
<div>
<div>
<DivWithId label="A" />
<DivWithId label="B" />
</div>
<DivWithId label="C" />
</div>
);
}

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
pipe(writable);
});

await clientAct(async () => {
ReactDOM.hydrateRoot(container, <App />);
});
});
});
60 changes: 59 additions & 1 deletion packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -186,6 +190,7 @@ import {
invalidateContextProvider,
} from './ReactFiberContext.new';
import {
getIsHydrating,
enterHydrationState,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -359,6 +369,7 @@ function updateForwardRef(

// The rest is a fork of updateFunctionComponent
let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
Expand All @@ -374,6 +385,7 @@ function updateForwardRef(
ref,
renderLanes,
);
hasId = checkDidRenderIdHook();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
Expand All @@ -388,6 +400,7 @@ function updateForwardRef(
ref,
renderLanes,
);
hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
Expand All @@ -402,6 +415,7 @@ function updateForwardRef(
ref,
renderLanes,
);
hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
Expand All @@ -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);
Expand Down Expand Up @@ -964,6 +985,7 @@ function updateFunctionComponent(
}

let nextChildren;
let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
Expand All @@ -979,6 +1001,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
Expand All @@ -993,6 +1016,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
Expand All @@ -1007,6 +1031,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
Expand All @@ -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);
Expand Down Expand Up @@ -1587,6 +1619,7 @@ function mountIndeterminateComponent(

prepareToReadContext(workInProgress, renderLanes);
let value;
let hasId;

if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
Expand Down Expand Up @@ -1623,6 +1656,7 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
setIsRendering(false);
} else {
value = renderWithHooks(
Expand All @@ -1633,6 +1667,7 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 9e2d8dd

Please sign in to comment.