在基于Expo的 React Native应用中,FlatList的每一项都包含一个输入字段
我有一个React Native Expo应用,在某个页面我用一个 FlatList 来展示产品列表。每个列表项都有一个输入框,虽然设计有点奇怪,但这是客户需要的。问题在于 FlatList 的回收机制,有时在页面底部的某个项的输入框上点击时,它会把焦点放到列表开头的某个项的输入框上,这种情况大多是随机发生,且只有在点击输入框时键盘尚未弹出时才会发生。这意味着用户不能点击列表底部的某个项并在其输入框中输入数量,因为焦点会跳到随机顶部项的输入框。我找到了一个修复方法,即禁用 FlatList 的回收,但这并不是真正的修复,因为随着项数增加,列表的速度会显著变慢。位于 FlatList 内的 removeClippedSubviews={false} 就是通过禁用项的回收来“修复”问题的那个方案。
下面是我的代码:
Catalogue.tsx
const listRef = useRef<FlatList<Product>>(null);
const renderProducts = () => {
return (
<FlatList
ref={listRef}
key={userHasRoleBusiness ? 'single' : 'double'}
data={products}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
numColumns={userHasRoleBusiness ? 1 : 2}
contentContainerStyle={flatListStyle}
ListHeaderComponent={renderHeader}
ListEmptyComponent={renderEmptyList}
onScroll={userHasRoleBusiness ? handleScroll : undefined}
scrollEventThrottle={16}
keyboardShouldPersistTaps="handled"
removeClippedSubviews={false}
/>
);
};
const renderItem = ({item}: { item: Product }) => {
return (
<View style={{width: userHasRoleBusiness ? '100%' : '50%', paddingHorizontal: 8}}>
<ProductItem
product={item}
isAdmin={userHasRoleAdmin}
isBusinessUser={userHasRoleBusiness}
onChangePrice={onChangePrice}
/>
</View>
);
};
下面是我的ProductItem组件现在的样子(它包含输入框):
boolean ProductItemProps {
product: Product;
isAdmin: boolean;
isBusinessUser: boolean;
onChangePrice: (product: Product, price: number) => Promise<void>;
};
const ProductItem = ({ product }: ProductItemProps) => {
const quantityInputRef = useRef<AddQuantityInputRef>(null);
return (
<AddQuantityInput ref={quantityInputRef} product={product} />
);
};
export default ProductItem;
// AddQuantityInput component
import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {Text, TextInput, View} from 'react-native';
import {theme} from '@src/constants';
import {Product} from '@src/types';
import {useAppDispatch, useAppSelector} from "@src/hooks";
import {removeFromCart, setProductQuantity} from "@src/store/slices/cartSlice";
type QuantityInputProps = {
product: Product;
removeDelayMs?: number; // In milliseconds
};
export type AddQuantityInputRef = {
focus: () => void;
};
const AddQuantityInput = forwardRef<AddQuantityInputRef, QuantityInputProps>(({product, removeDelayMs = 0}, ref) => {
const removeTimerRef = useRef<NodeJS.Timeout | null>(null);
const dispatch = useAppDispatch();
const quantityInCart = useAppSelector((state) =>
state.cartSlice.list.find(item => item.product.id === product.id)
)?.quantity ?? 0;
const [quantity, setQuantity] = useState(quantityInCart > 0 ? String(quantityInCart) : '');
const inputRef = useRef<TextInput>(null);
useImperativeHandle(ref, () => ({
focus: () => {
console.log("Product inside AddQuantity", product.name);
console.log("inputRef", inputRef);
inputRef.current?.focus()
}
}));
useEffect(() => {
setQuantity(quantityInCart > 0 ? String(quantityInCart) : '');
}, [quantityInCart]);
const handleQuantityChange = (text: string) => {
const cleaned = text.replace(/[^0-9]/g, '').slice(0, 3);
setQuantity(cleaned);
const qty = Number(cleaned);
if (removeTimerRef.current) {
clearTimeout(removeTimerRef.current);
removeTimerRef.current = null;
}
if (qty > 0) {
dispatch(setProductQuantity({product, quantity: qty}));
} else if (removeDelayMs > 0) {
removeTimerRef.current = setTimeout(() => {
dispatch(removeFromCart(product));
}, removeDelayMs);
} else {
dispatch(removeFromCart(product));
}
};
return (
<View style={{alignItems: 'center', marginHorizontal: 12, marginTop: 4}}>
<TextInput
ref={inputRef}
style={{
width: 56,
height: 36,
borderWidth: 1,
borderColor: quantity ? theme.colors.primary : '#ddd',
borderRadius: 8,
textAlign: 'center',
fontSize: 16,
fontWeight: '500',
padding: 0,
paddingHorizontal: 4
}}
value={quantity}
onChangeText={handleQuantityChange}
keyboardType="numeric"
maxLength={3}
placeholder="0"
cursorColor={theme.colors.primary}
/>
<Text style={{fontSize: 12, fontWeight: "400", marginTop: 4, textTransform: 'lowercase'}}>
{product.uom === "COUNT" ? "ct" : product.uom}
</Text>
</View>
);
});
export default AddQuantityInput;
解决方案
为每个列表项创建输入框是一种反模式,且会成为性能瓶颈,并且与FlatList的虚拟化行为和键盘输入焦点之间产生冲突。
客户需要的是为每个产品添加数量的能力。你可以在不牺牲性能、也不与React Native的工作方式对抗的前提下实现。
在每个产品列表中添加一个用于编辑数量的按钮,按下时它会打开BottomSheet,里面有输入框用于输入数量并相应更新。
通过这种方式,用户就可以为任意产品修改数量,并且你将只使用一个输入框,这是推荐的做法。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。