mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
a53da6abe1
This Hook will be used to drive a View Transition based on a gesture. ```js const [value, startGesture] = useSwipeTransition(prev, current, next); ``` The `enableSwipeTransition` flag will depend on `enableViewTransition` flag but we may decide to ship them independently. This PR doesn't do anything interesting yet. There will be a lot more PRs to build out the actual functionality. This is just wiring up the plumbing for the new Hook. This first PR is mainly concerned with how the whole starts (and stops). The core API is the `startGesture` function (although there will be other conveniences added in the future). You can call this to start a gesture with a source provider. You can call this multiple times in one event to batch multiple Hooks listening to the same provider. However, each render can only handle one source provider at a time and so it does one render per scheduled gesture provider. This uses a separate `GestureLane` to drive gesture renders by marking the Hook as having an update on that lane. Then schedule a render. These renders should be blocking and in the same microtask as the `startGesture` to ensure it can block the paint. So it's similar to sync. It may not be possible to finish it synchronously e.g. if something suspends. If so, it just tries again later when it can like any other render. This can also happen because it also may not be possible to drive more than one gesture at a time like if we're limited to one View Transition per document. So right now you can only run one gesture at a time in practice. These renders never commit. This means that we can't clear the `GestureLane` the normal way. Instead, we have to clear only the root's `pendingLanes` if we don't have any new renders scheduled. Then wait until something else updates the Fiber after all gestures on it have stopped before it really clears.
169 lines
5.2 KiB
JavaScript
169 lines
5.2 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
|
|
import type {Request} from './ReactFlightServer';
|
|
import type {Thenable, Usable, ReactComponentInfo} from 'shared/ReactTypes';
|
|
import type {ThenableState} from './ReactFlightThenable';
|
|
import {
|
|
REACT_MEMO_CACHE_SENTINEL,
|
|
REACT_CONTEXT_TYPE,
|
|
} from 'shared/ReactSymbols';
|
|
import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
|
|
import {isClientReference} from './ReactFlightServerConfig';
|
|
import {
|
|
enableUseEffectEventHook,
|
|
enableSwipeTransition,
|
|
} from 'shared/ReactFeatureFlags';
|
|
|
|
let currentRequest = null;
|
|
let thenableIndexCounter = 0;
|
|
let thenableState = null;
|
|
let currentComponentDebugInfo = null;
|
|
|
|
export function prepareToUseHooksForRequest(request: Request) {
|
|
currentRequest = request;
|
|
}
|
|
|
|
export function resetHooksForRequest() {
|
|
currentRequest = null;
|
|
}
|
|
|
|
export function prepareToUseHooksForComponent(
|
|
prevThenableState: ThenableState | null,
|
|
componentDebugInfo: null | ReactComponentInfo,
|
|
) {
|
|
thenableIndexCounter = 0;
|
|
thenableState = prevThenableState;
|
|
if (__DEV__) {
|
|
currentComponentDebugInfo = componentDebugInfo;
|
|
}
|
|
}
|
|
|
|
export function getThenableStateAfterSuspending(): ThenableState {
|
|
// If you use() to Suspend this should always exist but if you throw a Promise instead,
|
|
// which is not really supported anymore, it will be empty. We use the empty set as a
|
|
// marker to know if this was a replay of the same component or first attempt.
|
|
const state = thenableState || createThenableState();
|
|
if (__DEV__) {
|
|
// This is a hack but we stash the debug info here so that we don't need a completely
|
|
// different data structure just for this in DEV. Not too happy about it.
|
|
(state: any)._componentDebugInfo = currentComponentDebugInfo;
|
|
currentComponentDebugInfo = null;
|
|
}
|
|
thenableState = null;
|
|
return state;
|
|
}
|
|
|
|
export const HooksDispatcher: Dispatcher = {
|
|
readContext: (unsupportedContext: any),
|
|
|
|
use,
|
|
useCallback<T>(callback: T): T {
|
|
return callback;
|
|
},
|
|
useContext: (unsupportedContext: any),
|
|
useEffect: (unsupportedHook: any),
|
|
useImperativeHandle: (unsupportedHook: any),
|
|
useLayoutEffect: (unsupportedHook: any),
|
|
useInsertionEffect: (unsupportedHook: any),
|
|
useMemo<T>(nextCreate: () => T): T {
|
|
return nextCreate();
|
|
},
|
|
useReducer: (unsupportedHook: any),
|
|
useRef: (unsupportedHook: any),
|
|
useState: (unsupportedHook: any),
|
|
useDebugValue(): void {},
|
|
useDeferredValue: (unsupportedHook: any),
|
|
useTransition: (unsupportedHook: any),
|
|
useSyncExternalStore: (unsupportedHook: any),
|
|
useId,
|
|
useHostTransitionStatus: (unsupportedHook: any),
|
|
useFormState: (unsupportedHook: any),
|
|
useActionState: (unsupportedHook: any),
|
|
useOptimistic: (unsupportedHook: any),
|
|
useMemoCache(size: number): Array<any> {
|
|
const data = new Array<any>(size);
|
|
for (let i = 0; i < size; i++) {
|
|
data[i] = REACT_MEMO_CACHE_SENTINEL;
|
|
}
|
|
return data;
|
|
},
|
|
useCacheRefresh(): <T>(?() => T, ?T) => void {
|
|
return unsupportedRefresh;
|
|
},
|
|
};
|
|
if (enableUseEffectEventHook) {
|
|
HooksDispatcher.useEffectEvent = (unsupportedHook: any);
|
|
}
|
|
if (enableSwipeTransition) {
|
|
HooksDispatcher.useSwipeTransition = (unsupportedHook: any);
|
|
}
|
|
|
|
function unsupportedHook(): void {
|
|
throw new Error('This Hook is not supported in Server Components.');
|
|
}
|
|
|
|
function unsupportedRefresh(): void {
|
|
throw new Error(
|
|
'Refreshing the cache is not supported in Server Components.',
|
|
);
|
|
}
|
|
|
|
function unsupportedContext(): void {
|
|
throw new Error('Cannot read a Client Context from a Server Component.');
|
|
}
|
|
|
|
function useId(): string {
|
|
if (currentRequest === null) {
|
|
throw new Error('useId can only be used while React is rendering');
|
|
}
|
|
const id = currentRequest.identifierCount++;
|
|
// use 'S' for Flight components to distinguish from 'R' and 'r' in Fizz/Client
|
|
return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':';
|
|
}
|
|
|
|
function use<T>(usable: Usable<T>): T {
|
|
if (
|
|
(usable !== null && typeof usable === 'object') ||
|
|
typeof usable === 'function'
|
|
) {
|
|
// $FlowFixMe[method-unbinding]
|
|
if (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;
|
|
|
|
if (thenableState === null) {
|
|
thenableState = createThenableState();
|
|
}
|
|
return trackUsedThenable(thenableState, thenable, index);
|
|
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
|
|
unsupportedContext();
|
|
}
|
|
}
|
|
|
|
if (isClientReference(usable)) {
|
|
if (usable.value != null && usable.value.$$typeof === REACT_CONTEXT_TYPE) {
|
|
// Show a more specific message since it's a common mistake.
|
|
throw new Error('Cannot read a Client Context from a Server Component.');
|
|
} else {
|
|
throw new Error('Cannot use() an already resolved Client Reference.');
|
|
}
|
|
} else {
|
|
throw new Error(
|
|
// eslint-disable-next-line react-internal/safe-string-coercion
|
|
'An unsupported type was passed to use(): ' + String(usable),
|
|
);
|
|
}
|
|
}
|