Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experimental_use(promise) #25084

Merged
merged 5 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions packages/jest-react/src/internalAct.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import enqueueTask from 'shared/enqueueTask';
let actingUpdatesScopeDepth = 0;

export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
if (Scheduler.unstable_flushAllWithoutAsserting === undefined) {
if (Scheduler.unstable_flushUntilNextPaint === undefined) {
throw Error(
'This version of `act` requires a special mock build of Scheduler.',
);
Expand Down Expand Up @@ -120,19 +120,31 @@ export function act<T>(scope: () => Thenable<T> | T): Thenable<T> {
}

function flushActWork(resolve, reject) {
// Flush suspended fallbacks
// $FlowFixMe: Flow doesn't know about global Jest object
jest.runOnlyPendingTimers();
enqueueTask(() => {
if (Scheduler.unstable_hasPendingWork()) {
try {
const didFlushWork = Scheduler.unstable_flushAllWithoutAsserting();
if (didFlushWork) {
flushActWork(resolve, reject);
} else {
resolve();
}
Scheduler.unstable_flushUntilNextPaint();
} catch (error) {
reject(error);
}
});

// If Scheduler yields while there's still work, it's so that we can
// unblock the main thread (e.g. for paint or for microtasks). Yield to
// the main thread and continue in a new task.
enqueueTask(() => flushActWork(resolve, reject));
return;
}

// Once the scheduler queue is empty, run all the timers. The purpose of this
// is to force any pending fallbacks to commit. The public version of act does
// this with dev-only React runtime logic, but since our internal act needs to
// work work production builds of React, we have to cheat.
// $FlowFixMe: Flow doesn't know about global Jest object
jest.runOnlyPendingTimers();
if (Scheduler.unstable_hasPendingWork()) {
// Committing a fallback scheduled additional work. Continue flushing.
flushActWork(resolve, reject);
return;
}

resolve();
}
129 changes: 129 additions & 0 deletions packages/react-reconciler/src/ReactFiberHooks.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type {
MutableSourceSubscribeFn,
ReactContext,
StartTransitionOptions,
Usable,
Thenable,
} from 'shared/ReactTypes';
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.new';
Expand All @@ -32,6 +34,7 @@ import {
enableLazyContextPropagation,
enableUseMutableSource,
enableTransitionTracing,
enableUseHook,
enableUseMemoCacheHook,
} from 'shared/ReactFeatureFlags';

Expand Down Expand Up @@ -120,6 +123,10 @@ import {
} from './ReactFiberConcurrentUpdates.new';
import {getTreeId} from './ReactFiberTreeContext.new';
import {now} from './Scheduler';
import {
trackUsedThenable,
getPreviouslyUsedThenableAtIndex,
} from './ReactFiberWakeable.new';

const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;

Expand Down Expand Up @@ -205,6 +212,9 @@ let didScheduleRenderPhaseUpdate: boolean = false;
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
// Counts the number of useId hooks in this component.
let localIdCounter: number = 0;
// Counts number of `use`-d thenables
let thenableIndexCounter: 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.
Expand Down Expand Up @@ -403,6 +413,7 @@ export function renderWithHooks<Props, SecondArg>(

// didScheduleRenderPhaseUpdate = false;
// localIdCounter = 0;
// thenableIndexCounter = 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.
Expand Down Expand Up @@ -441,6 +452,7 @@ export function renderWithHooks<Props, SecondArg>(
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
thenableIndexCounter = 0;

if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
Expand Down Expand Up @@ -524,6 +536,7 @@ export function renderWithHooks<Props, SecondArg>(
didScheduleRenderPhaseUpdate = false;
// This is reset by checkDidRenderIdHook
// localIdCounter = 0;
thenableIndexCounter = 0;

if (didRenderTooFewHooks) {
throw new Error(
Expand Down Expand Up @@ -631,6 +644,7 @@ export function resetHooksAfterThrow(): void {

didScheduleRenderPhaseUpdateDuringThisPass = false;
localIdCounter = 0;
thenableIndexCounter = 0;
}

function mountWorkInProgressHook(): Hook {
Expand Down Expand Up @@ -722,6 +736,73 @@ function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
};
}

function use<T>(usable: Usable<T>): T {
if (
usable !== null &&
typeof usable === 'object' &&
typeof usable.then === 'function'
) {
// This is a thenable.
const thenable: Thenable<T> = (usable: any);

// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;

switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError = thenable.reason;
throw rejectedError;
}
default: {
const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
index,
);
if (prevThenableAtIndex !== null) {
switch (prevThenableAtIndex.status) {
case 'fulfilled': {
const fulfilledValue: T = prevThenableAtIndex.value;
return fulfilledValue;
}
case 'rejected': {
const rejectedError: mixed = prevThenableAtIndex.reason;
throw rejectedError;
}
default: {
// The thenable still hasn't resolved. Suspend with the same
// thenable as last time to avoid redundant listeners.
throw prevThenableAtIndex;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it should be an error case - e.g. you used something mutable conditionally.

We shouldn't bother retrying immediately unless lastUsedThenable was pinged. There's no point in retrying due to anything else because that can no longer be blocking. Anything preceding lastUsedThenable must have already been resolved or we wouldn't have gotten that far.

If something else pings maybe we need to reset the suspended thenables anyway - e.g. if we want to treat it as a restart.

So if we have anything in the slot from before, it must've been resolved. Otherwise something went wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that seems reasonable. I started a list of a warning cases but maybe this one should be an actual error

Copy link
Collaborator Author

@acdlite acdlite Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't bother retrying immediately unless lastUsedThenable was pinged. There's no point in retrying due to anything else because that can no longer be blocking. Anything preceding lastUsedThenable must have already been resolved or we wouldn't have gotten that far.

Yeah that is how it currently works. It's a different variable though because it also works for ad hoc wakeables (ones that aren't use-d), but the same logic you're describing:

if (wakeable === suspendedWakeable) {
// This ping is from the wakeable that just suspended. Mark it as pinged.
// When the work loop resumes, we'll immediately try rendering the fiber
// again instead of unwinding the stack.
wasPinged = true;
return true;
}

}
}
} else {
// This is the first time something has been used at this index.
// Stash the thenable at the current index so we can reuse it during
// the next attempt.
trackUsedThenable(thenable, index);

// Suspend.
// TODO: Throwing here is an implementation detail that allows us to
// unwind the call stack. But we shouldn't allow it to leak into
// userspace. Throw an opaque placeholder value instead of the
// actual thenable. If it doesn't get captured by the work loop, log
// a warning, because that means something in userspace must have
// caught it.
throw thenable;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add ping listeners directly here and instead throw a placeholder value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can do that

}
}
}
}

// TODO: Add support for Context

// eslint-disable-next-line react-internal/safe-string-coercion
throw new Error('An unsupported type was passed to use(): ' + String(usable));
}

function useMemoCache(size: number): Array<any> {
throw new Error('Not implemented.');
}
Expand Down Expand Up @@ -2421,6 +2502,9 @@ if (enableCache) {
(ContextOnlyDispatcher: Dispatcher).getCacheForType = getCacheForType;
(ContextOnlyDispatcher: Dispatcher).useCacheRefresh = throwInvalidHookError;
}
if (enableUseHook) {
(ContextOnlyDispatcher: Dispatcher).use = throwInvalidHookError;
}
if (enableUseMemoCacheHook) {
(ContextOnlyDispatcher: Dispatcher).useMemoCache = throwInvalidHookError;
}
Expand Down Expand Up @@ -2452,6 +2536,9 @@ if (enableCache) {
(HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}
if (enableUseHook) {
(HooksDispatcherOnMount: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMount: Dispatcher).useMemoCache = useMemoCache;
}
Expand Down Expand Up @@ -2485,6 +2572,9 @@ if (enableCache) {
if (enableUseMemoCacheHook) {
(HooksDispatcherOnUpdate: Dispatcher).useMemoCache = useMemoCache;
}
if (enableUseHook) {
(HooksDispatcherOnUpdate: Dispatcher).use = use;
}

const HooksDispatcherOnRerender: Dispatcher = {
readContext,
Expand Down Expand Up @@ -2513,6 +2603,9 @@ if (enableCache) {
(HooksDispatcherOnRerender: Dispatcher).getCacheForType = getCacheForType;
(HooksDispatcherOnRerender: Dispatcher).useCacheRefresh = updateRefresh;
}
if (enableUseHook) {
(HooksDispatcherOnRerender: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnRerender: Dispatcher).useMemoCache = useMemoCache;
}
Expand Down Expand Up @@ -2691,6 +2784,9 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnMountInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = useMemoCache;
}
Expand Down Expand Up @@ -2836,6 +2932,9 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useMemoCache = useMemoCache;
}
Expand Down Expand Up @@ -2981,6 +3080,9 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = useMemoCache;
}
Expand Down Expand Up @@ -3127,6 +3229,9 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).use = use;
}
if (enableUseMemoCacheHook) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = useMemoCache;
}
Expand Down Expand Up @@ -3289,6 +3394,14 @@ if (__DEV__) {
return mountRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useMemoCache = function(
size: number,
Expand Down Expand Up @@ -3456,6 +3569,14 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useMemoCache = function(
size: number,
Expand Down Expand Up @@ -3624,6 +3745,14 @@ if (__DEV__) {
return updateRefresh();
};
}
if (enableUseHook) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).use = function<T>(
usable: Usable<T>,
): T {
warnInvalidHookAccess();
return use(usable);
};
}
if (enableUseMemoCacheHook) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useMemoCache = function(
size: number,
Expand Down
Loading