React Native中useEffect触发页面导航两次问题排查
我正在开发一款基于Expo的React Native多页面应用,应用带有可切换页面的固定底部Footer,Footer包含对应三个页面的图标。原本功能正常,添加选中图标高亮功能后出现问题:点击Footer图标时,图标能正常高亮,但页面无法切换。在useEffect中打印activeIcon发现,每次点击后useEffect会触发两次,先输出选中的路由名称,随后又回到初始的MessageFeed,导致导航回初始页面。
Footer.js 代码
const Footer = (props: Props) => { const navigation = useNavigation(); const [activeIcon, setActiveIcon] = useState('MessageFeed'); useEffect(() => { // Navigate to the activeIcon route after state update navigation.navigate(activeIcon); }, [activeIcon, navigation]); const handleIconClick = (routeName) => { setActiveIcon(routeName); // Update active icon immediately }; return ( <View style={{flex: 0.1, flexDirection: 'row', justifyContent: 'space-around', marginTop: 10}}> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('MyProfile')}> <FontAwesomeIcon icon={faUser} size={30} color={activeIcon === 'MyProfile' ? '#fb5b5a' : null}/> </TouchableOpacity> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('MessageFeed')}> <FontAwesomeIcon icon={faHouse} size={30} color={activeIcon === 'MessageFeed' ? '#fb5b5a' : null}/> </TouchableOpacity> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('CreateTextMessage')}> <FontAwesomeIcon icon={faCirclePlus} size={30} color={activeIcon === 'CreateTextMessage' ? '#fb5b5a' : null}/> </TouchableOpacity> </View> ); }
控制台输出(点击第一个图标时)
LOG MyProfile LOG MessageFeed
useEffect触发两次的直接原因:Expo/React Native在开发模式下默认启用
StrictMode,这个模式会刻意重复执行useEffect等生命周期函数,用于检测潜在的代码问题,所以开发环境下触发两次是正常现象,但这不是页面回退的核心原因。页面无法切换的核心问题:Footer组件靠内部状态
activeIcon控制导航,但没有和导航栈的实际路由状态同步。当调用navigation.navigate切换页面后,组件树可能因父组件重新渲染导致Footer被重新挂载,此时activeIcon会被重置为初始值MessageFeed,进而触发useEffect再次调用navigation.navigate('MessageFeed'),强制切回初始页面。
简单来说就是:组件内部状态和导航系统状态脱节,组件重渲染时内部状态重置,导致导航回退。
方案1:同步导航状态到组件内部状态
监听路由变化,将当前路由名称同步到activeIcon,确保内部状态和实际路由一致:
const Footer = (props: Props) => { const navigation = useNavigation(); const [activeIcon, setActiveIcon] = useState('MessageFeed'); // 监听路由变化,同步activeIcon useEffect(() => { const unsubscribe = navigation.addListener('state', () => { const currentRoute = navigation.getCurrentRoute()?.name; if (currentRoute) { setActiveIcon(currentRoute); } }); return unsubscribe; // 组件卸载时清理监听 }, [navigation]); const handleIconClick = (routeName) => { // 避免重复导航到当前页面 if (activeIcon !== routeName) { navigation.navigate(routeName); } }; return ( <View style={{flex: 0.1, flexDirection: 'row', justifyContent: 'space-around', marginTop: 10}}> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('MyProfile')}> <FontAwesomeIcon icon={faUser} size={30} color={activeIcon === 'MyProfile' ? '#fb5b5a' : null}/> </TouchableOpacity> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('MessageFeed')}> <FontAwesomeIcon icon={faHouse} size={30} color={activeIcon === 'MessageFeed' ? '#fb5b5a' : null}/> </TouchableOpacity> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('CreateTextMessage')}> <FontAwesomeIcon icon={faCirclePlus} size={30} color={activeIcon === 'CreateTextMessage' ? '#fb5b5a' : null}/> </TouchableOpacity> </View> ); }
方案2:直接使用导航栈状态判断高亮
完全去掉组件内部的activeIcon状态,直接从导航系统获取当前路由名称来控制图标颜色,彻底避免状态同步问题:
const Footer = (props: Props) => { const navigation = useNavigation(); // 获取当前路由名称,默认值设为初始页 const currentRoute = navigation.getCurrentRoute()?.name || 'MessageFeed'; const handleIconClick = (routeName) => { if (currentRoute !== routeName) { navigation.navigate(routeName); } }; return ( <View style={{flex: 0.1, flexDirection: 'row', justifyContent: 'space-around', marginTop: 10}}> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('MyProfile')}> <FontAwesomeIcon icon={faUser} size={30} color={currentRoute === 'MyProfile' ? '#fb5b5a' : null}/> </TouchableOpacity> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('MessageFeed')}> <FontAwesomeIcon icon={faHouse} size={30} color={currentRoute === 'MessageFeed' ? '#fb5b5a' : null}/> </TouchableOpacity> <TouchableOpacity style={styles.bottomBtn} onPress={() => handleIconClick('CreateTextMessage')}> <FontAwesomeIcon icon={faCirclePlus} size={30} color={currentRoute === 'CreateTextMessage' ? '#fb5b5a' : null}/> </TouchableOpacity> </View> ); }
内容的提问来源于stack exchange,提问作者Mario Mateaș

