Merge 2764403f05 into sapling-pr-archive-mofeiZ

This commit is contained in:
mofeiZ
2024-12-16 15:10:48 -05:00
committed by GitHub
22 changed files with 308 additions and 563 deletions
@@ -0,0 +1,129 @@
## Input
```javascript
import {makeArray, mutate} from 'shared-runtime';
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component({foo, bar}: {foo: number; bar: number}) {
let x = {foo};
let y: {bar: number; x?: {foo: number}} = {bar};
const f0 = function () {
let a = makeArray(y); // a = [y]
let b = x;
// this writes y.x = x
a[0].x = b;
};
f0();
mutate(y.x);
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 3, bar: 4}],
sequentialRenders: [
{foo: 3, bar: 4},
{foo: 3, bar: 5},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { makeArray, mutate } from "shared-runtime";
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component(t0) {
const $ = _c(5);
const { foo, bar } = t0;
let t1;
if ($[0] !== foo) {
t1 = { foo };
$[0] = foo;
$[1] = t1;
} else {
t1 = $[1];
}
const x = t1;
let y;
if ($[2] !== bar || $[3] !== x) {
y = { bar };
const f0 = function () {
const a = makeArray(y);
const b = x;
a[0].x = b;
};
f0();
mutate(y.x);
$[2] = bar;
$[3] = x;
$[4] = y;
} else {
y = $[4];
}
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ foo: 3, bar: 4 }],
sequentialRenders: [
{ foo: 3, bar: 4 },
{ foo: 3, bar: 5 },
],
};
```
@@ -0,0 +1,48 @@
import {makeArray, mutate} from 'shared-runtime';
/**
* Bug repro:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe"}}
* Forget:
* (kind: ok)
* {"bar":4,"x":{"foo":3,"wat0":"joe"}}
* {"bar":5,"x":{"foo":3,"wat0":"joe","wat1":"joe"}}
*
* Fork of `capturing-func-alias-captured-mutate`, but instead of directly
* aliasing `y` via `[y]`, we make an opaque call.
*
* Note that the bug here is that we don't infer that `a = makeArray(y)`
* potentially captures a context variable into a local variable. As a result,
* we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
* currently inferring that this lambda captures `y` (for a potential later
* mutation) and simply reads `x`.
*
* Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
* used when we analyze CallExpressions.
*/
function Component({foo, bar}: {foo: number; bar: number}) {
let x = {foo};
let y: {bar: number; x?: {foo: number}} = {bar};
const f0 = function () {
let a = makeArray(y); // a = [y]
let b = x;
// this writes y.x = x
a[0].x = b;
};
f0();
mutate(y.x);
return y;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{foo: 3, bar: 4}],
sequentialRenders: [
{foo: 3, bar: 4},
{foo: 3, bar: 5},
],
};
@@ -479,6 +479,7 @@ const skipFilter = new Set([
// bugs
'fbt/bug-fbt-plural-multiple-function-calls',
'fbt/bug-fbt-plural-multiple-mixed-call-tag',
`bug-capturing-func-maybealias-captured-mutate`,
'bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr',
'bug-invalid-hoisting-functionexpr',
'bug-aliased-capture-aliased-mutate',
+3 -2
View File
@@ -27,7 +27,8 @@ function Foo({children}) {
return <div>{children}</div>;
}
function Bar({children}) {
async function Bar({children}) {
await new Promise(resolve => setTimeout(() => resolve('deferred text'), 10));
return <div>{children}</div>;
}
@@ -81,7 +82,7 @@ export default async function App({prerender}) {
<Client />
<Note />
<Foo>{dedupedChild}</Foo>
<Bar>{dedupedChild}</Bar>
<Bar>{Promise.resolve([dedupedChild])}</Bar>
</Container>
</body>
</html>
+55 -5
View File
@@ -69,7 +69,11 @@ import {createBoundServerReference} from './ReactFlightReplyClient';
import {readTemporaryReference} from './ReactFlightTemporaryReferences';
import {logComponentRender} from './ReactFlightPerformanceTrack';
import {
markAllTracksInOrder,
logComponentRender,
logDedupedComponentRender,
} from './ReactFlightPerformanceTrack';
import {
REACT_LAZY_TYPE,
@@ -127,6 +131,7 @@ export type JSONValue =
type ProfilingResult = {
track: number,
endTime: number,
component: null | ReactComponentInfo,
};
const ROW_ID = 0;
@@ -643,7 +648,14 @@ export function reportGlobalError(response: Response, error: Error): void {
}
});
if (enableProfilerTimer && enableComponentPerformanceTrack) {
flushComponentPerformance(getChunk(response, 0), 0, -Infinity);
markAllTracksInOrder();
flushComponentPerformance(
response,
getChunk(response, 0),
0,
-Infinity,
-Infinity,
);
}
}
@@ -2742,9 +2754,11 @@ function resolveTypedArray(
}
function flushComponentPerformance(
response: Response,
root: SomeChunk<any>,
trackIdx: number, // Next available track
trackTime: number, // The time after which it is available
trackTime: number, // The time after which it is available,
parentEndTime: number,
): ProfilingResult {
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
// eslint-disable-next-line react-internal/prod-error-codes
@@ -2761,6 +2775,22 @@ function flushComponentPerformance(
// chunk in two places. We should extend the current end time as if it was
// rendered as part of this tree.
const previousResult: ProfilingResult = root._children;
const previousEndTime = previousResult.endTime;
if (
parentEndTime > -Infinity &&
parentEndTime < previousEndTime &&
previousResult.component !== null
) {
// Log a placeholder for the deduped value under this child starting
// from the end of the self time of the parent and spanning until the
// the deduped end.
logDedupedComponentRender(
previousResult.component,
trackIdx,
parentEndTime,
previousEndTime,
);
}
// Since we didn't bump the track this time, we just return the same track.
previousResult.track = trackIdx;
return previousResult;
@@ -2788,25 +2818,42 @@ function flushComponentPerformance(
// The start time of this component is before the end time of the previous
// component on this track so we need to bump the next one to a parallel track.
trackIdx++;
trackTime = startTime;
}
trackTime = startTime;
break;
}
}
}
for (let i = debugInfo.length - 1; i >= 0; i--) {
const info = debugInfo[i];
if (typeof info.time === 'number') {
if (info.time > parentEndTime) {
parentEndTime = info.time;
}
}
}
}
const result: ProfilingResult = {track: trackIdx, endTime: -Infinity};
const result: ProfilingResult = {
track: trackIdx,
endTime: -Infinity,
component: null,
};
root._children = result;
let childrenEndTime = -Infinity;
let childTrackIdx = trackIdx;
let childTrackTime = trackTime;
for (let i = 0; i < children.length; i++) {
const childResult = flushComponentPerformance(
response,
children[i],
childTrackIdx,
childTrackTime,
parentEndTime,
);
if (childResult.component !== null) {
result.component = childResult.component;
}
childTrackIdx = childResult.track;
const childEndTime = childResult.endTime;
childTrackTime = childEndTime;
@@ -2837,7 +2884,10 @@ function flushComponentPerformance(
startTime,
endTime,
childrenEndTime,
response._rootEnvironmentName,
);
// Track the root most component of the result for deduping logging.
result.component = componentInfo;
}
}
}
+52 -4
View File
@@ -19,6 +19,26 @@ const supportsUserTiming =
const COMPONENTS_TRACK = 'Server Components ⚛';
const componentsTrackMarker = {
startTime: 0.001,
detail: {
devtools: {
color: 'primary-light',
track: 'Primary',
trackGroup: COMPONENTS_TRACK,
},
},
};
export function markAllTracksInOrder() {
if (supportsUserTiming) {
// Ensure we create the Server Component track groups earlier than the Client Scheduler
// and Client Components. We can always add the 0 time slot even if it's in the past.
// That's still considered for ordering.
performance.mark('Server Components Track', componentsTrackMarker);
}
}
// Reused to avoid thrashing the GC.
const reusableComponentDevToolDetails = {
color: 'primary',
@@ -52,21 +72,49 @@ export function logComponentRender(
startTime: number,
endTime: number,
childrenEndTime: number,
rootEnv: string,
): void {
if (supportsUserTiming && childrenEndTime >= 0 && trackIdx < 10) {
const env = componentInfo.env;
const name = componentInfo.name;
const isPrimaryEnv = env === rootEnv;
const selfTime = endTime - startTime;
reusableComponentDevToolDetails.color =
selfTime < 0.5
? 'primary-light'
? isPrimaryEnv
? 'primary-light'
: 'secondary-light'
: selfTime < 50
? 'primary'
? isPrimaryEnv
? 'primary'
: 'secondary'
: selfTime < 500
? 'primary-dark'
? isPrimaryEnv
? 'primary-dark'
: 'secondary-dark'
: 'error';
reusableComponentDevToolDetails.track = trackNames[trackIdx];
reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
reusableComponentOptions.end = childrenEndTime;
performance.measure(name, reusableComponentOptions);
const entryName =
isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']';
performance.measure(entryName, reusableComponentOptions);
}
}
export function logDedupedComponentRender(
componentInfo: ReactComponentInfo,
trackIdx: number,
startTime: number,
endTime: number,
): void {
if (supportsUserTiming && endTime >= 0 && trackIdx < 10) {
const name = componentInfo.name;
reusableComponentDevToolDetails.color = 'tertiary-light';
reusableComponentDevToolDetails.track = trackNames[trackIdx];
reusableComponentOptions.start = startTime < 0 ? 0 : startTime;
reusableComponentOptions.end = endTime;
const entryName = name + ' [deduped]';
performance.measure(entryName, reusableComponentOptions);
}
}
+1 -29
View File
@@ -20,7 +20,6 @@ import type {
Dependencies,
Fiber,
Dispatcher as DispatcherType,
ContextDependencyWithSelect,
} from 'react-reconciler/src/ReactInternalTypes';
import type {TransitionStatus} from 'react-reconciler/src/ReactFiberConfig';
@@ -76,13 +75,6 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
try {
// Use all hooks here to add them to the hook log.
Dispatcher.useContext(({_currentValue: null}: any));
if (typeof Dispatcher.unstable_useContextWithBailout === 'function') {
// This type check is for Flow only.
Dispatcher.unstable_useContextWithBailout(
({_currentValue: null}: any),
null,
);
}
Dispatcher.useState(null);
Dispatcher.useReducer((s: mixed, a: mixed) => s, null);
Dispatcher.useRef(null);
@@ -150,10 +142,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
let currentFiber: null | Fiber = null;
let currentHook: null | Hook = null;
let currentContextDependency:
| null
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed> = null;
let currentContextDependency: null | ContextDependency<mixed> = null;
function nextHook(): null | Hook {
const hook = currentHook;
@@ -274,22 +263,6 @@ function useContext<T>(context: ReactContext<T>): T {
return value;
}
function unstable_useContextWithBailout<T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
const value = readContext(context);
hookLog.push({
displayName: context.displayName || null,
primitive: 'ContextWithBailout',
stackError: new Error(),
value: value,
debugInfo: null,
dispatcherHookName: 'ContextWithBailout',
});
return value;
}
function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
@@ -764,7 +737,6 @@ const Dispatcher: DispatcherType = {
useCacheRefresh,
useCallback,
useContext,
unstable_useContextWithBailout,
useEffect,
useImperativeHandle,
useDebugValue,
+2 -113
View File
@@ -39,12 +39,11 @@ import {
enableLazyContextPropagation,
enableTransitionTracing,
enableUseEffectEventHook,
enableUseResourceEffectHook,
enableLegacyCache,
debugRenderPhaseSideEffectsForStrictMode,
disableLegacyMode,
enableNoCloningMemoCache,
enableContextProfiling,
enableUseResourceEffectHook,
} from 'shared/ReactFeatureFlags';
import {
REACT_CONTEXT_TYPE,
@@ -78,11 +77,7 @@ import {
ContinuousEventPriority,
higherEventPriority,
} from './ReactEventPriorities';
import {
readContext,
readContextAndCompare,
checkIfContextChanged,
} from './ReactFiberNewContext';
import {readContext, checkIfContextChanged} from './ReactFiberNewContext';
import {HostRoot, CacheComponent, HostComponent} from './ReactWorkTags';
import {
LayoutStatic as LayoutStaticEffect,
@@ -1111,16 +1106,6 @@ function updateWorkInProgressHook(): Hook {
return workInProgressHook;
}
function unstable_useContextWithBailout<T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
if (select === null) {
return readContext(context);
}
return readContextAndCompare(context, select);
}
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
return {
lastEffect: null,
@@ -3958,10 +3943,6 @@ if (enableUseEffectEventHook) {
if (enableUseResourceEffectHook) {
(ContextOnlyDispatcher: Dispatcher).useResourceEffect = throwInvalidHookError;
}
if (enableContextProfiling) {
(ContextOnlyDispatcher: Dispatcher).unstable_useContextWithBailout =
throwInvalidHookError;
}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
@@ -3995,10 +3976,6 @@ if (enableUseEffectEventHook) {
if (enableUseResourceEffectHook) {
(HooksDispatcherOnMount: Dispatcher).useResourceEffect = mountResourceEffect;
}
if (enableContextProfiling) {
(HooksDispatcherOnMount: Dispatcher).unstable_useContextWithBailout =
unstable_useContextWithBailout;
}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@@ -4033,10 +4010,6 @@ if (enableUseResourceEffectHook) {
(HooksDispatcherOnUpdate: Dispatcher).useResourceEffect =
updateResourceEffect;
}
if (enableContextProfiling) {
(HooksDispatcherOnUpdate: Dispatcher).unstable_useContextWithBailout =
unstable_useContextWithBailout;
}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@@ -4071,10 +4044,6 @@ if (enableUseResourceEffectHook) {
(HooksDispatcherOnRerender: Dispatcher).useResourceEffect =
updateResourceEffect;
}
if (enableContextProfiling) {
(HooksDispatcherOnRerender: Dispatcher).unstable_useContextWithBailout =
unstable_useContextWithBailout;
}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -4296,17 +4265,6 @@ if (__DEV__) {
);
};
}
if (enableContextProfiling) {
(HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout =
function <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
currentHookNameInDev = 'useContext';
mountHookTypesDev();
return unstable_useContextWithBailout(context, select);
};
}
HooksDispatcherOnMountWithHookTypesInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -4494,17 +4452,6 @@ if (__DEV__) {
);
};
}
if (enableContextProfiling) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout =
function <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
currentHookNameInDev = 'useContext';
updateHookTypesDev();
return unstable_useContextWithBailout(context, select);
};
}
HooksDispatcherOnUpdateInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -4692,17 +4639,6 @@ if (__DEV__) {
);
};
}
if (enableContextProfiling) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout =
function <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
currentHookNameInDev = 'useContext';
updateHookTypesDev();
return unstable_useContextWithBailout(context, select);
};
}
HooksDispatcherOnRerenderInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -4890,17 +4826,6 @@ if (__DEV__) {
);
};
}
if (enableContextProfiling) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout =
function <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
currentHookNameInDev = 'useContext';
updateHookTypesDev();
return unstable_useContextWithBailout(context, select);
};
}
InvalidNestedHooksDispatcherOnMountInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -5114,18 +5039,6 @@ if (__DEV__) {
);
};
}
if (enableContextProfiling) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout =
function <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
currentHookNameInDev = 'useContext';
warnInvalidHookAccess();
mountHookTypesDev();
return unstable_useContextWithBailout(context, select);
};
}
InvalidNestedHooksDispatcherOnUpdateInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -5339,18 +5252,6 @@ if (__DEV__) {
);
};
}
if (enableContextProfiling) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout =
function <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
currentHookNameInDev = 'useContext';
warnInvalidHookAccess();
updateHookTypesDev();
return unstable_useContextWithBailout(context, select);
};
}
InvalidNestedHooksDispatcherOnRerenderInDEV = {
readContext<T>(context: ReactContext<T>): T {
@@ -5564,16 +5465,4 @@ if (__DEV__) {
);
};
}
if (enableContextProfiling) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout =
function <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
currentHookNameInDev = 'useContext';
warnInvalidHookAccess();
updateHookTypesDev();
return unstable_useContextWithBailout(context, select);
};
}
}
+6 -131
View File
@@ -12,7 +12,6 @@ import type {
Fiber,
ContextDependency,
Dependencies,
ContextDependencyWithSelect,
} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack';
import type {Lanes} from './ReactFiberLane';
@@ -48,8 +47,6 @@ import {
enableRenderableContext,
} from 'shared/ReactFeatureFlags';
import {getHostTransitionProvider} from './ReactFiberHostContext';
import isArray from '../../shared/isArray';
import {enableContextProfiling} from '../../shared/ReactFeatureFlags';
const valueCursor: StackCursor<mixed> = createCursor(null);
@@ -69,10 +66,7 @@ if (__DEV__) {
}
let currentlyRenderingFiber: Fiber | null = null;
let lastContextDependency:
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null = null;
let lastContextDependency: ContextDependency<mixed> | null = null;
let isDisallowedContextReadInDEV: boolean = false;
@@ -401,23 +395,6 @@ function propagateContextChanges<T>(
const context: ReactContext<T> = contexts[i];
// Check if the context matches.
if (dependency.context === context) {
if (enableContextProfiling) {
const select = dependency.select;
if (select != null && dependency.lastSelectedValue != null) {
const newValue = isPrimaryRenderer
? dependency.context._currentValue
: dependency.context._currentValue2;
if (
!checkIfSelectedContextValuesChanged(
dependency.lastSelectedValue,
select(newValue),
)
) {
// Compared value hasn't changed. Bail out early.
continue findContext;
}
}
}
// Match! Schedule an update on this fiber.
// In the lazy implementation, don't mark a dirty flag on the
@@ -657,29 +634,6 @@ function propagateParentContextChanges(
workInProgress.flags |= DidPropagateContext;
}
function checkIfSelectedContextValuesChanged(
oldComparedValue: Array<mixed>,
newComparedValue: Array<mixed>,
): boolean {
// We have an implicit contract that compare functions must return arrays.
// This allows us to compare multiple values in the same context access
// since compiling to additional hook calls regresses perf.
if (isArray(oldComparedValue) && isArray(newComparedValue)) {
if (oldComparedValue.length !== newComparedValue.length) {
return true;
}
for (let i = 0; i < oldComparedValue.length; i++) {
if (!is(newComparedValue[i], oldComparedValue[i])) {
return true;
}
}
} else {
throw new Error('Compared context values must be arrays');
}
return false;
}
export function checkIfContextChanged(
currentDependencies: Dependencies,
): boolean {
@@ -698,23 +652,8 @@ export function checkIfContextChanged(
? context._currentValue
: context._currentValue2;
const oldValue = dependency.memoizedValue;
if (
enableContextProfiling &&
dependency.select != null &&
dependency.lastSelectedValue != null
) {
if (
checkIfSelectedContextValuesChanged(
dependency.lastSelectedValue,
dependency.select(newValue),
)
) {
return true;
}
} else {
if (!is(newValue, oldValue)) {
return true;
}
if (!is(newValue, oldValue)) {
return true;
}
dependency = dependency.next;
}
@@ -747,21 +686,6 @@ export function prepareToReadContext(
}
}
export function readContextAndCompare<C>(
context: ReactContext<C>,
select: C => Array<mixed>,
): C {
if (!(enableLazyContextPropagation && enableContextProfiling)) {
throw new Error('Not implemented.');
}
return readContextForConsumer_withSelect(
currentlyRenderingFiber,
context,
select,
);
}
export function readContext<T>(context: ReactContext<T>): T {
if (__DEV__) {
// This warning would fire if you read context inside a Hook like useMemo.
@@ -789,59 +713,10 @@ export function readContextDuringReconciliation<T>(
return readContextForConsumer(consumer, context);
}
function readContextForConsumer_withSelect<C>(
function readContextForConsumer<T>(
consumer: Fiber | null,
context: ReactContext<C>,
select: C => Array<mixed>,
): C {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
select: ((select: any): (context: mixed) => Array<mixed>),
lastSelectedValue: select(value),
};
if (lastContextDependency === null) {
if (consumer === null) {
throw new Error(
'Context can only be read while React is rendering. ' +
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
'In function components, you can read it directly in the function body, but not ' +
'inside Hooks like useReducer() or useMemo().',
);
}
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
return value;
}
function readContextForConsumer<C>(
consumer: Fiber | null,
context: ReactContext<C>,
): C {
context: ReactContext<T>,
): T {
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
@@ -63,7 +63,7 @@ export function setCurrentTrackFromLanes(lanes: Lanes): void {
}
const blockingLaneMarker = {
startTime: 0,
startTime: 0.003,
detail: {
devtools: {
color: 'primary-light',
@@ -74,7 +74,7 @@ const blockingLaneMarker = {
};
const transitionLaneMarker = {
startTime: 0,
startTime: 0.003,
detail: {
devtools: {
color: 'primary-light',
@@ -85,7 +85,7 @@ const transitionLaneMarker = {
};
const suspenseLaneMarker = {
startTime: 0,
startTime: 0.003,
detail: {
devtools: {
color: 'primary-light',
@@ -96,7 +96,7 @@ const suspenseLaneMarker = {
};
const idleLaneMarker = {
startTime: 0,
startTime: 0.003,
detail: {
devtools: {
color: 'primary-light',
+7 -20
View File
@@ -63,27 +63,18 @@ export type HookType =
| 'useFormState'
| 'useActionState';
export type ContextDependency<C> = {
context: ReactContext<C>,
next: ContextDependency<mixed> | ContextDependencyWithSelect<mixed> | null,
memoizedValue: C,
};
export type ContextDependencyWithSelect<C> = {
context: ReactContext<C>,
next: ContextDependency<mixed> | ContextDependencyWithSelect<mixed> | null,
memoizedValue: C,
select: C => Array<mixed>,
lastSelectedValue: ?Array<mixed>,
export type ContextDependency<T> = {
context: ReactContext<T>,
next: ContextDependency<mixed> | null,
memoizedValue: T,
...
};
export type Dependencies = {
lanes: Lanes,
firstContext:
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null,
firstContext: ContextDependency<mixed> | null,
_debugThenableState?: null | ThenableState, // DEV-only
...
};
export type MemoCache = {
@@ -401,10 +392,6 @@ export type Dispatcher = {
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
unstable_useContextWithBailout?: <T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
) => T,
useContext<T>(context: ReactContext<T>): T,
useRef<T>(initialValue: T): {current: T},
useEffect(
@@ -1,217 +0,0 @@
let React;
let ReactNoop;
let Scheduler;
let act;
let assertLog;
let useState;
let useContext;
let unstable_useContextWithBailout;
describe('ReactContextWithBailout', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
const testUtils = require('internal-test-utils');
act = testUtils.act;
assertLog = testUtils.assertLog;
useState = React.useState;
useContext = React.useContext;
unstable_useContextWithBailout = React.unstable_useContextWithBailout;
});
function Text({text}) {
Scheduler.log(text);
return text;
}
// @gate enableLazyContextPropagation && enableContextProfiling
test('unstable_useContextWithBailout basic usage', async () => {
const Context = React.createContext();
let setContext;
function App() {
const [context, _setContext] = useState({a: 'A0', b: 'B0', c: 'C0'});
setContext = _setContext;
return (
<Context.Provider value={context}>
<Indirection />
</Context.Provider>
);
}
// Intermediate parent that bails out. Children will only re-render when the
// context changes.
const Indirection = React.memo(() => {
return (
<>
A: <A />, B: <B />, C: <C />, AB: <AB />
</>
);
});
function A() {
const {a} = unstable_useContextWithBailout(Context, context => [
context.a,
]);
return <Text text={a} />;
}
function B() {
const {b} = unstable_useContextWithBailout(Context, context => [
context.b,
]);
return <Text text={b} />;
}
function C() {
const {c} = unstable_useContextWithBailout(Context, context => [
context.c,
]);
return <Text text={c} />;
}
function AB() {
const {a, b} = unstable_useContextWithBailout(Context, context => [
context.a,
context.b,
]);
return <Text text={a + b} />;
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App />);
});
assertLog(['A0', 'B0', 'C0', 'A0B0']);
expect(root).toMatchRenderedOutput('A: A0, B: B0, C: C0, AB: A0B0');
// Update a. Only the A and AB consumer should re-render.
await act(async () => {
setContext({a: 'A1', c: 'C0', b: 'B0'});
});
assertLog(['A1', 'A1B0']);
expect(root).toMatchRenderedOutput('A: A1, B: B0, C: C0, AB: A1B0');
// Update b. Only the B and AB consumer should re-render.
await act(async () => {
setContext({a: 'A1', b: 'B1', c: 'C0'});
});
assertLog(['B1', 'A1B1']);
expect(root).toMatchRenderedOutput('A: A1, B: B1, C: C0, AB: A1B1');
// Update c. Only the C consumer should re-render.
await act(async () => {
setContext({a: 'A1', b: 'B1', c: 'C1'});
});
assertLog(['C1']);
expect(root).toMatchRenderedOutput('A: A1, B: B1, C: C1, AB: A1B1');
});
// @gate enableLazyContextPropagation && enableContextProfiling
test('unstable_useContextWithBailout and useContext subscribing to same context in same component', async () => {
const Context = React.createContext();
let setContext;
function App() {
const [context, _setContext] = useState({a: 0, b: 0, unrelated: 0});
setContext = _setContext;
return (
<Context.Provider value={context}>
<Indirection />
</Context.Provider>
);
}
// Intermediate parent that bails out. Children will only re-render when the
// context changes.
const Indirection = React.memo(() => {
return <Child />;
});
function Child() {
const {a} = unstable_useContextWithBailout(Context, context => [
context.a,
]);
const context = useContext(Context);
return <Text text={`A: ${a}, B: ${context.b}`} />;
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App />);
});
assertLog(['A: 0, B: 0']);
expect(root).toMatchRenderedOutput('A: 0, B: 0');
// Update an unrelated field that isn't used by the component. The context
// attempts to bail out, but the normal context forces an update.
await act(async () => {
setContext({a: 0, b: 0, unrelated: 1});
});
assertLog(['A: 0, B: 0']);
expect(root).toMatchRenderedOutput('A: 0, B: 0');
});
// @gate enableLazyContextPropagation && enableContextProfiling
test('unstable_useContextWithBailout and useContext subscribing to different contexts in same component', async () => {
const ContextA = React.createContext();
const ContextB = React.createContext();
let setContextA;
let setContextB;
function App() {
const [a, _setContextA] = useState({a: 0, unrelated: 0});
const [b, _setContextB] = useState(0);
setContextA = _setContextA;
setContextB = _setContextB;
return (
<ContextA.Provider value={a}>
<ContextB.Provider value={b}>
<Indirection />
</ContextB.Provider>
</ContextA.Provider>
);
}
// Intermediate parent that bails out. Children will only re-render when the
// context changes.
const Indirection = React.memo(() => {
return <Child />;
});
function Child() {
const {a} = unstable_useContextWithBailout(ContextA, context => [
context.a,
]);
const b = useContext(ContextB);
return <Text text={`A: ${a}, B: ${b}`} />;
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App />);
});
assertLog(['A: 0, B: 0']);
expect(root).toMatchRenderedOutput('A: 0, B: 0');
// Update a field in A that isn't part of the compared context. It should
// bail out.
await act(async () => {
setContextA({a: 0, unrelated: 1});
});
assertLog([]);
expect(root).toMatchRenderedOutput('A: 0, B: 0');
// Now update the same a field again, but this time, also update a different
// context in the same batch. The other context prevents a bail out.
await act(async () => {
setContextA({a: 0, unrelated: 1});
setContextB(1);
});
assertLog(['A: 0, B: 1']);
expect(root).toMatchRenderedOutput('A: 0, B: 1');
});
});
-1
View File
@@ -42,7 +42,6 @@ export {
use,
useActionState,
useCallback,
unstable_useContextWithBailout,
useContext,
useDebugValue,
useDeferredValue,
-2
View File
@@ -37,7 +37,6 @@ import {postpone} from './ReactPostpone';
import {
getCacheForType,
useCallback,
unstable_useContextWithBailout,
useContext,
useEffect,
useEffectEvent,
@@ -86,7 +85,6 @@ export {
cache,
postpone as unstable_postpone,
useCallback,
unstable_useContextWithBailout,
useContext,
useEffect,
useEffectEvent as experimental_useEffectEvent,
-25
View File
@@ -19,10 +19,6 @@ import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {enableUseResourceEffectHook} from 'shared/ReactFeatureFlags';
import {
enableContextProfiling,
enableLazyContextPropagation,
} from '../../shared/ReactFeatureFlags';
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
@@ -69,27 +65,6 @@ export function useContext<T>(Context: ReactContext<T>): T {
return dispatcher.useContext(Context);
}
export function unstable_useContextWithBailout<T>(
context: ReactContext<T>,
select: (T => Array<mixed>) | null,
): T {
if (!(enableLazyContextPropagation && enableContextProfiling)) {
throw new Error('Not implemented.');
}
const dispatcher = resolveDispatcher();
if (__DEV__) {
if (context.$$typeof === REACT_CONSUMER_TYPE) {
console.error(
'Calling useContext(Context.Consumer) is not supported and will cause bugs. ' +
'Did you mean to call useContext(Context) instead?',
);
}
}
// $FlowFixMe[not-a-function] This is unstable, thus optional
return dispatcher.unstable_useContextWithBailout(context, select);
}
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
-3
View File
@@ -101,9 +101,6 @@ export const enableTransitionTracing = false;
export const enableLazyContextPropagation = true;
// Expose unstable useContext for performance testing
export const enableContextProfiling = false;
// FB-only usage. The new API has different semantics.
export const enableLegacyHidden = false;
@@ -53,7 +53,6 @@ export const enableFizzExternalRuntime = true;
export const enableGetInspectorDataForInstanceInProduction = true;
export const enableHalt = false;
export const enableInfiniteRenderLoopDetection = false;
export const enableContextProfiling = false;
export const enableLazyContextPropagation = true;
export const enableLegacyCache = false;
export const enableLegacyFBSupport = false;
@@ -43,7 +43,6 @@ export const enableHalt = false;
export const enableHiddenSubtreeInsertionEffectCleanup = false;
export const enableInfiniteRenderLoopDetection = false;
export const enableLazyContextPropagation = true;
export const enableContextProfiling = false;
export const enableLegacyCache = false;
export const enableLegacyFBSupport = false;
export const enableLegacyHidden = false;
@@ -49,7 +49,6 @@ export const transitionLaneExpirationMs = 5000;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = true;
export const enableContextProfiling = false;
export const enableLegacyHidden = false;
export const enableTransitionTracing = false;
@@ -51,7 +51,6 @@ export const transitionLaneExpirationMs = 5000;
export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = true;
export const enableContextProfiling = false;
export const enableLegacyHidden = false;
export const enableTransitionTracing = false;
@@ -79,8 +79,6 @@ export const enablePostpone = false;
export const enableHalt = false;
export const enableContextProfiling = true;
// TODO: www currently relies on this feature. It's disabled in open source.
// Need to remove it.
export const disableCommentsAsDOMContainers = false;
-1
View File
@@ -530,4 +530,3 @@
"542": "Suspense Exception: This is not a real error! It's an implementation detail of `useActionState` to interrupt the current render. You must either rethrow it immediately, or move the `useActionState` call outside of the `try/catch` block. Capturing without rethrowing will lead to unexpected behavior.\n\nTo handle async errors, wrap your component in an error boundary.",
"543": "Expected a ResourceEffectUpdate to be pushed together with ResourceEffectIdentity. This is a bug in React."
}