在基于Expo的 React Native应用中,FlatList的每一项都包含一个输入字段

移动开发 2026-07-10

我有一个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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章