Unblock React Compiler for 4 components with deferred ref access (#11549)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tomasz Zawadzki
2026-08-27 17:07:34 +02:00
committed by GitHub
parent 296b74d781
commit b9bff931a3
5 changed files with 32 additions and 10 deletions
+7 -1
View File
@@ -47,7 +47,13 @@ export function SearchInput({
<TextField.Root>
<TextField.Icon icon={MagnifyingGlassIcon} />
<TextField.Input
inputRef={mergeRefs([internalRef, ref])}
/*
* Deferred into the callback: React Compiler only special-cases the
* `ref` prop, so a merged ref built during render and handed to
* `inputRef` reads as accessing a ref. `mergeRefs` already returns a
* fresh function per render, so this adds no identity churn.
*/
inputRef={node => mergeRefs([internalRef, ref])(node)}
label={label || l`Search`}
value={value}
placeholder={l`Search`}
+8 -5
View File
@@ -26,6 +26,7 @@ import {
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
const Context = createContext<{
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null> | null
@@ -101,11 +102,13 @@ export function Root({children, isInvalid = false, style}: RootProps) {
{zIndex: 0},
style,
]}
{...web({
onClick: () => inputRef.current?.focus(),
onMouseOver: onHoverIn,
onMouseOut: onHoverOut,
})}>
{...(IS_WEB
? {
onClick: () => inputRef.current?.focus(),
onMouseOver: onHoverIn,
onMouseOut: onHoverOut,
}
: {})}>
{children}
</View>
</Context.Provider>
+6 -1
View File
@@ -74,8 +74,13 @@ export function useDraggableScroll<
}
}, [cursor])
/*
* Deferred into the callback so the merge does not happen during the render of
* whichever component uses this hook - see SearchInput for the full reasoning.
*/
const refs = useMemo(
() => mergeRefs(outerRef ? [ref, outerRef] : [ref]),
() => (node: Scrollable | null) =>
mergeRefs(outerRef ? [ref, outerRef] : [ref])(node),
[ref, outerRef],
)
+1 -1
View File
@@ -31,7 +31,7 @@ function assignRef<T>(
* @returns The function `mergeRefs` is being returned. It takes an array of mutable or legacy refs and
* returns a ref callback function that can be used to merge multiple refs into a single ref.
*/
export function mergeRefs<T>(refs: (Ref<T> | undefined)[]): Ref<T> {
export function mergeRefs<T>(refs: (Ref<T> | undefined)[]): RefCallback<T> {
return (value: T | null) => {
const cleanups: (() => void)[] = []
@@ -1,4 +1,4 @@
import {useRef, useState} from 'react'
import {useEffect, useRef, useState} from 'react'
import {View} from 'react-native'
import PagerView from 'react-native-pager-view'
import {type PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
@@ -26,9 +26,17 @@ export function ValuePropositionPager({
if (step !== activePage) {
setActivePage(step)
ref.current?.setPage(step)
}
/*
* In an effect rather than inline above: driving the pager is a side effect,
* and reading a ref during render is a Rules of React violation. `initialPage`
* already covers the first render, so the mount run is a no-op.
*/
useEffect(() => {
ref.current?.setPage(step)
}, [step])
const images = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]]
return (