Skip to content

Commit 4272b7f

Browse files
committed
Implementation of useContextSelector hook
1 parent 4621ce6 commit 4272b7f

File tree

5 files changed

+341
-2
lines changed

5 files changed

+341
-2
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
1717
import ReactSharedInternals from 'shared/ReactSharedInternals';
1818

1919
import {NoWork} from './ReactFiberExpirationTime';
20-
import {readContext} from './ReactFiberNewContext';
20+
import {readContext, selectFromContext} from './ReactFiberNewContext';
2121
import {
2222
Update as UpdateEffect,
2323
Passive as PassiveEffect,
@@ -64,6 +64,7 @@ export type Dispatcher = {
6464
context: ReactContext<T>,
6565
observedBits: void | number | boolean,
6666
): T,
67+
useContextSelector<T, S>(context: ReactContext<T>, selector: (T) => S): S,
6768
useRef<T>(initialValue: T): {current: T},
6869
useEffect(
6970
create: () => (() => void) | void,
@@ -602,6 +603,63 @@ function updateWorkInProgressHook(): Hook {
602603
return workInProgressHook;
603604
}
604605

606+
function makeSelect<T, S>(
607+
context: ReactContext<T>,
608+
selector: T => S,
609+
): (T, (T) => S) => [S, boolean] {
610+
// close over memoized value and selection
611+
let previousValue, previousSelection;
612+
613+
// select function will return a tuple of the selection as well as whether
614+
// the selection was a new value or not
615+
return function select(value: T) {
616+
let selection = previousSelection;
617+
let isNew = false;
618+
619+
// don't recompute if values are the same
620+
if (!is(value, previousValue)) {
621+
selection = selector(value);
622+
if (!is(selection, previousSelection)) {
623+
// if same we can still consider the selection memoized since the selected values are identical
624+
isNew = true;
625+
}
626+
}
627+
previousValue = value;
628+
previousSelection = selection;
629+
return [selection, isNew];
630+
};
631+
}
632+
633+
function mountContextSelector<T, S>(
634+
context: ReactContext<T>,
635+
selector: T => S,
636+
): S {
637+
const hook = mountWorkInProgressHook();
638+
let select = makeSelect(context, selector);
639+
let [selection] = selectFromContext(context, select);
640+
hook.memoizedState = [context, selector, select];
641+
return selection;
642+
}
643+
644+
function updateContextSelector<T, S>(
645+
context: ReactContext<T>,
646+
selector: T => S,
647+
): S {
648+
const hook = updateWorkInProgressHook();
649+
let [previousContext, previousSelector, previousSelect] = hook.memoizedState;
650+
651+
if (context !== previousContext || selector !== previousSelector) {
652+
// context and or selector have changed. we need to discard memoizedState
653+
// and recreate our select function
654+
let select = makeSelect(context, selector);
655+
let [selection] = selectFromContext(context, select);
656+
hook.memoizedState = [context, selector, select];
657+
return selection;
658+
} else {
659+
return selectFromContext(context, previousSelect)[0];
660+
}
661+
}
662+
605663
function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {
606664
return {
607665
lastEffect: null,
@@ -1223,6 +1281,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
12231281

12241282
useCallback: throwInvalidHookError,
12251283
useContext: throwInvalidHookError,
1284+
useContextSelector: throwInvalidHookError,
12261285
useEffect: throwInvalidHookError,
12271286
useImperativeHandle: throwInvalidHookError,
12281287
useLayoutEffect: throwInvalidHookError,
@@ -1238,6 +1297,7 @@ const HooksDispatcherOnMount: Dispatcher = {
12381297

12391298
useCallback: mountCallback,
12401299
useContext: readContext,
1300+
useContextSelector: mountContextSelector,
12411301
useEffect: mountEffect,
12421302
useImperativeHandle: mountImperativeHandle,
12431303
useLayoutEffect: mountLayoutEffect,
@@ -1253,6 +1313,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
12531313

12541314
useCallback: updateCallback,
12551315
useContext: readContext,
1316+
useContextSelector: updateContextSelector,
12561317
useEffect: updateEffect,
12571318
useImperativeHandle: updateImperativeHandle,
12581319
useLayoutEffect: updateLayoutEffect,
@@ -1312,6 +1373,11 @@ if (__DEV__) {
13121373
mountHookTypesDev();
13131374
return readContext(context, observedBits);
13141375
},
1376+
useContextSelector<T, S>(context: ReactContext<T>, selector: T => S): S {
1377+
currentHookNameInDev = 'useContextSelector';
1378+
mountHookTypesDev();
1379+
return mountContextSelector(context, selector);
1380+
},
13151381
useEffect(
13161382
create: () => (() => void) | void,
13171383
deps: Array<mixed> | void | null,
@@ -1413,6 +1479,11 @@ if (__DEV__) {
14131479
updateHookTypesDev();
14141480
return readContext(context, observedBits);
14151481
},
1482+
useContextSelector<T, S>(context: ReactContext<T>, selector: T => S): S {
1483+
currentHookNameInDev = 'useContextSelector';
1484+
updateHookTypesDev();
1485+
return mountContextSelector(context, selector);
1486+
},
14161487
useEffect(
14171488
create: () => (() => void) | void,
14181489
deps: Array<mixed> | void | null,
@@ -1510,6 +1581,11 @@ if (__DEV__) {
15101581
updateHookTypesDev();
15111582
return readContext(context, observedBits);
15121583
},
1584+
useContextSelector<T, S>(context: ReactContext<T>, selector: T => S): S {
1585+
currentHookNameInDev = 'useContextSelector';
1586+
updateHookTypesDev();
1587+
return updateContextSelector(context, selector);
1588+
},
15131589
useEffect(
15141590
create: () => (() => void) | void,
15151591
deps: Array<mixed> | void | null,
@@ -1610,6 +1686,12 @@ if (__DEV__) {
16101686
mountHookTypesDev();
16111687
return readContext(context, observedBits);
16121688
},
1689+
useContextSelector<T, S>(context: ReactContext<T>, selector: T => S): S {
1690+
currentHookNameInDev = 'useContextSelector';
1691+
warnInvalidHookAccess();
1692+
mountHookTypesDev();
1693+
return mountContextSelector(context, selector);
1694+
},
16131695
useEffect(
16141696
create: () => (() => void) | void,
16151697
deps: Array<mixed> | void | null,
@@ -1718,6 +1800,12 @@ if (__DEV__) {
17181800
updateHookTypesDev();
17191801
return readContext(context, observedBits);
17201802
},
1803+
useContextSelector<T, S>(context: ReactContext<T>, selector: T => S): S {
1804+
currentHookNameInDev = 'useContextSelector';
1805+
warnInvalidHookAccess();
1806+
updateHookTypesDev();
1807+
return updateContextSelector(context, selector);
1808+
},
17211809
useEffect(
17221810
create: () => (() => void) | void,
17231811
deps: Array<mixed> | void | null,

packages/react-reconciler/src/ReactFiberNewContext.js

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,21 @@ export function checkContextDependencies(
266266
let context = dependency.context;
267267
let observedBits = dependency.observedBits;
268268
if ((observedBits & context._currentChangedBits) !== 0) {
269-
return true;
269+
let requiresUpdate = true;
270+
271+
let selector = dependency.selector;
272+
if (typeof selector === 'function') {
273+
let [, isNew] = selector(
274+
isPrimaryRenderer
275+
? context._currentValue
276+
: context._currentValue2,
277+
);
278+
requiresUpdate = isNew;
279+
}
280+
281+
if (requiresUpdate) {
282+
return true;
283+
}
270284
}
271285
dependency = dependency.next;
272286
}
@@ -634,3 +648,50 @@ export function readContext<T>(
634648
}
635649
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
636650
}
651+
652+
export function selectFromContext<T, S>(
653+
context: ReactContext<T>,
654+
select: T => [S, boolean],
655+
): [S, boolean] {
656+
if (__DEV__) {
657+
// This warning would fire if you read context inside a Hook like useMemo.
658+
// Unlike the class check below, it's not enforced in production for perf.
659+
warning(
660+
!isDisallowedContextReadInDEV,
661+
'Context can only be read while React is rendering. ' +
662+
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
663+
'In function components, you can read it directly in the function body, but not ' +
664+
'inside Hooks like useReducer() or useMemo().',
665+
);
666+
}
667+
668+
let contextItem = {
669+
context: ((context: any): ReactContext<mixed>),
670+
observedBits: MAX_SIGNED_31_BIT_INT,
671+
selector: select,
672+
next: null,
673+
};
674+
675+
if (lastContextDependency === null) {
676+
invariant(
677+
currentlyRenderingFiber !== null,
678+
'Context can only be read while React is rendering. ' +
679+
'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
680+
'In function components, you can read it directly in the function body, but not ' +
681+
'inside Hooks like useReducer() or useMemo().',
682+
);
683+
684+
// This is the first dependency for this component. Create a new list.
685+
lastContextDependency = contextItem;
686+
currentlyRenderingFiber.contextDependencies = {
687+
first: contextItem,
688+
expirationTime: NoWork,
689+
};
690+
} else {
691+
// Append a new context item.
692+
lastContextDependency = lastContextDependency.next = contextItem;
693+
}
694+
return isPrimaryRenderer
695+
? select(context._currentValue)
696+
: select(context._currentValue2);
697+
}

0 commit comments

Comments
 (0)