From f298a4ef5e400a758bce5223c17b7bdf98994674 Mon Sep 17 00:00:00 2001 From: Tomasz Zawadzki Date: Thu, 27 Aug 2026 17:10:10 +0200 Subject: [PATCH] Unblock React Compiler for 6 components by removing render-phase ref access (#11544) Co-authored-by: Claude Opus 5 (1M context) --- src/components/ProgressGuide/FollowDialog.tsx | 10 ++------- src/components/WhoCanReply.tsx | 21 +++++-------------- .../contacts/components/OTPInput.tsx | 8 +++++-- src/lib/hooks/useAnimatedValue.ts | 16 +++++++------- src/lib/react-query.tsx | 6 +++--- src/state/messages/message-drafts.tsx | 7 +++---- 6 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 1ab2668ae5..5e6a3a97ca 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -270,10 +270,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { // Track seen profiles const seenProfilesRef = useRef>(new Set()) - const itemsRef = useRef(items) - itemsRef.current = items - const selectedInterestRef = useRef(selectedInterest) - selectedInterestRef.current = selectedInterest const onViewableItemsChanged = useNonReactiveCallback( ({viewableItems}: {viewableItems: ViewToken[]}) => { @@ -282,7 +278,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { if (item.type === 'profile') { if (!seenProfilesRef.current.has(item.profile.did)) { seenProfilesRef.current.add(item.profile.did) - const position = itemsRef.current.findIndex( + const position = items.findIndex( i => i.type === 'profile' && i.profile.did === item.profile.did, ) ax.metric('suggestedUser:seen', { @@ -292,9 +288,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { position: position !== -1 ? position : 0, suggestedDid: item.profile.did, category: - selectedInterestRef.current === FOR_YOU_TAB - ? null - : selectedInterestRef.current, + selectedInterest === FOR_YOU_TAB ? null : selectedInterest, }) } } diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx index 2202ed8e33..280cbd0cbb 100644 --- a/src/components/WhoCanReply.tsx +++ b/src/components/WhoCanReply.tsx @@ -1,11 +1,5 @@ import {Fragment, useMemo, useRef} from 'react' -import { - Keyboard, - Platform, - type StyleProp, - View, - type ViewStyle, -} from 'react-native' +import {Keyboard, type StyleProp, View, type ViewStyle} from 'react-native' import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -32,7 +26,7 @@ import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Gr import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' -import {IS_NATIVE} from '#/env' +import {IS_NATIVE, IS_WEB} from '#/env' import {app} from '#/lexicons' import * as bsky from '#/types/bsky' @@ -110,14 +104,9 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { } onPress={onPressOpen} {...(isThreadAuthor - ? Platform.select({ - web: { - onHoverIn: prefetch, - }, - native: { - onPressIn: prefetch, - }, - }) + ? IS_WEB + ? {onHoverIn: prefetch} + : {onPressIn: prefetch} : {})} hitSlop={HITSLOP_10}> {({hovered, focused, pressed}) => ( diff --git a/src/components/contacts/components/OTPInput.tsx b/src/components/contacts/components/OTPInput.tsx index ef8944a575..94151a8257 100644 --- a/src/components/contacts/components/OTPInput.tsx +++ b/src/components/contacts/components/OTPInput.tsx @@ -9,7 +9,7 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {mergeRefs} from '#/lib/merge-refs' -import {atoms as a, ios, platform, useTheme} from '#/alf' +import {atoms as a, platform, useTheme} from '#/alf' import {useInteractionState} from '#/components/hooks/useInteractionState' import {Text} from '#/components/Typography' import {IS_ANDROID, IS_IOS} from '#/env' @@ -94,7 +94,11 @@ export function OTPInput({ setTimeout(() => innerRef.current?.focus(), 100))} + onLayout={ + IS_IOS + ? () => setTimeout(() => innerRef.current?.focus(), 100) + : undefined + } autoFocus={IS_ANDROID} accessible accessibilityLabel={label} diff --git a/src/lib/hooks/useAnimatedValue.ts b/src/lib/hooks/useAnimatedValue.ts index eb567c02b1..eb23051d23 100644 --- a/src/lib/hooks/useAnimatedValue.ts +++ b/src/lib/hooks/useAnimatedValue.ts @@ -1,12 +1,12 @@ -import {useRef} from 'react' +import {useState} from 'react' import {Animated} from 'react-native' export function useAnimatedValue(initialValue: number) { - const lazyRef = useRef(undefined) - - if (lazyRef.current === undefined) { - lazyRef.current = new Animated.Value(initialValue) - } - - return lazyRef.current + /* + * A lazy `useState` initialiser rather than a lazily-populated ref: both + * construct once and keep the same instance, but reading a ref during render + * is a Rules of React violation. + */ + const [value] = useState(() => new Animated.Value(initialValue)) + return value } diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index adfa483677..f2c101d2b8 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -1,4 +1,4 @@ -import {useEffect, useRef, useState} from 'react' +import {useEffect, useState} from 'react' import {AppState, type AppStateStatus} from 'react-native' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' import { @@ -168,8 +168,8 @@ function QueryProviderInner({ children: React.ReactNode currentDid: string | undefined }) { - const initialDid = useRef(currentDid) - if (currentDid !== initialDid.current) { + const [initialDid] = useState(currentDid) + if (currentDid !== initialDid) { throw Error( 'Something is very wrong. Expected did to be stable due to key above.', ) diff --git a/src/state/messages/message-drafts.tsx b/src/state/messages/message-drafts.tsx index bc5ecbca98..6f2b1122df 100644 --- a/src/state/messages/message-drafts.tsx +++ b/src/state/messages/message-drafts.tsx @@ -2,9 +2,9 @@ import { createContext, useContext, useEffect, + useEffectEvent, useMemo, useReducer, - useRef, } from 'react' import {useCurrentConvoId} from './current-convo-id' @@ -44,8 +44,7 @@ export function useMessageDraft() { export function useSaveMessageDraft(message: string) { const {currentConvoId} = useCurrentConvoId() const {dispatch} = useMessageDraftsContext() - const messageRef = useRef(message) - messageRef.current = message + const getMessage = useEffectEvent(() => message) useEffect(() => { return () => { @@ -53,7 +52,7 @@ export function useSaveMessageDraft(message: string) { dispatch({ type: 'set', convoId: currentConvoId, - draft: messageRef.current, + draft: getMessage(), }) } }