From 98c570e3b80155481c80e054d5f2c31ee1324fb7 Mon Sep 17 00:00:00 2001 From: LESANF Date: Mon, 28 Sep 2026 16:44:41 +0900 Subject: [PATCH 1/3] Stop remounting the app when toggling Touchables in the Element Inspector --- .../Pressability/PressabilityDebug.js | 28 +++- .../__tests__/PressabilityDebug-itest.js | 137 ++++++++++++++++++ .../Libraries/ReactNative/AppContainer-dev.js | 13 +- packages/react-native/Libraries/Text/Text.js | 29 +++- .../devmenu/elementinspector/Inspector.js | 6 +- 5 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 packages/react-native/Libraries/Pressability/__tests__/PressabilityDebug-itest.js diff --git a/packages/react-native/Libraries/Pressability/PressabilityDebug.js b/packages/react-native/Libraries/Pressability/PressabilityDebug.js index 90b83e49e51c..d4c42daef71c 100644 --- a/packages/react-native/Libraries/Pressability/PressabilityDebug.js +++ b/packages/react-native/Libraries/Pressability/PressabilityDebug.js @@ -36,8 +36,9 @@ type Props = Readonly<{ * */ export function PressabilityDebugView(props: Props): React.Node { + const enabled = useIsEnabled(); if (__DEV__) { - if (isEnabled()) { + if (enabled) { const normalizedColor = normalizeColor(props.color); if (typeof normalizedColor !== 'number') { return null; @@ -70,6 +71,7 @@ export function PressabilityDebugView(props: Props): React.Node { } let isDebugEnabled = false; +const listeners: Set<() => void> = new Set(); export function isEnabled(): boolean { if (__DEV__) { @@ -80,6 +82,30 @@ export function isEnabled(): boolean { export function setEnabled(value: boolean): void { if (__DEV__) { + if (isDebugEnabled === value) { + return; + } isDebugEnabled = value; + listeners.forEach(listener => listener()); } } + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} + +function useIsEnabledDev(): boolean { + return React.useSyncExternalStore(subscribe, isEnabled); +} + +/** + * Like `isEnabled`, but re-renders the calling component when the value + * changes, so toggling it does not require remounting the app. Outside of + * `__DEV__` it is `isEnabled` and uses no hooks. + */ +export const useIsEnabled: () => boolean = __DEV__ + ? useIsEnabledDev + : isEnabled; diff --git a/packages/react-native/Libraries/Pressability/__tests__/PressabilityDebug-itest.js b/packages/react-native/Libraries/Pressability/__tests__/PressabilityDebug-itest.js new file mode 100644 index 000000000000..25bc6a3aa1e5 --- /dev/null +++ b/packages/react-native/Libraries/Pressability/__tests__/PressabilityDebug-itest.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import * as PressabilityDebug from '../PressabilityDebug'; +import * as Fantom from '@react-native/fantom'; +import * as React from 'react'; +import {useEffect} from 'react'; +import {Pressable, Text} from 'react-native'; + +describe('PressabilityDebug', () => { + beforeEach(() => { + Fantom.runTask(() => { + PressabilityDebug.setEnabled(false); + }); + }); + + afterEach(() => { + Fantom.runTask(() => { + PressabilityDebug.setEnabled(false); + }); + }); + + it('shows press targets when enabled, without remounting', () => { + const root = Fantom.createRoot(); + let mountCount = 0; + + function Screen() { + useEffect(() => { + mountCount++; + }, []); + return ; + } + + Fantom.runTask(() => { + root.render(); + }); + + expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(); + + Fantom.runTask(() => { + PressabilityDebug.setEnabled(true); + }); + + expect(root.getRenderedOutput({props: []}).toJSX()).toEqual( + + + , + ); + + Fantom.runTask(() => { + PressabilityDebug.setEnabled(false); + }); + + expect(root.getRenderedOutput({props: []}).toJSX()).toEqual(); + expect(mountCount).toBe(1); + }); + + it('colors pressable text when enabled', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( {}}>text); + }); + + expect( + root.getRenderedOutput({props: ['foregroundColor']}).toJSX(), + ).toEqual( + text, + ); + + Fantom.runTask(() => { + PressabilityDebug.setEnabled(true); + }); + + expect( + root.getRenderedOutput({props: ['foregroundColor']}).toJSX(), + ).toEqual( + text, + ); + }); + + it('colors nested pressable text when enabled', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render( + + {}}>nested + , + ); + }); + + Fantom.runTask(() => { + PressabilityDebug.setEnabled(true); + }); + + expect( + root.getRenderedOutput({props: ['foregroundColor']}).toJSX(), + ).toEqual( + + nested + , + ); + }); + + it('does not color disabled text', () => { + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + PressabilityDebug.setEnabled(true); + }); + + Fantom.runTask(() => { + root.render( + {}}> + text + , + ); + }); + + expect( + root.getRenderedOutput({props: ['foregroundColor']}).toJSX(), + ).toEqual( + text, + ); + }); +}); diff --git a/packages/react-native/Libraries/ReactNative/AppContainer-dev.js b/packages/react-native/Libraries/ReactNative/AppContainer-dev.js index a0abc38caec4..a9457cf09b36 100644 --- a/packages/react-native/Libraries/ReactNative/AppContainer-dev.js +++ b/packages/react-native/Libraries/ReactNative/AppContainer-dev.js @@ -27,7 +27,7 @@ import {RootTagContext, createRootTag} from './RootTag'; import * as React from 'react'; import {useRef} from 'react'; -const {useEffect, useState, useCallback} = React; +const {useEffect, useState} = React; const reactDevToolsHook: ReactDevToolsGlobalHook = (window as $FlowFixMe) .__REACT_DEVTOOLS_GLOBAL_HOOK__; @@ -51,7 +51,6 @@ type ExternalInspection = { type InspectorDeferredProps = { inspectedViewRef: InspectedViewRef, - onInspectedViewRerenderRequest: () => void, reactDevToolsAgent?: ReactDevToolsAgent, devMenuInspectorOpen: boolean, externalInspection: ExternalInspection, @@ -59,7 +58,6 @@ type InspectorDeferredProps = { const InspectorDeferred = ({ inspectedViewRef, - onInspectedViewRerenderRequest, reactDevToolsAgent, devMenuInspectorOpen, externalInspection, @@ -72,7 +70,6 @@ const InspectorDeferred = ({ return ( (reactDevToolsHook?.reactDevtoolsAgent); @@ -157,7 +153,6 @@ const AppContainer = ({ {children} @@ -172,11 +167,6 @@ const AppContainer = ({ ); } - const onInspectedViewRerenderRequest = useCallback( - () => setKey(k => k + 1), - [], - ); - return ( = 0)) { @@ -485,10 +480,15 @@ component PressableVirtualText( const [isHighlighted, eventHandlersForText] = useTextPressability( textPressabilityProps, ); + const style = usePressabilityDebugStyle( + textProps.style, + textPressabilityProps.onPress, + ); return ( void, reactDevToolsAgent?: ReactDevToolsAgent, devMenuInspectorOpen: boolean, externalInspection: ExternalInspection, @@ -64,7 +63,6 @@ type Props = { function Inspector({ inspectedViewRef, - onRequestRerenderApp, reactDevToolsAgent, devMenuInspectorOpen, externalInspection, @@ -79,6 +77,7 @@ function Inspector({ const [selectionIndex, setSelectionIndex] = useState(null); const [elementsHierarchy, setElementsHierarchy] = useState(null); + const touchTargeting = PressabilityDebug.useIsEnabled(); // Derive inspecting state: external inspection forces it on, otherwise use local state const isInspecting = externalInspectingEnabled || inspectingEnabled; @@ -160,7 +159,6 @@ function Inspector({ const setTouchTargeting = (val: boolean) => { PressabilityDebug.setEnabled(val); - onRequestRerenderApp(); }; const panelContainerStyle = @@ -188,7 +186,7 @@ function Inspector({ hierarchy={elementsHierarchy} selection={selectionIndex} setSelection={setSelection} - touchTargeting={PressabilityDebug.isEnabled()} + touchTargeting={touchTargeting} setTouchTargeting={setTouchTargeting} /> From 8fc1b64ac042d0a5158210dd19d20d54f590ebc2 Mon Sep 17 00:00:00 2001 From: LESANF Date: Mon, 28 Sep 2026 19:18:43 +0900 Subject: [PATCH 2/3] Return false from useIsEnabled outside of DEV --- .../react-native/Libraries/Pressability/PressabilityDebug.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Pressability/PressabilityDebug.js b/packages/react-native/Libraries/Pressability/PressabilityDebug.js index d4c42daef71c..842844133f4d 100644 --- a/packages/react-native/Libraries/Pressability/PressabilityDebug.js +++ b/packages/react-native/Libraries/Pressability/PressabilityDebug.js @@ -104,8 +104,8 @@ function useIsEnabledDev(): boolean { /** * Like `isEnabled`, but re-renders the calling component when the value * changes, so toggling it does not require remounting the app. Outside of - * `__DEV__` it is `isEnabled` and uses no hooks. + * `__DEV__` it always returns `false` and uses no hooks. */ export const useIsEnabled: () => boolean = __DEV__ ? useIsEnabledDev - : isEnabled; + : () => false; From 2e0caa84a1607f7c63c62ee5239fc6f6e0150a72 Mon Sep 17 00:00:00 2001 From: LESANF Date: Mon, 28 Sep 2026 19:18:43 +0900 Subject: [PATCH 3/3] Stub usePressabilityDebugStyle outside of DEV --- packages/react-native/Libraries/Text/Text.js | 31 ++++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/react-native/Libraries/Text/Text.js b/packages/react-native/Libraries/Text/Text.js index 90fc64c07183..8885d64165da 100644 --- a/packages/react-native/Libraries/Text/Text.js +++ b/packages/react-native/Libraries/Text/Text.js @@ -466,6 +466,24 @@ function useTextPressability({ ); } +function usePressabilityDebugStyleDev( + style: ?TextStyleProp, + onPress: ?(event: GestureResponderEvent) => unknown, +): ?TextStyleProp { + const isDebugEnabled = PressabilityDebug.useIsEnabled(); + return isDebugEnabled && onPress != null + ? [style, {color: 'magenta'}] + : style; +} + +/** + * Colors pressable text when press targets are shown by the Inspector. + * Outside of `__DEV__` it returns the style unchanged and uses no hooks. + */ +const usePressabilityDebugStyle: typeof usePressabilityDebugStyleDev = __DEV__ + ? usePressabilityDebugStyleDev + : style => style; + /** * Wrap the NativeVirtualText component and initialize pressability. * @@ -533,19 +551,6 @@ component PressableText( ); } -/** - * Colors pressable text when press targets are shown by the Inspector. - */ -function usePressabilityDebugStyle( - style: NativeTextProps['style'], - onPress: TextPressabilityProps['onPress'], -): NativeTextProps['style'] { - const isDebugEnabled = PressabilityDebug.useIsEnabled(); - return isDebugEnabled && onPress != null - ? [style, {color: 'magenta'}] - : style; -} - const userSelectToSelectableMap = { auto: true, text: true,