diff --git a/CLAUDE.md b/CLAUDE.md index d90cbf7cb3..7212eb6165 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -288,8 +288,11 @@ existing usages across the app for a canonical example. ### TextField Compound component at `#/components/forms/TextField` (`TextField.LabelText`, -`TextField.Root`, `TextField.Icon`, `TextField.Input`). Prefer `defaultValue` over -`value` (see Footguns). +`TextField.Root`, `TextField.Icon`, `TextField.Input`). Controlled inputs +(`value` + `onChangeText`) are fine and are usually what you want - the old +advice to reach for `defaultValue` was a New Architecture migration concern and +no longer applies. Reach for `defaultValue` only when nothing outside the input +needs to read the text. ### Typography @@ -507,24 +510,6 @@ This applies to: The Menu component on iOS specifically uses this pattern – see `src/components/Menu/index.tsx:151`. -### Controlled vs Uncontrolled Inputs - -Prefer `defaultValue` over `value` for TextInput on the old architecture: - -```tsx -// Preferred - uncontrolled - - -// Avoid when possible - controlled (can cause performance issues) - -``` - ### Platform-Specific Behavior Some components behave differently across platforms: diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx index 5e6a3a97ca..29649d8ab8 100644 --- a/src/components/ProgressGuide/FollowDialog.tsx +++ b/src/components/ProgressGuide/FollowDialog.tsx @@ -139,7 +139,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { const [searchText, setSearchText] = useState(lastSearchText) const moderationOpts = useModerationOpts() const listRef = useRef(null) - const inputRef = useRef>(null) const [headerHeight, setHeaderHeight] = useState(0) const {currentAccount} = useSession() @@ -305,7 +304,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { const onSelectTab = useCallback( (interest: string) => { setSelectedInterest(interest) - inputRef.current?.clear() setSearchText('') listRef.current?.scrollToOffset({ offset: 0, @@ -318,7 +316,6 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) { const listHeader = (
| null> listRef: React.RefObject onSelectTab: (v: string) => void searchText: string @@ -396,8 +391,7 @@ let Header = ({ { setSearchText(text) listRef.current?.scrollToOffset({offset: 0, animated: false}) @@ -668,13 +662,11 @@ function CardOuter({ function SearchInput({ onChangeText, onEscape, - inputRef, - defaultValue, + value, }: { onChangeText: (text: string) => void onEscape: () => void - inputRef: React.RefObject | null> - defaultValue: string + value: string }) { const t = useTheme() const {t: l} = useLingui() @@ -698,9 +690,8 @@ function SearchInput({ fill={interacted ? t.palette.primary_500 : t.palette.contrast_300} /> { innerRef.current?.focus() - innerRef.current?.clear() + /* + * Clear through state, not the input's imperative `clear()`: the digit + * row above renders from `value`, so a native-only clear would leave + * stale digits on screen. + */ + onChange('') }}> {[...value.padEnd(numberOfDigits, ' ')].map((digit, index) => { diff --git a/src/components/dialogs/EmailDialog/screens/Update.tsx b/src/components/dialogs/EmailDialog/screens/Update.tsx index 0261e48c75..d537f7919c 100644 --- a/src/components/dialogs/EmailDialog/screens/Update.tsx +++ b/src/components/dialogs/EmailDialog/screens/Update.tsx @@ -218,12 +218,9 @@ export function Update(_props: ScreenProps) { & { label: string - /** - * @deprecated Controlled inputs are *strongly* discouraged. Use `defaultValue` instead where possible. - * - * See https://github.com/facebook/react-native-website/pull/4247 - * - * Note: This guidance no longer applies once we migrate to the New Architecture! - */ value?: string onChangeText?: (value: string) => void isInvalid?: boolean diff --git a/src/features/gifPicker/GifPickerDialog.tsx b/src/features/gifPicker/GifPickerDialog.tsx index 02cb8ced3e..14e1a08e13 100644 --- a/src/features/gifPicker/GifPickerDialog.tsx +++ b/src/features/gifPicker/GifPickerDialog.tsx @@ -115,7 +115,6 @@ function GifPickerBody({ }, [effectiveSearch, isRecentsActive]) const onClearSearch = () => { - textInputRef.current?.clear() setRawSearch('') setActiveCategory('trending') textInputRef.current?.focus() @@ -148,9 +147,9 @@ function GifPickerBody({ <> 0} onEscape={() => control.close()} /> {showPills && ( diff --git a/src/features/gifPicker/components/GifPickerHeader.tsx b/src/features/gifPicker/components/GifPickerHeader.tsx index 62dfea5eb9..c6ab1ba725 100644 --- a/src/features/gifPicker/components/GifPickerHeader.tsx +++ b/src/features/gifPicker/components/GifPickerHeader.tsx @@ -10,16 +10,16 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' export function GifPickerHeader({ inputRef, + value, onChangeText, onClear, onEscape, - canClear, }: { inputRef: Ref> + value: string onChangeText: (text: string) => void onClear: () => void onEscape: () => void - canClear: boolean }) { const {t: l} = useLingui() const t = useTheme() @@ -47,6 +47,7 @@ export function GifPickerHeader({ comment: 'Placeholder text inside the GIF search input. KLIPY is the third-party GIF provider; keep the brand name as-is.', })} + value={value} onChangeText={onChangeText} returnKeyType="search" inputRef={inputRef} @@ -57,7 +58,7 @@ export function GifPickerHeader({ } }} /> - {canClear && ( + {value.length > 0 && (