React Native Expo:iPhone 14 Pro Max选中TextInput时屏幕闪烁
问题:iPhone 14 Pro Max上React Native Expo TextInput持续闪烁
开发的登录界面基于React Native Expo实现,其中的TextInput组件在iPhone 14 Pro Max设备上出现持续闪烁问题,但在多款模拟器及iPhone XS Max设备上测试均运行正常。
组件树对比
- 原始组件树:

- 闪烁时组件树:

相关代码
import { Ionicons, MaterialCommunityIcons, MaterialIcons, } from '@expo/vector-icons'; import React, {useEffect, useRef, useState} from 'react'; import { Alert, Image, Keyboard, Pressable, SafeAreaView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import SDButton from '../components/ui/sdbuttons'; import Input from '../boilerplate/input'; import {useTheme} from '../boilerplate/theme'; import {useAppContext} from '../boilerplate/store'; import { sendPasswordResetEmail, signInWithEmailAndPassword, } from 'firebase/auth'; import {doc, getDoc, updateDoc} from 'firebase/firestore'; import {auth, firestore, logout} from '../boilerplate/firebase'; import {marginViewVertical} from '../helpers/margins'; import {isEmailRegistered} from '../helpers/firestore'; import {TextInput} from 'react-native'; const SignIn = ({navigation}) => { const theme = useTheme(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); const [isEmailLocked, setIsEmailLocked] = useState(false); const [saving, setSaving] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [isForgotPasswordExpanded, setIsForgotPasswordExpanded] = useState(false); const context = useAppContext(); const passwordRef = useRef(); const loginCleanup = () => { console.log('cleaning up!'); setEmail(''); setPassword(''); setIsEmailLocked(false); setSaving(false); }; const handleForgotPassword = async () => { setSaving(true); await sendPasswordResetEmail(auth, email); setSaving(false); Alert.alert( 'Are your sure?', 'Are you sure you want to reset your password?', [ { text: 'Yes', onPress: () => { Alert.alert( 'Link Sent', 'Please check your inbox, and if nothing has arrived please check your spam folder.', [ { onPress: () => {}, }, ], ); }, }, { text: 'No', }, ], ); }; const handleForgotPressed = () => { setIsExpanded(false); setIsForgotPasswordExpanded(true); }; const checkEmail = async () => { try { setSaving(true); if (email === '') { Alert.alert('Log In Error', 'Please input email'); setSaving(false); } else if (await isEmailRegistered(email)) { setShowPassword(false); setIsExpanded(true); setIsEmailLocked(true); setSaving(false); } else { Alert.alert('Log In Error', 'Email does not exist'); setSaving(false); } } catch (e) { console.error('e', e); setSaving(false); } }; const handleBackPressed = () => { if (isForgotPasswordExpanded) { setShowPassword(false); setIsExpanded(true); setIsForgotPasswordExpanded(false); } else if (isExpanded) { setIsExpanded(false); setIsEmailLocked(false); } else navigation?.goBack(); }; const doLogin = async () => { Keyboard?.dismiss(); setSaving(true); if (email === '' || password === '') { Alert.alert('Log In Error', 'Please input email and password correctly'); setSaving(false); return; } signInWithEmailAndPassword(auth, email, password) .then(async userCredentails => { const customerData = await getDoc( doc(firestore, 'users', userCredentails?.user?.uid), ); const stripeSubscriptionEndDate = customerData?.data()?.userData?.stripeSubscriptionEndDate; const stripeCustomerId = customerData?.data()?.userData?.stripeCustomerId; const stripeSubscriptionId = customerData?.data()?.userData?.stripeSubscriptionId; if (!stripeCustomerId || !stripeSubscriptionId) { if (logout) await logout(); updateDoc(doc(firestore, 'users', userCredentails?.user?.uid), { token: '', }); context?.setProfile(undefined); context?.setIsAuthed(false); alert('You do not have a valid Sit Down subscription'); } else if (new Date() > new Date(stripeSubscriptionEndDate)) { if (logout) await logout(); updateDoc(doc(firestore, 'users', userCredentails?.user?.uid), { token: '', }); context?.setProfile(undefined); context?.setIsAuthed(false); alert('Your subscription has ended!'); } loginCleanup(); }) .catch(e => { console.error(e?.code); if (e?.code) alert('Please check your email and password'); setSaving(false); }); }; return ( <SafeAreaView style={{ flex: 1, backgroundColor: theme.primaryBackground, }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', width: '100%', marginTop: '1.5%', paddingHorizontal: '4%', alignItems: 'center', }}> <Image source={require('../assets/login-icon.png')} style={{ width: 35, height: 35, }} cachePolicy={'memory-disk'} /> <Pressable onPress={handleBackPressed}> <Ionicons name="close-circle" color="white" size={32} /> </Pressable> </View> <View style={{ flex: 1, justifyContent: 'center', paddingHorizontal: 50, paddingLeft: 20, }}> <View style={{width: '100%', paddingHorizontal: 5}}> <Text style={{ color: 'white', fontSize: 18, fontFamily: context?.helv600, }}> {isForgotPasswordExpanded ? 'Request a Password Reset Link' : 'Log In'} </Text> {!isForgotPasswordExpanded && ( <TouchableOpacity onPress={() => navigation?.navigate('SignUp')}> <Text style={{ color: '#88ffdc', fontSize: 15, marginTop: 10, fontFamily: context?.helv500, }}> or Join Now </Text> </TouchableOpacity> )} </View> {marginViewVertical('10%')} <View style={{ alignItems: 'center', justifyContent: 'space-between', alignSelf: 'center', width: '100%', borderBottomColor: '#fff', flexDirection: 'row', }}> <View style={{ flex: 1, width: '100%', padding: 5, alignContent: 'center', flexDirection: 'row', alignSelf: 'center', marginBottom: 10, // borderWidth: 1, // borderColor: 'white', }}> <MaterialCommunityIcons name="email-outline" size={24} color="#fff" /> <View style={{width: 10}} /> <TextInput style={[ styles.input, { color: isEmailLocked ? '#898b97' : 'white', }, ]} keyboardType={'email'} autoComplete={'off'} autoCapitalize={'none'} autoCorrect={false} value={email} onChangeText={setEmail} placeholder={'E-mail, required'} placeholderTextColor={'#898b97'} editable={!isEmailLocked} /> {/* <Input placeholder="E-mail, required" placeholderTextColor={'#898b97'} onChange={setEmail} value={email} isAutoCorrect={false} autoCapitalize={'none'} keyboardType={'email-address'} locked={isEmailLocked} />*/} </View> </View> {isExpanded && ( <> <View style={{ justifyContent: 'center', }}> <View style={{ alignItems: 'center', justifyContent: 'space-between', alignSelf: 'center', width: '100%', borderBottomColor: '#fff', //borderBottomWidth: 1, flexDirection: 'row', marginTop: 10, marginBottom: 10, }}> <View style={{ flex: 1, padding: 5, alignContent: 'center', flexDirection: 'row', justifyContent: 'flex-start', }}> <MaterialIcons name="lock-outline" size={24} color="#fff" /> <View style={{width: 10}} /> <TextInput style={[ styles.input, { flex: 1, width: '100%', color: '#fff', }, ]} autoComplete={'off'} autoCapitalize={'none'} autoCorrect={false} value={password} onChangeText={setPassword} placeholder={'Password, required'} placeholderTextColor={'#898b97'} secure={!showPassword} /> {/* <Input secure={!showPassword} placeholder="Password, required" placeholderTextColor={'#898b97'} onChange={setPassword} value={password} autoCapitalize={'none'} style={{ flex: 1, width: '100%', marginLeft: 10, color: '#fff', }} inputRef={passwordRef} /> */} </View> <Ionicons style={{position: 'absolute', right: -20, bottom: 30}} onPress={() => setShowPassword(!showPassword)} name={showPassword ? 'ios-eye' : 'ios-eye-off'} size={24} color="#898b97" /> </View> <TouchableOpacity style={{width: '100%'}} onPress={handleForgotPressed}> <Text style={{ color: '#00FFCD', fontSize: 14, }}> Forgot password? </Text> </TouchableOpacity> </View> {marginViewVertical(25)} </> )} {/* /////////////////////////////////////////////////////////////////////////////////////////// */} <View style={{ justifyContent: 'center', marginStart: 40, }}> <SDButton loading={saving} text={ isForgotPasswordExpanded ? 'Submit' : !isExpanded ? 'Continue' : 'Log In' } backgroundColor="#00FFCD" textColor="black" my={20} blurOnSubmit={false} onPress={() => { isForgotPasswordExpanded ? handleForgotPassword() : !isExpanded ? checkEmail() : doLogin(); }} /> </View> </View> </SafeAreaView> ); }; const styles = StyleSheet.create({ input: { alignItems: 'center', justifyContent: 'space-between', alignSelf: 'center', width: '88%', borderBottomColor: '#fff', borderBottomWidth: 1, marginBottom: 20, color: 'white', flexDirection: 'row', paddingBottom: 5, fontSize: 18, }, }); export default SignIn;
解决方案
针对iPhone 14 Pro Max上TextInput闪烁的问题,尝试以下修复方案:
1. 补充输入控件的系统优化禁用属性
在TextInput中添加textContentType="none",彻底禁用iOS系统的输入关联优化,避免触发不必要的组件重渲染:
<TextInput // 原有属性保留 textContentType="none" />
2. 缓存动态样式减少重绘
Email输入框的颜色依赖isEmailLocked状态切换,使用useMemo缓存样式,避免每次状态变更都重新生成样式对象:
import { useMemo } from 'react'; // 组件内部定义 const emailInputStyle = useMemo(() => [ styles.input, { color: isEmailLocked ? '#898b97' : 'white' } ], [isEmailLocked]); // 在TextInput中使用 <TextInput style={emailInputStyle} />
3. 减少组件树动态变更
对比闪烁前后的组件树,发现存在节点挂载/卸载的频繁变更。将条件渲染的组件改为通过样式控制显隐,避免组件树频繁重构:
// 替换原有的{isExpanded && <Component />}写法 <View style={{ display: isExpanded ? 'flex' : 'none' }}> {/* 原密码输入框及相关内容 */} </View>
4. 升级Expo及React Native版本
iPhone 14 Pro Max的灵动岛设计可能与旧版本框架存在适配问题,升级到最新稳定版:
expo upgrade
5. 检查状态更新逻辑
排查checkEmail、handleBackPressed等函数中的状态更新代码,确保没有重复设置相同状态值,避免触发无意义的组件渲染。
内容的提问来源于stack exchange,提问作者Abdul Ahad
相关产品推荐
相关产品推荐

