fix[devtools/useMemoCache]: implement a working copy of useMemoCache (#27659)
In https://github.com/facebook/react/pull/27472 I've removed broken `useMemoCache` implementation and replaced it with a stub. It actually produces errors when trying to inspect components, which are compiled with Forget. The main difference from the implementation in https://github.com/facebook/react/pull/26696 is that we are using corresponding `Fiber` here, which has patched `updateQueue` with `memoCache`. Previously we would check it on a hook object, which doesn't have `updateQueue`. Tested on pages, which are using Forget and by inspecting elements, which are transpiled with Forget.
Ruslan Lesiutin committed
Nov 14, 2023 at 18:23 UTC
aec521a96d3f1bebc2ba38553d14f4989c6e88e0
2 files changed
+97
-24
packages/react-debug-tools/src/ReactDebugHooks.js
+43
-7
@@ -26,6 +26,7 @@ import {
26
ContextProvider,
27
ForwardRef,
28
} from 'react-reconciler/src/ReactWorkTags';
29
+import {REACT_MEMO_CACHE_SENTINEL} from 'shared/ReactSymbols';
30
31
type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher;
32
@@ -93,7 +94,9 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
94
return primitiveStackCache;
95
}
96
97
+let currentFiber: null | Fiber = null;
98
let currentHook: null | Hook = null;
99
+
100
function nextHook(): null | Hook {
101
const hook = currentHook;
102
if (hook !== null) {
@@ -319,9 +322,31 @@ function useId(): string {
322
323
// useMemoCache is an implementation detail of Forget's memoization
324
// it should not be called directly in user-generated code
322
-// we keep it as a stub for dispatcher
325
function useMemoCache(size: number): Array<any> {
324
- return [];
326
+ const fiber = currentFiber;
327
+ // Don't throw, in case this is called from getPrimitiveStackCache
328
+ if (fiber == null) {
329
+ return [];
330
+ }
331
+
332
+ // $FlowFixMe[incompatible-use]: updateQueue is mixed
333
+ const memoCache = fiber.updateQueue?.memoCache;
334
+ if (memoCache == null) {
335
+ return [];
336
+ }
337
+
338
+ let data = memoCache.data[memoCache.index];
339
+ if (data === undefined) {
340
+ data = memoCache.data[memoCache.index] = new Array(size);
341
+ for (let i = 0; i < size; i++) {
342
+ data[i] = REACT_MEMO_CACHE_SENTINEL;
343
+ }
344
+ }
345
+
346
+ // We don't write anything to hookLog on purpose, so this hook remains invisible to users.
347
+
348
+ memoCache.index++;
349
+ return data;
350
}
351
352
const Dispatcher: DispatcherType = {
@@ -699,9 +724,11 @@ export function inspectHooks<Props>(
724
}
725
726
const previousDispatcher = currentDispatcher.current;
702
- let readHookLog;
727
currentDispatcher.current = DispatcherProxy;
728
+
729
+ let readHookLog;
730
let ancestorStackError;
731
+
732
try {
733
ancestorStackError = new Error();
734
renderFunction(props);
@@ -798,19 +825,25 @@ export function inspectHooksOfFiber(
825
'Unknown Fiber. Needs to be a function component to inspect hooks.',
826
);
827
}
828
+
829
// Warm up the cache so that it doesn't consume the currentHook.
830
getPrimitiveStackCache();
831
+
832
+ // Set up the current hook so that we can step through and read the
833
+ // current state from them.
834
+ currentHook = (fiber.memoizedState: Hook);
835
+ currentFiber = fiber;
836
+
837
const type = fiber.type;
838
let props = fiber.memoizedProps;
839
if (type !== fiber.elementType) {
840
props = resolveDefaultProps(type, props);
841
}
808
- // Set up the current hook so that we can step through and read the
809
- // current state from them.
810
- currentHook = (fiber.memoizedState: Hook);
811
- const contextMap = new Map<ReactContext<$FlowFixMe>, $FlowFixMe>();
842
+
843
+ const contextMap = new Map<ReactContext<any>, any>();
844
try {
845
setupContexts(contextMap, fiber);
846
+
847
if (fiber.tag === ForwardRef) {
848
return inspectHooksOfForwardRef(
849
type.render,
@@ -820,9 +853,12 @@ export function inspectHooksOfFiber(
853
includeHooksSource,
854
);
855
}
856
+
857
return inspectHooks(type, props, currentDispatcher, includeHooksSource);
858
} finally {
859
+ currentFiber = null;
860
currentHook = null;
861
+
862
restoreContexts(contextMap);
863
}
864
}
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+54
-17
@@ -633,27 +633,64 @@ describe('ReactHooksInspectionIntegration', () => {
633
});
634
});
635
636
- // @gate enableUseMemoCacheHook
637
- it('useMemoCache should not be inspectable', () => {
638
- function Foo() {
639
- const $ = useMemoCache(1);
640
- let t0;
641
-
642
- if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
643
- t0 = <div>{1}</div>;
644
- $[0] = t0;
645
- } else {
646
- t0 = $[0];
636
+ describe('useMemoCache', () => {
637
+ // @gate enableUseMemoCacheHook
638
+ it('should not be inspectable', () => {
639
+ function Foo() {
640
+ const $ = useMemoCache(1);
641
+ let t0;
642
+
643
+ if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
644
+ t0 = <div>{1}</div>;
645
+ $[0] = t0;
646
+ } else {
647
+ t0 = $[0];
648
+ }
649
+
650
+ return t0;
651
}
652
649
- return t0;
650
- }
653
+ const renderer = ReactTestRenderer.create(<Foo />);
654
+ const childFiber = renderer.root.findByType(Foo)._currentFiber();
655
+ const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
656
652
- const renderer = ReactTestRenderer.create(<Foo />);
653
- const childFiber = renderer.root.findByType(Foo)._currentFiber();
654
- const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
657
+ expect(tree.length).toEqual(0);
658
+ });
659
+
660
+ // @gate enableUseMemoCacheHook
661
+ it('should work in combination with other hooks', () => {
662
+ function useSomething() {
663
+ const [something] = React.useState(null);
664
+ const changeOtherSomething = React.useCallback(() => {}, [something]);
665
+
666
+ return [something, changeOtherSomething];
667
+ }
668
656
- expect(tree.length).toEqual(0);
669
+ function Foo() {
670
+ const $ = useMemoCache(10);
671
+
672
+ useSomething();
673
+ React.useState(1);
674
+ React.useEffect(() => {});
675
+
676
+ let t0;
677
+
678
+ if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
679
+ t0 = <div>{1}</div>;
680
+ $[0] = t0;
681
+ } else {
682
+ t0 = $[0];
683
+ }
684
+
685
+ return t0;
686
+ }
687
+
688
+ const renderer = ReactTestRenderer.create(<Foo />);
689
+ const childFiber = renderer.root.findByType(Foo)._currentFiber();
690
+ const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
691
+
692
+ expect(tree.length).toEqual(3);
693
+ });
694
});
695
696
describe('useDebugValue', () => {