React Native中状态更新时useEffect未触发的问题排查
在React Native组件中使用useState()更新状态,希望状态设置完成后将详情保存到服务器,于是在useEffect()钩子中实现了该逻辑。状态设置有效,能在屏幕上看到对应值,但尽管已将note作为useEffect()依赖数组的依赖项,状态更新时该函数从未触发。
相关代码片段:
const [note, setNote] = useState(''); const dispatch = useDispatch(); useEffect(() => { if (note) { console.log('updating note...'); dispatch(updateNote(props.client.id, note)); } }, [note]);
TextInput更新状态的代码(因在TabNavigator的screenOptions中传入组件,使用onChangeText()或onChange()会出现React失去焦点问题,故使用onBlur):
<TextInput key='note' style={{ color: '#fff', fontSize: 16, width: '100%', textAlign: 'center', }} placeholder='Tap here to share something...' placeholderTextColor={styles.colors.textPlaceholder} maxLength={50} onBlur={(text) => setNote(text)} defaultValue={note || props?.client?.note?.value} />
将父组件内的CustomHeader函数传入TabNavigator的screenOptions的代码:
screenOptions={{ headerShown: true, headerStyle: { backgroundColor: Colors.primary, elevation: 0, shadowOpacity: 0, shadowColor: 'transparent', height: 170, }, key: 'patient-tab', headerShadowVisible: false, tabBarStyle: { backgroundColor: Colors.primary }, headerTintColor: Colors.light, headerTitle: (props) => <CustomHeader {...props} />, // Passed in here tabBarActiveTintColor: Colors.light, tabBarInactiveTintColor: Colors.lightInactive, tabBarActiveBackgroundColor: Colors.primary, tabBarInactiveBackgroundColor: Colors.primary, }}
完整代码:
export const ClientBottomTabNavigator = (props) => { const [note, setNote] = useState(''); const dispatch = useDispatch(); useEffect(() => { if (props.client.id && note) { console.log('updating note...'); dispatch(updateNote(props.client.id, note)); } }, [props.client.id, note]); const CustomHeader = () => { return ( <View> <View style={{ width: '100%', flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}> <Feather name='chevron-left' size={24} onPress={() => props.navigation.goBack()} color={styles.colors.textInverse} style={{ justifySelf: 'flex-start', alignSelf: 'flex-start', width: '32%', marginBottom: 6 }} /> <Text style={{ color: '#fff', fontSize: 18, alignSelf: 'center', justifySelf: 'center', fontWeight: 'bold', }} > {props?.client?.firstName} {props?.client?.lastName} </Text> <Feather name='' // Leave this blank style={{ justifySelf: 'flex-end', alignSelf: 'flex-end', width: '32%' }} /> </View> <View style={{ alignItems: 'center', marginBottom: 6 }}> <Text style={{ color: '#fff', fontSize: 18, marginBottom: 6 }}> {convertDiscipline(props?.client?.discipline)} </Text> <View> <TextInput key='note' style={{ color: '#fff', fontSize: 16, width: '100%', textAlign: 'center', }} placeholder='Tap here to share something…' placeholderTextColor={styles.colors.textPlaceholder} maxLength={50} onBlur={(text) => setNote(text)} defaultValue={note || props?.client?.note?.value} /> </View> </View> </View> ); }; const Tab = createBottomTabNavigator(); return ( <Tab.Navigator screenOptions={{ headerShown: true, headerStyle: { backgroundColor: Colors.primary, elevation: 0, shadowOpacity: 0, shadowColor: 'transparent', height: 170, }, key: 'client-tab', headerShadowVisible: false, tabBarStyle: { backgroundColor: Colors.primary }, headerTintColor: Colors.light, headerTitle: (props) => <CustomHeader {...props} />, // Passed in here tabBarActiveTintColor: Colors.light, tabBarInactiveTintColor: Colors.lightInactive, tabBarActiveBackgroundColor: Colors.primary, tabBarInactiveBackgroundColor: Colors.primary, }} initialRouteName={'Visits'} > <Tab.Screen name='Visits' component={VisitsTab} options={{ tabBarLabel: 'VISITS', tabBarIcon: ({ color, size }) => <FontAwesome name='street-view' color={color} size={size} />, }} /> <Tab.Screen name='Chart' component={ChartTab} options={{ tabBarLabel: 'CHARTS', tabBarIcon: ({ color, size }) => <FontAwesome name='id-badge' color={color} size={size} />, }} /> <Tab.Screen name='Goals' component={GoalsTab} options={{ tabBarLabel: 'EDIT GOALS', tabBarIcon: ({ color, size }) => <FontAwesome name='trophy' color={color} size={size} />, }} /> </Tab.Navigator> ); };
1. 修复onBlur参数错误
TextInput的onBlur回调接收的是FocusEvent对象,不是输入框的文本值。你直接把事件对象传给setNote,导致note变成了对象类型,而非预期的字符串,这会导致useEffect的逻辑异常。
修改代码,通过event.nativeEvent.text获取输入框文本:
onBlur={(event) => setNote(event.nativeEvent.text)}
2. 解决CustomHeader重复创建导致的状态隔离
你在ClientBottomTabNavigator内部定义CustomHeader,每次父组件重渲染时,CustomHeader都会被重新定义。React会认为这是全新组件,导致setNote更新的是旧组件实例关联的状态,而useEffect监听的是新实例的状态,最终出现状态更新但useEffect不触发的情况。
有两种修复方式:
方式一:将CustomHeader提取到组件外部
// 提取到外部,通过props传递所需数据 const CustomHeader = ({ note, setNote, client, navigation }) => { return ( <View> <View style={{ width: '100%', flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 }}> <Feather name='chevron-left' size={24} onPress={() => navigation.goBack()} color={styles.colors.textInverse} style={{ justifySelf: 'flex-start', alignSelf: 'flex-start', width: '32%', marginBottom: 6 }} /> <Text style={{ color: '#fff', fontSize: 18, alignSelf: 'center', justifySelf: 'center', fontWeight: 'bold', }} > {client?.firstName} {client?.lastName} </Text> <Feather name='' style={{ justifySelf: 'flex-end', alignSelf: 'flex-end', width: '32%' }} /> </View> <View style={{ alignItems: 'center', marginBottom: 6 }}> <Text style={{ color: '#fff', fontSize: 18, marginBottom: 6 }}> {convertDiscipline(client?.discipline)} </Text> <View> <TextInput key='note' style={{ color: '#fff', fontSize: 16, width: '100%', textAlign: 'center', }} placeholder='Tap here to share something…' placeholderTextColor={styles.colors.textPlaceholder} maxLength={50} onBlur={(event) => setNote(event.nativeEvent.text)} value={note || client?.note?.value} /> </View> </View> </View> ); }; export const ClientBottomTabNavigator = (props) => { const [note, setNote] = useState(''); const dispatch = useDispatch(); useEffect(() => { if (props.client.id && note) { console.log('updating note...'); dispatch(updateNote(props.client.id, note)); } }, [props.client.id, note, dispatch]); // 补充dispatch到依赖数组,避免lint警告 const Tab = createBottomTabNavigator(); return ( <Tab.Navigator screenOptions={{ // 原有配置不变 headerTitle: () => <CustomHeader note={note} setNote={setNote} client={props.client} navigation={props.navigation} />, }} initialRouteName={'Visits'} > {/* 原有Tab.Screen配置不变 */} </Tab.Navigator> ); };
方式二:用useCallback缓存CustomHeader
export const ClientBottomTabNavigator = (props) => { const [note, setNote] = useState(''); const dispatch = useDispatch(); useEffect(() => { if (props.client.id && note) { console.log('updating note...'); dispatch(updateNote(props.client.id, note)); } }, [props.client.id, note, dispatch]); // 使用useCallback缓存组件,避免重复创建 const CustomHeader = useCallback(() => { return ( <View> {/* 原有代码不变,仅修正onBlur参数 */} <TextInput key='note' style={{ color: '#fff', fontSize: 16, width: '100%', textAlign: 'center', }} placeholder='Tap here to share something…' placeholderTextColor={styles.colors.textPlaceholder} maxLength={50} onBlur={(event) => setNote(event.nativeEvent.text)} value={note || props?.client?.note?.value} /> </View> ); }, [note, props.client, props.navigation]); // 添加所有用到的依赖 const Tab = createBottomTabNavigator(); return ( <Tab.Navigator screenOptions={{ // 原有配置不变 headerTitle: (headerProps) => <CustomHeader {...headerProps} />, }} initialRouteName={'Visits'} > {/* 原有Tab.Screen配置不变 */} </Tab.Navigator> ); };
3. 修正defaultValue为value
defaultValue仅在组件初始化时生效,状态更新后输入框不会同步显示新值。改用value属性实现双向绑定:
<TextInput // 其他属性不变 value={note || props?.client?.note?.value} onBlur={(event) => setNote(event.nativeEvent.text)} />
内容的提问来源于stack exchange,提问作者Muirik

