Skip to content

Commit

Permalink
Simplify Continuous Hydration Targets
Browse files Browse the repository at this point in the history
Let's use a constant priority for this. This helps us avoid restarting
a render when switching targets and simplifies the model.

The downside is that now we're not down-prioritizing the previous hover
target. However, we think that's ok because it'll only do one level too
much and then stop.
  • Loading branch information
sebmarkbage committed Feb 1, 2020
1 parent 38cd758 commit 5910493
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
document.body.removeChild(container);
});

it('hydrates the last target as higher priority for continuous events', async () => {
it('hydrates the hovered targets as higher priority for continuous events', async () => {
let suspend = false;
let resolve;
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
Expand Down Expand Up @@ -669,21 +669,107 @@ describe('ReactDOMServerSelectiveHydration', () => {

// We should prioritize hydrating D first because we clicked it.
// Next we should hydrate C since that's the current hover target.
// Next it doesn't matter if we hydrate A or B first but as an
// implementation detail we're currently hydrating B first since
// we at one point hovered over it and we never deprioritized it.
// To simplify implementation details we hydrate both B and C at
// the same time since B was already scheduled.
// This is ok because it will at least not continue for nested
// boundary. See the next test below.
expect(Scheduler).toFlushAndYield([
'D',
'Clicked D',
'B', // Ideally this should be later.
'C',
'Hover C',
'B',
'A',
]);

document.body.removeChild(container);
});

it('hydrates the last target path first for continuous events', async () => {
let suspend = false;
let resolve;
let promise = new Promise(resolvePromise => (resolve = resolvePromise));

function Child({text}) {
if ((text === 'A' || text === 'D') && suspend) {
throw promise;
}
Scheduler.unstable_yieldValue(text);
return (
<span
onMouseEnter={e => {
e.preventDefault();
Scheduler.unstable_yieldValue('Hover ' + text);
}}>
{text}
</span>
);
}

function App() {
Scheduler.unstable_yieldValue('App');
return (
<div>
<Suspense fallback="Loading...">
<Child text="A" />
</Suspense>
<Suspense fallback="Loading...">
<div>
<Suspense fallback="Loading...">
<Child text="B" />
</Suspense>
</div>
<Child text="C" />
</Suspense>
<Suspense fallback="Loading...">
<Child text="D" />
</Suspense>
</div>
);
}

let finalHTML = ReactDOMServer.renderToString(<App />);

expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']);

let container = document.createElement('div');
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);

container.innerHTML = finalHTML;

let spanB = container.getElementsByTagName('span')[1];
let spanC = container.getElementsByTagName('span')[2];
let spanD = container.getElementsByTagName('span')[3];

suspend = true;

// A and D will be suspended. We'll click on D which should take
// priority, after we unsuspend.
let root = ReactDOM.createRoot(container, {hydrate: true});
root.render(<App />);

// Nothing has been hydrated so far.
expect(Scheduler).toHaveYielded([]);

// Hover over B and then C.
dispatchMouseHoverEvent(spanB, spanD);
dispatchMouseHoverEvent(spanC, spanB);

suspend = false;
resolve();
await promise;

// We should prioritize hydrating D first because we clicked it.
// Next we should hydrate C since that's the current hover target.
// Next it doesn't matter if we hydrate A or B first but as an
// implementation detail we're currently hydrating B first since
// we at one point hovered over it and we never deprioritized it.
expect(Scheduler).toFlushAndYield(['App', 'C', 'Hover C', 'A', 'B', 'D']);

document.body.removeChild(container);
});

it('hydrates the last explicitly hydrated target at higher priority', async () => {
function Child({text}) {
Scheduler.unstable_yieldValue(text);
Expand Down
16 changes: 3 additions & 13 deletions packages/react-reconciler/src/ReactFiberExpirationTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ export const Never = 1;
// Idle is slightly higher priority than Never. It must completely finish in
// order to be consistent.
export const Idle = 2;
// Continuous Hydration is a moving priority. It is slightly higher than Idle
// and is used to increase priority of hover targets. It is increasing with
// each usage so that last always wins.
let ContinuousHydration = 3;
// Continuous Hydration is slightly higher than Idle and is used to increase
// priority of hover targets.
export const ContinuousHydration = 3;
export const Sync = MAX_SIGNED_31_BIT_INT;
export const Batched = Sync - 1;

Expand Down Expand Up @@ -119,15 +118,6 @@ export function computeInteractiveExpiration(currentTime: ExpirationTime) {
);
}

export function computeContinuousHydrationExpiration(
currentTime: ExpirationTime,
) {
// Each time we ask for a new one of these we increase the priority.
// This ensures that the last one always wins since we can't deprioritize
// once we've scheduled work already.
return ContinuousHydration++;
}

export function inferPriorityFromExpirationTime(
currentTime: ExpirationTime,
expirationTime: ExpirationTime,
Expand Down
9 changes: 3 additions & 6 deletions packages/react-reconciler/src/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ import {
import {StrictMode} from './ReactTypeOfMode';
import {
Sync,
ContinuousHydration,
computeInteractiveExpiration,
computeContinuousHydrationExpiration,
} from './ReactFiberExpirationTime';
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
import {
Expand Down Expand Up @@ -384,11 +384,8 @@ export function attemptContinuousHydration(fiber: Fiber): void {
// Suspense.
return;
}
let expTime = computeContinuousHydrationExpiration(
requestCurrentTimeForUpdate(),
);
scheduleWork(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
scheduleWork(fiber, ContinuousHydration);
markRetryTimeIfNotHydrated(fiber, ContinuousHydration);
}

export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
Expand Down

0 comments on commit 5910493

Please sign in to comment.