英文:
TextInput keyboard flicker and unselects
问题
以下是翻译好的部分:
问题
问题出现在当另一个产品出现以添加到列表中时,选择输入(名称)时,键盘会迅速出现和消失,并且输入(名称)变为未选择状态。只有当我选择输入(数量)时,第二个产品的键盘会保持显示,然后我才能选择输入(名称)而不会闪烁。有人能告诉我为什么会发生这种情况吗?
重现错误的步骤
(注意:设置和运行项目的步骤在git存储库的自述文件中)
- 运行项目
- 选择任何商店并点击下一步
- 在输入(名称)中,键入与您选择的商店相关的products.json文件中的任何名称。例如,如果您选择了"Cyber Monkeys",则键入cat6
- 单击其他任何地方取消选择输入(名称),然后观察第二个产品表单出现在下方
- 在第二个产品表单中,选择输入(名称),然后观察屏幕闪烁
代码
Products.tsx
import React, { useEffect, useRef, useState } from "react";
import { useNavigation } from "@react-navigation/native";
import type { StackNavigationProp } from "@react-navigation/stack";
import type { StackMainParamList } from "@app/navigation/stacks";
import { useOrdersStore } from "@app/stores";
import { BtnMain, MainView } from "@app/components";
import { ProductsList, TopShop } from "./productsChildren";
import { copyByValue, screenNavigations } from "@app/utils";
import { OrderProduct, OrderProductNameValidationStatus } from "@app/types";
const newProduct: OrderProduct = {
validationNameStatus: "empty",
name: "",
qty: "1",
notes: "",
};
export default function Products(): JSX.Element {
// 变量
const refIsNextClicked = useRef(false);
const navigation = useNavigation<StackNavigationProp<StackMainParamList>>();
const { order, setProducts } = useOrdersStore((store) => store);
const [tempProducts, setTempProducts] = useState<Array<OrderProduct>>([
...order.products,
copyByValue(newProduct),
]);
// 函数
const unselectNameInput = async (
text: string,
index: number,
status: OrderProductNameValidationStatus
) => {
const updatedProducts: Array<OrderProduct> = copyByValue(tempProducts);
updatedProducts[index].name = text;
updatedProducts[index].validationNameStatus = status;
if (
status === "success" &&
updatedProducts[updatedProducts.length - 1].name.length > 0
) {
updatedProducts.push(copyByValue(newProduct));
}
setTempProducts(updatedProducts);
};
const unselectQtyInput = (text: string, index: number) => {
const updatedProducts: Array<OrderProduct> = copyByValue(tempProducts);
updatedProducts[index].qty = text;
setTempProducts(updatedProducts);
};
const removeProduct = (index: number) => {
setTempProducts(tempProducts.filter((p, i) => i !== index));
};
const handleNext = () => {
const validProducts = tempProducts.filter(
(p) => p.validationNameStatus === "success"
);
if (validProducts.length === 0) return;
refIsNextClicked.current = true;
setProducts(validProducts);
};
// 设置
useEffect(() => {
if (order.products.length > 0 && refIsNextClicked.current) {
navigation.navigate(screenNavigations.review.route as never);
}
}, [order.products]);
// 渲染
return (
<MainView>
<TopShop order={order} />
<ProductsList
shopId={order.shop?.id || 1}
products={tempProducts}
unselectNameInput={unselectNameInput}
unselectQtyInput={unselectQtyInput}
removeProduct={removeProduct}
/>
<BtnMain name="Next" onPress={handleNext} />
</MainView>
);
}
ProductsList.tsx
import React from "react";
import { SafeAreaView, FlatList, StyleSheet, StatusBar } from "react-native";
import { OrderProduct, OrderProductNameValidationStatus } from "@app/types";
import { ProductListItem } from "./productsListChildren";
interface Props {
shopId: number;
products: Array<OrderProduct>;
unselectNameInput: (
text: string,
index: number,
status: OrderProductNameValidationStatus
) => Promise<void>;
unselectQtyInput: (text: string, index: number) => void;
removeProduct: (index: number) => void;
}
export default function ProductsList(props: Props): JSX.Element {
const { products } = props;
return (
<SafeAreaView style={styles.container}>
<FlatList
data={products}
renderItem={({ item, index }) => (
<ProductListItem {...props} prodIndex={index} product={item} />
)}
keyExtractor={(item, index) => `${item.name}-${index}`}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
padding: 10,
},
});
ProductListItem.tsx
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { FontAwesome as Icon } from "@expo/vector-icons";
import { OrderProduct, OrderProductNameValidationStatus } from "@app/types";
import { NameInput, QtyInput } from "./productListItemChildren";
interface Props {
shopId: number;
prodIndex: number;
product: OrderProduct;
unselectNameInput: (
text: string,
index: number,
status: OrderProductNameValidationStatus
) => Promise<void>;
unselectQtyInput: (text: string, index: number) => void;
removeProduct: (index: number) => void;
}
export default function ProductListItem({
shopId,
prodIndex,
product,
unselectNameInput,
unselectQtyInput,
removeProduct,
}: Props): JSX.Element {
return (
<View style={{ marginBottom: 20, backgroundColor: "#ffffff" }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 10,
backgroundColor: "#f8f8f6",
borderBottomWidth: 1,
borderBottom
<details>
<summary>英文:</summary>
## The Bug ##
The issue is when another product appears for a 2nd one to add to the list, when selecting the input(name) the keyboard appears and disappears very quickly, and the input(name) becomes unselected. It is only when I select input(qty) for the 2nd product, they keyboard stays, then I can select the input(name) without the flickering. Can anyone tell me why this is happening?
[![enter image description here][1]][1]
**source code:** https://github.com/jamespagedev/react-native-expo-input-handling/tree/problem-second-product-name-input-flicker
# Steps to reproduce error #
(note: steps to setup and run project are in the readme of the git repository)
1. Run the project
2. Select any shop and click next
3. In the input(name), type in any name from the products.json file related to the shop you selected. For example, if you selected "Cyber Monkeys", type cat6
4. Unselect the input(name) by clicking anywhere else, and watch the 2nd product form appear below
5. In the 2nd product form, select the input(name) and watch the screen flicker
# The Code #
#### Products.tsx ####
import React, { useEffect, useRef, useState } from "react";
import { useNavigation } from "@react-navigation/native";
import type { StackNavigationProp } from "@react-navigation/stack";
import type { StackMainParamList } from "@app/navigation/stacks";
import { useOrdersStore } from "@app/stores";
import { BtnMain, MainView } from "@app/components";
import { ProductsList, TopShop } from "./productsChildren";
import { copyByValue, screenNavigations } from "@app/utils";
import { OrderProduct, OrderProductNameValidationStatus } from "@app/types";
const newProduct: OrderProduct = {
validationNameStatus: "empty",
name: "",
qty: "1",
notes: "",
};
export default function Products(): JSX.Element {
// variables
const refIsNextClicked = useRef(false);
const navigation = useNavigation<StackNavigationProp<StackMainParamList>>();
const { order, setProducts } = useOrdersStore((store) => store);
const [tempProducts, setTempProducts] = useState<Array<OrderProduct>>([
...order.products,
copyByValue(newProduct),
]);
// functions
const unselectNameInput = async (
text: string,
index: number,
status: OrderProductNameValidationStatus
) => {
const updatedProducts: Array<OrderProduct> = copyByValue(tempProducts);
updatedProducts[index].name = text;
updatedProducts[index].validationNameStatus = status;
if (
status === "success" &&
updatedProducts[updatedProducts.length - 1].name.length > 0
) {
updatedProducts.push(copyByValue(newProduct));
}
setTempProducts(updatedProducts);
};
const unselectQtyInput = (text: string, index: number) => {
const updatedProducts: Array<OrderProduct> = copyByValue(tempProducts);
updatedProducts[index].qty = text;
setTempProducts(updatedProducts);
};
const removeProduct = (index: number) => {
setTempProducts(tempProducts.filter((p, i) => i !== index));
};
const handleNext = () => {
const validProducts = tempProducts.filter(
(p) => p.validationNameStatus === "success"
);
if (validProducts.length === 0) return;
refIsNextClicked.current = true;
setProducts(validProducts);
};
// setup
useEffect(() => {
if (order.products.length > 0 && refIsNextClicked.current) {
navigation.navigate(screenNavigations.review.route as never);
}
}, [order.products]);
// render
return (
<MainView>
<TopShop order={order} />
<ProductsList
shopId={order.shop?.id || 1}
products={tempProducts}
unselectNameInput={unselectNameInput}
unselectQtyInput={unselectQtyInput}
removeProduct={removeProduct}
/>
<BtnMain name="Next" onPress={handleNext} />
</MainView>
);
}
#### ProductsList.tsx ####
import React from "react";
import { SafeAreaView, FlatList, StyleSheet, StatusBar } from "react-native";
import { OrderProduct, OrderProductNameValidationStatus } from "@app/types";
import { ProductListItem } from "./productsListChildren";
interface Props {
shopId: number;
products: Array<OrderProduct>;
unselectNameInput: (
text: string,
index: number,
status: OrderProductNameValidationStatus
) => Promise<void>;
unselectQtyInput: (text: string, index: number) => void;
removeProduct: (index: number) => void;
}
export default function ProductsList(props: Props): JSX.Element {
const { products } = props;
return (
<SafeAreaView style={styles.container}>
<FlatList
data={products}
renderItem={({ item, index }) => (
<ProductListItem {...props} prodIndex={index} product={item} />
)}
keyExtractor={(item, index) => ${item.name}-${index}
}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
padding: 10,
},
});
#### ProductListItem.tsx ####
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { FontAwesome as Icon } from "@expo/vector-icons";
import { OrderProduct, OrderProductNameValidationStatus } from "@app/types";
import { NameInput, QtyInput } from "./productListItemChildren";
interface Props {
shopId: number;
prodIndex: number;
product: OrderProduct;
unselectNameInput: (
text: string,
index: number,
status: OrderProductNameValidationStatus
) => Promise<void>;
unselectQtyInput: (text: string, index: number) => void;
removeProduct: (index: number) => void;
}
export default function ProductListItem({
shopId,
prodIndex,
product,
unselectNameInput,
unselectQtyInput,
removeProduct,
}: Props): JSX.Element {
return (
<View style={{ marginBottom: 20, backgroundColor: "#ffffff" }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 10,
backgroundColor: "#f8f8f6",
borderBottomWidth: 1,
borderBottomColor: "#cccccc",
}}
>
<Text>PRODUCT {prodIndex + 1}</Text>
{prodIndex !== 0 && (
<Pressable
style={styles.closeButton}
onPress={() => removeProduct(prodIndex)}
>
<Icon name="close" size={24} color="#969696" />
</Pressable>
)}
</View>
<View style={{ paddingHorizontal: 10, paddingTop: 10 }}>
<View
style={{
marginBottom: 10,
borderBottomWidth: 1,
borderBottomColor: "#cccccc",
}}
>
<NameInput
shopId={shopId}
prodIndex={prodIndex}
prodName={product.name}
prodValidation={product.validationNameStatus}
unselectNameInput={unselectNameInput}
/>
<QtyInput
prodIndex={prodIndex}
prodQty={product.qty}
unselectQtyInput={unselectQtyInput}
/>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
closeButton: {
paddingHorizontal: 3,
},
});
#### NameInput.tsx ####
import React, { useState } from "react";
import { ActivityIndicator, Text, TextInput, View } from "react-native";
import {
AntDesign as AntIcon,
MaterialIcons as MatIcon,
} from "@expo/vector-icons";
import { OrderProductNameValidationStatus } from "@app/types";
import { fakeApiValidateProduct } from "@app/apis";
interface Props {
shopId: number;
prodIndex: number;
prodName: string;
prodValidation: OrderProductNameValidationStatus;
unselectNameInput: (
text: string,
index: number,
status: OrderProductNameValidationStatus
) => Promise<void>;
}
export default function NameInput({
shopId,
prodIndex,
prodName,
prodValidation,
unselectNameInput,
}: Props): JSX.Element {
// variables
const [name, setName] = useState(prodName);
const [isNameSelected, setIsNameSelected] = useState(false);
const [validateNameStatus, setValidateNameStatus] =
useState<OrderProductNameValidationStatus>(prodValidation);
// functions
const handleUnselectNameInput = async (text: string, prodIndex: number) => {
setIsNameSelected(false);
if (prodName === name) return;
if (name.length > 0) {
setValidateNameStatus("validating");
const prodValidResponse: boolean = await fakeApiValidateProduct(
shopId as any,
name
);
if (prodValidResponse) {
unselectNameInput(text, prodIndex, "success");
} else {
unselectNameInput(text, prodIndex, "failed");
}
}
};
// render
return (
<View style={{ position: "relative" }}>
<Text>Name</Text>
<TextInput
style={{
marginBottom: 10,
padding: 10,
borderWidth: 2,
borderColor: isNameSelected ? "#2ecc71" : "#96a6ad",
borderRadius: 2,
}}
onChangeText={(text: string) => setName(text)}
onFocus={() => setIsNameSelected(true)}
onBlur={() => handleUnselectNameInput(name, prodIndex)}
value={name}
/>
{validateNameStatus === "success" ? (
<View style={{ position: "absolute", bottom: 24, right: 10 }}>
<AntIcon name="checkcircle" size={24} color="#23c0b5" />
</View>
) : validateNameStatus === "failed" ? (
<View style={{ position: "absolute", bottom: 24, right: 10 }}>
<MatIcon name="error" size={24} color="#ffa125" />
</View>
) : validateNameStatus === "validating" ? (
<View style={{ position: "absolute", bottom: 24, right: 10 }}>
<ActivityIndicator size={25} />
</View>
) : null}
</View>
);
}
#### QtyInput.tsx ####
import React, { memo, useState } from "react";
import { Text, TextInput, View } from "react-native";
interface Props {
prodIndex: number;
prodQty: string;
unselectQtyInput: (text: string, index: number) => void;
}
function QtyInput({
prodIndex,
prodQty,
unselectQtyInput,
}: Props): JSX.Element {
// variables
const [qty, setQty] = useState(prodQty);
const [isQtySelected, setIsQtySelected] = useState(false);
// functions
const handleUnselectedQtyInput = (text: string, prodIndex: number) => {
if (!/^\d+$/.test(text) && text.length > 0) return; // numbers only
setIsQtySelected(false);
const qtyUpdated = text;
if (qtyUpdated.length === 0) {
setQty("1");
unselectQtyInput("1", prodIndex);
} else {
unselectQtyInput(qtyUpdated, prodIndex);
}
};
// render
return (
<View>
<Text>Qty</Text>
<TextInput
style={{
marginBottom: 10,
padding: 10,
borderWidth: 2,
borderColor: isQtySelected ? "#2ecc71" : "#96a6ad",
borderRadius: 2,
}}
keyboardType="numeric"
onChangeText={(text: string) => setQty(text)}
onFocus={() => setIsQtySelected(true)}
onBlur={() => handleUnselectedQtyInput(qty, prodIndex)}
value={qty}
/>
</View>
);
}
export default memo(QtyInput);
[1]: https://i.stack.imgur.com/SmEkK.gif
</details>
# 答案1
**得分**: 1
在Flatlist中,重新渲染React的方式导致了无法选择第二个产品名称输入框的阻塞。我在这里找到了相同的问题:https://github.com/facebook/react-native/issues/28246
#### 修复方法 ####
修复方法只是添加这一行 `removeClippedSubviews={false}`
#### 示例:ProductList.tsx 组件 ####
```jsx
export default function ProductsList(props: Props): JSX.Element {
const { products } = props;
return (
<SafeAreaView style={styles.container}>
<FlatList
data={products}
renderItem={({ item, index }) => (
<ProductListItem {...props} prodIndex={index} product={item} />
)}
keyExtractor={(item, index) => `${item.name}-${index}`}
removeClippedSubviews={false} // <--------------------------- 添加这一行
/>
</SafeAreaView>
);
}
```
<details>
<summary>英文:</summary>
Something in the Flatlist in the way it re-renders react is causing the blocking of selecting the 2nd product name input. I found the same issue here: https://github.com/facebook/react-native/issues/28246
#### The Fix ####
The fix was just to add this line `removeClippedSubviews={false}`
#### Example: ProductList.tsx Component ####
```
export default function ProductsList(props: Props): JSX.Element {
const { products } = props;
return (
<SafeAreaView style={styles.container}>
<FlatList
data={products}
renderItem={({ item, index }) => (
<ProductListItem {...props} prodIndex={index} product={item} />
)}
keyExtractor={(item, index) => `${item.name}-${index}`}
removeClippedSubviews={false} // <--------------------------- add this line
/>
</SafeAreaView>
);
}
```
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论