Unblock React Compiler for 4 components with deferred ref access
React Compiler special-cases the `ref` prop, but a ref-derived value built
during render and passed anywhere else reads as accessing a ref. Deferring the
work into the callback keeps it out of render; mergeRefs already returned a
fresh function per render, so this adds no identity churn.
- SearchInput, useDraggableScrollView: defer the mergeRefs call
- merge-refs: return RefCallback<T> rather than Ref<T>, which is what it has
always returned, so the deferred call sites typecheck
- TextField: web({...}) hides a ref-reading handler from the compiler, so use a
plain IS_WEB conditional
- ValuePropositionPager: setPage moves out of render into an effect
Skipped components: 125 -> 121.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,13 @@ export function SearchInput({
|
|||||||
<TextField.Root>
|
<TextField.Root>
|
||||||
<TextField.Icon icon={MagnifyingGlassIcon} />
|
<TextField.Icon icon={MagnifyingGlassIcon} />
|
||||||
<TextField.Input
|
<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`}
|
label={label || l`Search`}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder={l`Search`}
|
placeholder={l`Search`}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {IS_WEB} from '#/env'
|
||||||
|
|
||||||
const Context = createContext<{
|
const Context = createContext<{
|
||||||
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null> | null
|
inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null> | null
|
||||||
@@ -101,11 +102,13 @@ export function Root({children, isInvalid = false, style}: RootProps) {
|
|||||||
{zIndex: 0},
|
{zIndex: 0},
|
||||||
style,
|
style,
|
||||||
]}
|
]}
|
||||||
{...web({
|
{...(IS_WEB
|
||||||
onClick: () => inputRef.current?.focus(),
|
? {
|
||||||
onMouseOver: onHoverIn,
|
onClick: () => inputRef.current?.focus(),
|
||||||
onMouseOut: onHoverOut,
|
onMouseOver: onHoverIn,
|
||||||
})}>
|
onMouseOut: onHoverOut,
|
||||||
|
}
|
||||||
|
: {})}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
|
|||||||
@@ -74,8 +74,13 @@ export function useDraggableScroll<
|
|||||||
}
|
}
|
||||||
}, [cursor])
|
}, [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(
|
const refs = useMemo(
|
||||||
() => mergeRefs(outerRef ? [ref, outerRef] : [ref]),
|
() => (node: Scrollable | null) =>
|
||||||
|
mergeRefs(outerRef ? [ref, outerRef] : [ref])(node),
|
||||||
[ref, outerRef],
|
[ref, outerRef],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 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.
|
* 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) => {
|
return (value: T | null) => {
|
||||||
const cleanups: (() => void)[] = []
|
const cleanups: (() => void)[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useRef, useState} from 'react'
|
import {useEffect, useRef, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import PagerView from 'react-native-pager-view'
|
import PagerView from 'react-native-pager-view'
|
||||||
import {type PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
|
import {type PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
|
||||||
@@ -26,9 +26,17 @@ export function ValuePropositionPager({
|
|||||||
|
|
||||||
if (step !== activePage) {
|
if (step !== activePage) {
|
||||||
setActivePage(step)
|
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]]
|
const images = [PROP_1[t.name], PROP_2[t.name], PROP_3[t.name]]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user