From ecb40417b8986ca2cdf8d17db982226a39d71c49 Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Thu, 14 Nov 2024 12:48:45 -0800 Subject: [PATCH 1/2] refactor(fiber.ts, overlay.ts, types.ts, utils.ts): Enhance change detection in components --- src/fiber.ts | 187 +++++++++++++++++++++++++++++-------------------- src/overlay.ts | 33 +++------ src/types.ts | 2 +- src/utils.ts | 19 +++++ 4 files changed, 140 insertions(+), 101 deletions(-) diff --git a/src/fiber.ts b/src/fiber.ts index 7d5576d7..fa8859ae 100644 --- a/src/fiber.ts +++ b/src/fiber.ts @@ -1,5 +1,6 @@ import type { Fiber, FiberRoot } from 'react-reconciler'; -import { NO_OP } from './utils'; +import * as React from 'react'; +import { didChange, NO_OP } from './utils'; import type { Renderer } from './types'; const PerformedWorkFlag = 0b01; @@ -10,6 +11,11 @@ const ForwardRefTag = 11; const MemoComponentTag = 14; const SimpleMemoComponentTag = 15; +const ReactSharedInternals = + (React as any) + ?.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE || + (React as any)?.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + export const didFiberRender = (fiber: Fiber | null): boolean => { if (!fiber) return true; // mount (probably) const prevProps = fiber.alternate?.memoizedProps || {}; @@ -129,83 +135,110 @@ export const registerDevtoolsHook = ({ onCommitFiberRoot(rendererID, root); }; - // const renderersArray = Array.from(devtoolsHook.renderers.values()); - // for (let i = 0, len = renderersArray.length; i < len; i++) { - // const renderer = renderersArray[i]; - // controlDispatcherRef(renderer.currentDispatcherRef); - // } + if (ReactSharedInternals) { + controlDispatcherRef(ReactSharedInternals); + } return devtoolsHook; }; -// TODO: check useMemo / useCallback / useMemoCache (React Compiler) - -// const REACT_MAJOR_VERSION = Number(React.version.split('.')[0]); -// const dispatcherRefs = new Set(); - -// export const controlDispatcherRef = (currentDispatcherRef: any) => { -// const ref = currentDispatcherRef; -// if (ref && !dispatcherRefs.has(ref)) { -// // Renamed to ".H" in React 19 -// const propName = REACT_MAJOR_VERSION > 18 ? 'H' : 'current'; -// let currentDispatcher = ref[propName]; -// const seenDispatchers = new Set(); - -// Object.defineProperty(ref, propName, { -// get: () => currentDispatcher, -// set(current: any) { -// currentDispatcher = current; - -// if ( -// !current || -// seenDispatchers.has(current) || -// current.useRef === current.useImperativeHandle || -// /warnInvalidContextAccess\(\)/.test(current.readContext.toString()) -// ) { -// return; -// } -// seenDispatchers.add(current); -// const isInComponent = peekIsInComponent(current); -// if (!isInComponent) return; -// const prevUseCallback = current.useCallback; -// const useCallback = (fn: (...args: any[]) => any, deps: any[]) => { -// return prevUseCallback(fn, deps); -// }; -// current.useCallback = useCallback; - -// const prevUseMemo = current.useMemo; -// const useMemo = (fn: (...args: any[]) => any, deps: any[]) => { -// return prevUseMemo(fn, deps); -// }; -// current.useMemo = useMemo; -// }, -// }); -// dispatcherRefs.add(ref); -// } -// }; - -// const invalidHookErrFunctions = new WeakMap<() => void, boolean>(); - -// /** -// * Check if you can currently run hooks in a component. This avoids allocting -// * a new hook on the stack by "peeking." Note that this doesn't correctly handle some cases -// * For example, if you are iterating through Array.map, it won't check if you allocate more/less hooks between renders -// * -// * This function checks the current dispatcher, which is swapped with an invalid / valid state by React. If -// * the current dispatcher is invalid (includes the string ("Error")), it will return false. -// */ -// export const peekIsInComponent = ( -// dispatcher: Record void>, -// ): boolean => { -// const hook = dispatcher.useRef; - -// if (typeof hook !== 'function' || invalidHookErrFunctions.has(hook)) { -// return false; -// } -// const str = hook.toString(); -// if (str.includes('Error')) { -// invalidHookErrFunctions.set(hook, true); -// return false; -// } -// return true; -// }; +const REACT_MAJOR_VERSION = Number(React.version.split('.')[0]); +const dispatcherRefs = new Set(); + +export const controlDispatcherRef = (currentDispatcherRef: any) => { + const ref = currentDispatcherRef; + if (ref && !dispatcherRefs.has(ref)) { + // Renamed to ".H" in React 19 + const propName = REACT_MAJOR_VERSION > 18 ? 'H' : 'current'; + let currentDispatcher = ref[propName]; + const seenDispatchers = new Set(); + + const callbackCache = new Map(); + const memoCache = new Map(); + + Object.defineProperty(ref, propName, { + get: () => currentDispatcher, + set(current: any) { + currentDispatcher = current; + + if ( + !current || + seenDispatchers.has(current) || + current.useRef === current.useImperativeHandle || + /warnInvalidContextAccess\(\)/.test(current.readContext.toString()) + ) { + return; + } + seenDispatchers.add(current); + const isInComponent = peekIsInComponent(current); + if (!isInComponent) return; + const prevUseCallback = current.useCallback; + const useCallback = (fn: (...args: any[]) => any, deps: any[]) => { + try { + const key = fn.toString(); + const prevDeps = callbackCache.get(key); + if (prevDeps && prevDeps.length === deps.length) { + for (let i = 0; i < prevDeps.length; i++) { + const changed = didChange(prevDeps[i], deps[i]); + if (!changed) break; + // do something + } + } + callbackCache.set(key, deps); + } catch (_err) { + /**/ + } + return prevUseCallback(fn, deps); + }; + current.useCallback = useCallback; + + const prevUseMemo = current.useMemo; + const useMemo = (fn: (...args: any[]) => any, deps: any[]) => { + try { + const key = fn.toString(); + const prevDeps = callbackCache.get(key); + if (prevDeps && prevDeps.length === deps.length) { + for (let i = 0; i < prevDeps.length; i++) { + const changed = didChange(prevDeps[i], deps[i]); + if (!changed) break; + // do something + } + } + memoCache.set(key, deps); + } catch (_err) { + /**/ + } + return prevUseMemo(fn, deps); + }; + current.useMemo = useMemo; + }, + }); + dispatcherRefs.add(ref); + } +}; + +const invalidHookErrFunctions = new WeakMap<() => void, boolean>(); + +/** + * Check if you can currently run hooks in a component. This avoids allocting + * a new hook on the stack by "peeking." Note that this doesn't correctly handle some cases + * For example, if you are iterating through Array.map, it won't check if you allocate more/less hooks between renders + * + * This function checks the current dispatcher, which is swapped with an invalid / valid state by React. If + * the current dispatcher is invalid (includes the string ("Error")), it will return false. + */ +export const peekIsInComponent = ( + dispatcher: Record void>, +): boolean => { + const hook = dispatcher.useRef; + + if (typeof hook !== 'function' || invalidHookErrFunctions.has(hook)) { + return false; + } + const str = hook.toString(); + if (str.includes('Error')) { + invalidHookErrFunctions.set(hook, true); + return false; + } + return true; +}; diff --git a/src/overlay.ts b/src/overlay.ts index e7496379..b479bf01 100644 --- a/src/overlay.ts +++ b/src/overlay.ts @@ -7,13 +7,8 @@ import { getType, traverseFiber, } from './fiber'; -import type { - OutlineLabel, - Outline, - ChangedProp, - OutlinePaintTask, -} from './types'; -import { onIdle, fastSerialize } from './utils'; +import type { OutlineLabel, Outline, Change, OutlinePaintTask } from './types'; +import { onIdle, didChange } from './utils'; import { getCurrentOptions } from './auto'; import { MONO_FONT, PURPLE_RGB } from './constants'; @@ -58,8 +53,7 @@ export const getOutline = (fiber: Fiber | null): Outline | null => { const type = getType(fiber.type); if (!type) return null; - const changedProps: ChangedProp[] = []; - const unstableTypes = ['function', 'object']; + const changedProps: Change[] = []; let unstable = false; const prevProps = fiber.alternate?.memoizedProps; @@ -69,32 +63,25 @@ export const getOutline = (fiber: Fiber | null): Outline | null => { const prevValue = prevProps?.[propName]; const nextValue = nextProps?.[propName]; + const changed = didChange(prevValue, nextValue); + if ( - Object.is(prevValue, nextValue) || + !changed || React.isValidElement(prevValue) || React.isValidElement(nextValue) || propName === 'children' ) { continue; } - const changedProp: ChangedProp = { + const changedProp: Change = { name: propName, prevValue, nextValue, - unstable: false, + unstable: changed === 'unstable', }; changedProps.push(changedProp); - const prevValueString = fastSerialize(prevValue); - const nextValueString = fastSerialize(nextValue); - - if ( - !unstableTypes.includes(typeof prevValue) || - !unstableTypes.includes(typeof nextValue) || - prevValueString !== nextValueString - ) { - continue; - } + if (changed !== 'unstable') continue; unstable = true; changedProp.unstable = true; @@ -413,7 +400,7 @@ export const createFullscreenCanvas = () => { export const createStatus = () => { const status = createElement( - ``, + ``, ) as HTMLDivElement; let isHidden = localStorage.getItem('react-scan-hidden') === 'true'; diff --git a/src/types.ts b/src/types.ts index b3aef71f..3eadc92b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,7 +46,7 @@ export interface OutlineLabel { text: string | null; } -export interface ChangedProp { +export interface Change { name: string; prevValue: any; nextValue: any; diff --git a/src/utils.ts b/src/utils.ts index 2f37cb28..1dcd0906 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -80,3 +80,22 @@ export const isProd = () => { export const NO_OP = () => { /**/ }; + +const unstableTypes = ['function', 'object']; + +export const didChange = (prevValue: any, nextValue: any) => { + if (Object.is(prevValue, nextValue)) return false; + + const prevValueString = fastSerialize(prevValue); + const nextValueString = fastSerialize(nextValue); + + if ( + !unstableTypes.includes(typeof prevValue) || + !unstableTypes.includes(typeof nextValue) || + prevValueString !== nextValueString + ) { + return true; + } + + return 'unstable'; +}; From c4c79cf4160e67ef355f69788ce2a668f599df6c Mon Sep 17 00:00:00 2001 From: Aiden Bai Date: Thu, 14 Nov 2024 17:58:07 -0800 Subject: [PATCH 2/2] refactor(fiber): Update getFiberDEV function logic --- src/fiber.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/fiber.ts b/src/fiber.ts index fa8859ae..b37672eb 100644 --- a/src/fiber.ts +++ b/src/fiber.ts @@ -145,6 +145,13 @@ export const registerDevtoolsHook = ({ const REACT_MAJOR_VERSION = Number(React.version.split('.')[0]); const dispatcherRefs = new Set(); +export const getFiberDEV = (): Fiber | null => { + return ( + ReactSharedInternals?.A?.getOwner() ?? + ReactSharedInternals?.ReactCurrentOwner?.current + ); +}; + export const controlDispatcherRef = (currentDispatcherRef: any) => { const ref = currentDispatcherRef; if (ref && !dispatcherRefs.has(ref)) {