Skip to content

Commit c6a3e4d

Browse files
author
Jack Pope
committed
Add unstable context access API for internal profiling
1 parent f6cce07 commit c6a3e4d

15 files changed

+421
-14
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
155155

156156
let currentFiber: null | Fiber = null;
157157
let currentHook: null | Hook = null;
158-
let currentContextDependency: null | ContextDependency<mixed> = null;
158+
let currentContextDependency: null | ContextDependency<mixed, mixed> = null;
159159

160160
function nextHook(): null | Hook {
161161
const hook = currentHook;

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
enableUseDeferredValueInitialArg,
4848
disableLegacyMode,
4949
enableNoCloningMemoCache,
50+
enableContextProfiling,
5051
} from 'shared/ReactFeatureFlags';
5152
import {
5253
REACT_CONTEXT_TYPE,
@@ -81,7 +82,11 @@ import {
8182
ContinuousEventPriority,
8283
higherEventPriority,
8384
} from './ReactEventPriorities';
84-
import {readContext, checkIfContextChanged} from './ReactFiberNewContext';
85+
import {
86+
readContext,
87+
readContextAndCompare,
88+
checkIfContextChanged,
89+
} from './ReactFiberNewContext';
8590
import {HostRoot, CacheComponent, HostComponent} from './ReactWorkTags';
8691
import {
8792
LayoutStatic as LayoutStaticEffect,
@@ -1053,6 +1058,13 @@ function updateWorkInProgressHook(): Hook {
10531058
return workInProgressHook;
10541059
}
10551060

1061+
function unstable_useContextWithBailout<T>(
1062+
context: ReactContext<T>,
1063+
compare: void | (T => mixed),
1064+
): T {
1065+
return readContextAndCompare(context, compare);
1066+
}
1067+
10561068
// NOTE: defining two versions of this function to avoid size impact when this feature is disabled.
10571069
// Previously this function was inlined, the additional `memoCache` property makes it not inlined.
10581070
let createFunctionComponentUpdateQueue: () => FunctionComponentUpdateQueue;
@@ -3689,6 +3701,10 @@ if (enableAsyncActions) {
36893701
if (enableAsyncActions) {
36903702
(ContextOnlyDispatcher: Dispatcher).useOptimistic = throwInvalidHookError;
36913703
}
3704+
if (enableContextProfiling) {
3705+
(ContextOnlyDispatcher: Dispatcher).unstable_useContextWithBailout =
3706+
throwInvalidHookError;
3707+
}
36923708

36933709
const HooksDispatcherOnMount: Dispatcher = {
36943710
readContext,
@@ -3728,6 +3744,10 @@ if (enableAsyncActions) {
37283744
if (enableAsyncActions) {
37293745
(HooksDispatcherOnMount: Dispatcher).useOptimistic = mountOptimistic;
37303746
}
3747+
if (enableContextProfiling) {
3748+
(HooksDispatcherOnMount: Dispatcher).unstable_useContextWithBailout =
3749+
unstable_useContextWithBailout;
3750+
}
37313751

37323752
const HooksDispatcherOnUpdate: Dispatcher = {
37333753
readContext,
@@ -3767,6 +3787,10 @@ if (enableAsyncActions) {
37673787
if (enableAsyncActions) {
37683788
(HooksDispatcherOnUpdate: Dispatcher).useOptimistic = updateOptimistic;
37693789
}
3790+
if (enableContextProfiling) {
3791+
(HooksDispatcherOnUpdate: Dispatcher).unstable_useContextWithBailout =
3792+
unstable_useContextWithBailout;
3793+
}
37703794

37713795
const HooksDispatcherOnRerender: Dispatcher = {
37723796
readContext,
@@ -3806,6 +3830,10 @@ if (enableAsyncActions) {
38063830
if (enableAsyncActions) {
38073831
(HooksDispatcherOnRerender: Dispatcher).useOptimistic = rerenderOptimistic;
38083832
}
3833+
if (enableContextProfiling) {
3834+
(HooksDispatcherOnRerender: Dispatcher).unstable_useContextWithBailout =
3835+
unstable_useContextWithBailout;
3836+
}
38093837

38103838
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
38113839
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -4019,6 +4047,14 @@ if (__DEV__) {
40194047
return mountOptimistic(passthrough, reducer);
40204048
};
40214049
}
4050+
if (enableContextProfiling) {
4051+
(HooksDispatcherOnMountInDEV: Dispatcher).unstable_useContextWithBailout =
4052+
function <T>(context: ReactContext<T>, compare: void | (T => mixed)): T {
4053+
currentHookNameInDev = 'useContext';
4054+
mountHookTypesDev();
4055+
return unstable_useContextWithBailout(context, compare);
4056+
};
4057+
}
40224058

40234059
HooksDispatcherOnMountWithHookTypesInDEV = {
40244060
readContext<T>(context: ReactContext<T>): T {
@@ -4200,6 +4236,14 @@ if (__DEV__) {
42004236
return mountOptimistic(passthrough, reducer);
42014237
};
42024238
}
4239+
if (enableContextProfiling) {
4240+
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).unstable_useContextWithBailout =
4241+
function <T>(context: ReactContext<T>, compare: void | (T => mixed)): T {
4242+
currentHookNameInDev = 'useContext';
4243+
updateHookTypesDev();
4244+
return unstable_useContextWithBailout(context, compare);
4245+
};
4246+
}
42034247

42044248
HooksDispatcherOnUpdateInDEV = {
42054249
readContext<T>(context: ReactContext<T>): T {
@@ -4380,6 +4424,14 @@ if (__DEV__) {
43804424
return updateOptimistic(passthrough, reducer);
43814425
};
43824426
}
4427+
if (enableContextProfiling) {
4428+
(HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout =
4429+
function <T>(context: ReactContext<T>, compare: void | (T => mixed)): T {
4430+
currentHookNameInDev = 'useContext';
4431+
updateHookTypesDev();
4432+
return unstable_useContextWithBailout(context, compare);
4433+
};
4434+
}
43834435

43844436
HooksDispatcherOnRerenderInDEV = {
43854437
readContext<T>(context: ReactContext<T>): T {
@@ -4560,6 +4612,14 @@ if (__DEV__) {
45604612
return rerenderOptimistic(passthrough, reducer);
45614613
};
45624614
}
4615+
if (enableContextProfiling) {
4616+
(HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout =
4617+
function <T>(context: ReactContext<T>, compare: void | (T => mixed)): T {
4618+
currentHookNameInDev = 'useContext';
4619+
updateHookTypesDev();
4620+
return unstable_useContextWithBailout(context, compare);
4621+
};
4622+
}
45634623

45644624
InvalidNestedHooksDispatcherOnMountInDEV = {
45654625
readContext<T>(context: ReactContext<T>): T {
@@ -4766,6 +4826,15 @@ if (__DEV__) {
47664826
return mountOptimistic(passthrough, reducer);
47674827
};
47684828
}
4829+
if (enableContextProfiling) {
4830+
(HooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout =
4831+
function <T>(context: ReactContext<T>, compare: void | (T => mixed)): T {
4832+
currentHookNameInDev = 'useContext';
4833+
warnInvalidHookAccess();
4834+
mountHookTypesDev();
4835+
return unstable_useContextWithBailout(context, compare);
4836+
};
4837+
}
47694838

47704839
InvalidNestedHooksDispatcherOnUpdateInDEV = {
47714840
readContext<T>(context: ReactContext<T>): T {
@@ -4972,6 +5041,15 @@ if (__DEV__) {
49725041
return updateOptimistic(passthrough, reducer);
49735042
};
49745043
}
5044+
if (enableContextProfiling) {
5045+
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).unstable_useContextWithBailout =
5046+
function <T>(context: ReactContext<T>, compare: void | (T => mixed)): T {
5047+
currentHookNameInDev = 'useContext';
5048+
warnInvalidHookAccess();
5049+
updateHookTypesDev();
5050+
return unstable_useContextWithBailout(context, compare);
5051+
};
5052+
}
49755053

49765054
InvalidNestedHooksDispatcherOnRerenderInDEV = {
49775055
readContext<T>(context: ReactContext<T>): T {
@@ -5178,4 +5256,13 @@ if (__DEV__) {
51785256
return rerenderOptimistic(passthrough, reducer);
51795257
};
51805258
}
5259+
if (enableContextProfiling) {
5260+
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).unstable_useContextWithBailout =
5261+
function <T>(context: ReactContext<T>, compare: void | (T => mixed)): T {
5262+
currentHookNameInDev = 'useContext';
5263+
warnInvalidHookAccess();
5264+
updateHookTypesDev();
5265+
return unstable_useContextWithBailout(context, compare);
5266+
};
5267+
}
51815268
}

packages/react-reconciler/src/ReactFiberNewContext.js

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import {
5151
getHostTransitionProvider,
5252
HostTransitionContext,
5353
} from './ReactFiberHostContext';
54+
import isArray from '../../shared/isArray';
55+
import {enableContextProfiling} from '../../shared/ReactFeatureFlags';
5456

5557
const valueCursor: StackCursor<mixed> = createCursor(null);
5658

@@ -70,7 +72,7 @@ if (__DEV__) {
7072
}
7173

7274
let currentlyRenderingFiber: Fiber | null = null;
73-
let lastContextDependency: ContextDependency<mixed> | null = null;
75+
let lastContextDependency: ContextDependency<mixed, mixed> | null = null;
7476
let lastFullyObservedContext: ReactContext<any> | null = null;
7577

7678
let isDisallowedContextReadInDEV: boolean = false;
@@ -400,8 +402,22 @@ function propagateContextChanges<T>(
400402
findContext: for (let i = 0; i < contexts.length; i++) {
401403
const context: ReactContext<T> = contexts[i];
402404
// Check if the context matches.
403-
// TODO: Compare selected values to bail out early.
404405
if (dependency.context === context) {
406+
const compare = dependency.compare;
407+
if (enableContextProfiling && compare != null) {
408+
const newValue = isPrimaryRenderer
409+
? dependency.context._currentValue
410+
: dependency.context._currentValue2;
411+
if (
412+
!checkIfComparedContextValuesChanged(
413+
dependency.lastComparedValue,
414+
compare(newValue),
415+
)
416+
) {
417+
// Compared value hasn't changed. Bail out early.
418+
continue findContext;
419+
}
420+
}
405421
// Match! Schedule an update on this fiber.
406422

407423
// In the lazy implementation, don't mark a dirty flag on the
@@ -641,6 +657,28 @@ function propagateParentContextChanges(
641657
workInProgress.flags |= DidPropagateContext;
642658
}
643659

660+
function checkIfComparedContextValuesChanged(
661+
oldComparedValue: mixed,
662+
newComparedValue: mixed,
663+
): boolean {
664+
if (isArray(oldComparedValue) && isArray(newComparedValue)) {
665+
for (
666+
let i = 0;
667+
i < oldComparedValue.length && i < newComparedValue.length;
668+
i++
669+
) {
670+
if (!is(newComparedValue[i], oldComparedValue[i])) {
671+
return true;
672+
}
673+
}
674+
} else {
675+
if (!is(newComparedValue, oldComparedValue)) {
676+
return true;
677+
}
678+
}
679+
return false;
680+
}
681+
644682
export function checkIfContextChanged(
645683
currentDependencies: Dependencies,
646684
): boolean {
@@ -659,8 +697,20 @@ export function checkIfContextChanged(
659697
? context._currentValue
660698
: context._currentValue2;
661699
const oldValue = dependency.memoizedValue;
662-
if (!is(newValue, oldValue)) {
663-
return true;
700+
const compare = dependency.compare;
701+
if (enableContextProfiling && compare != null) {
702+
if (
703+
checkIfComparedContextValuesChanged(
704+
dependency.lastComparedValue,
705+
compare(newValue),
706+
)
707+
) {
708+
return true;
709+
}
710+
} else {
711+
if (!is(newValue, oldValue)) {
712+
return true;
713+
}
664714
}
665715
dependency = dependency.next;
666716
}
@@ -694,6 +744,17 @@ export function prepareToReadContext(
694744
}
695745
}
696746

747+
export function readContextAndCompare<C>(
748+
context: ReactContext<C>,
749+
compare: void | (C => mixed),
750+
): C {
751+
if (!enableLazyContextPropagation) {
752+
return readContext(context);
753+
}
754+
755+
return readContextForConsumer(currentlyRenderingFiber, context, compare);
756+
}
757+
697758
export function readContext<T>(context: ReactContext<T>): T {
698759
if (__DEV__) {
699760
// This warning would fire if you read context inside a Hook like useMemo.
@@ -721,10 +782,13 @@ export function readContextDuringReconciliation<T>(
721782
return readContextForConsumer(consumer, context);
722783
}
723784

724-
function readContextForConsumer<T>(
785+
type ContextCompare<C, S> = C => S;
786+
787+
function readContextForConsumer<C, S>(
725788
consumer: Fiber | null,
726-
context: ReactContext<T>,
727-
): T {
789+
context: ReactContext<C>,
790+
compare?: void | (C => S),
791+
): C {
728792
const value = isPrimaryRenderer
729793
? context._currentValue
730794
: context._currentValue2;
@@ -736,6 +800,8 @@ function readContextForConsumer<T>(
736800
context: ((context: any): ReactContext<mixed>),
737801
memoizedValue: value,
738802
next: null,
803+
compare: ((compare: any): ContextCompare<mixed, mixed> | null),
804+
lastComparedValue: compare != null ? compare(value) : null,
739805
};
740806

741807
if (lastContextDependency === null) {

packages/react-reconciler/src/ReactInternalTypes.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,18 @@ export type HookType =
6161
| 'useFormState'
6262
| 'useActionState';
6363

64-
export type ContextDependency<T> = {
65-
context: ReactContext<T>,
66-
next: ContextDependency<mixed> | null,
67-
memoizedValue: T,
64+
export type ContextDependency<C, S> = {
65+
context: ReactContext<C>,
66+
next: ContextDependency<mixed, mixed> | null,
67+
memoizedValue: C,
68+
compare: (C => S) | null,
69+
lastComparedValue: S | null,
6870
...
6971
};
7072

7173
export type Dependencies = {
7274
lanes: Lanes,
73-
firstContext: ContextDependency<mixed> | null,
75+
firstContext: ContextDependency<mixed, mixed> | null,
7476
...
7577
};
7678

@@ -384,6 +386,10 @@ export type Dispatcher = {
384386
initialArg: I,
385387
init?: (I) => S,
386388
): [S, Dispatch<A>],
389+
unstable_useContextWithBailout?: <T>(
390+
context: ReactContext<T>,
391+
compare: void | (T => mixed),
392+
) => T,
387393
useContext<T>(context: ReactContext<T>): T,
388394
useRef<T>(initialValue: T): {current: T},
389395
useEffect(

0 commit comments

Comments
 (0)