React Native BottomTabNavigator非关联状态更新引发子组件重渲染问题及解决
进入AwayScreen调用setStateB修改stateB时,仅依赖stateA的HomeScreen会发生重渲染,但只有在首次进入HomeScreen(完成首次渲染)后才会出现这个现象。另外注意到AboutScreen不会因父Tab导航器重渲染而重渲染,说明子组件不会单纯因父组件重渲染触发重渲染。想实现HomeScreen不会因AwayScreen调用setStateB而重渲染,是否可行?该怎么操作?
console.log("Starting app"); const Tabs = createBottomTabNavigator(); const MyTabs = () => { console.log("Tabs are rendering"); const [stateA, setStateA] = useState<number>(0); const [stateB, setStateB] = useState<number>(0); return ( <Tabs.Navigator screenOptions={({ route }) => ({ tabBarIcon: ({ focused, size }) => { let iconName; if (route.name === "Home") { iconName = focused ? "home" : "home-outline"; } else if (route.name === "Away") { iconName = focused ? "away" : "away-outline"; } else if (route.name === "Scan") { iconName = focused ? "bluetooth" : "bluetooth-outline"; } else if (route.name === "About") { iconName = focused ? "flower" : "flower-outline"; } return ( <Ionicons name={iconName} size={size} /> ); }, })} initialRouteName="Scan"> <Tabs.Screen name="Scan" children={props => ( <ScanScreen setStateA={setStateA} {...props} /> )} /> <Tabs.Screen name="Home" children={props => ( <HomeScreen stateA={stateA} {...props} /> )} /> <Tabs.Screen name="Away" children={props => ( <AwayScreen stateB={stateB} setStateB={setStateB} {...props} /> )} /> <Tabs.Screen name="About" component={AboutScreen} /> </Tabs.Navigator> ); }; const App = () => { console.log(" "); console.log("Top of App - rendering"); return ( <NavigationContainer> <View style={globalStyles.navigationContainer}> <MyTabs /> </View> </NavigationContainer> ); };
实验1:未进入过HomeScreen时操作
从ScanScreen进入AwayScreen,点击滑块调用setStateB修改stateB,日志输出:
LOG Starting app LOG Running "test_app" with {"rootTag":71} LOG Top of App - rendering LOG Tabs are rendering LOG ScanScreen rendering LOG AwayScreen rendering LOG Tabs are rendering LOG ScanScreen rendering LOG AwayScreen rendering
实验2:先进入HomeScreen再操作
从ScanScreen进入HomeScreen(仅展示stateA),再进入AwayScreen修改stateB,日志输出:
LOG Starting app LOG Running "test_app" with {"rootTag":71} LOG Top of App - rendering LOG Tabs are rendering LOG ScanScreen rendering LOG HomeScreen rendering LOG AwayScreen rendering LOG Tabs are rendering LOG ScanScreen rendering LOG HomeScreen rendering LOG AwayScreen rendering
可行,核心思路是隔离state的作用域,避免MyTabs组件因stateB更新整体重渲染导致HomeScreen触发重渲染,同时利用React的优化机制减少不必要的重渲染。
方法1:拆分state到独立组件(推荐)
把stateA和stateB分别放到对应的屏幕组件或单独的封装组件中,避免MyTabs持有所有state导致整体重渲染:
// 封装HomeScreen的state逻辑 const HomeScreenWrapper = (props) => { const [stateA, setStateA] = useState<number>(0); // 若ScanScreen需要修改stateA,可通过Context或回调传递setStateA return <HomeScreen stateA={stateA} {...props} />; }; // 封装AwayScreen的state逻辑 const AwayScreenWrapper = (props) => { const [stateB, setStateB] = useState<number>(0); return <AwayScreen stateB={stateB} setStateB={setStateB} {...props} />; }; // 修改MyTabs组件,不再持有stateA和stateB const MyTabs = () => { console.log("Tabs are rendering"); return ( <Tabs.Navigator // 保持原screenOptions配置不变 initialRouteName="Scan"> <Tabs.Screen name="Scan" children={props => ( <ScanScreen // 若需修改stateA,通过Context传递setStateA {...props} /> )} /> <Tabs.Screen name="Home" component={HomeScreenWrapper} /> <Tabs.Screen name="Away" component={AwayScreenWrapper} /> <Tabs.Screen name="About" component={AboutScreen} /> </Tabs.Navigator> ); };
方法2:用React.memo+useCallback优化渲染
因为使用children属性传递组件时,每次MyTabs重渲染都会创建新的函数实例,导致React.memo失效。可以提前用useCallback固定渲染函数,再给HomeScreen添加React.memo:
// 给HomeScreen添加React.memo,仅当props变化时重渲染 const HomeScreen = React.memo(({ stateA }) => { console.log("HomeScreen rendering"); // 组件内容 }); // 在MyTabs中用useCallback固定渲染函数 const MyTabs = () => { console.log("Tabs are rendering"); const [stateA, setStateA] = useState<number>(0); const [stateB, setStateB] = useState<number>(0); // 固定渲染函数,避免每次创建新实例 const renderHomeScreen = useCallback((props) => { return <HomeScreen stateA={stateA} {...props} />; }, [stateA]); // 仅当stateA变化时更新函数 return ( <Tabs.Navigator // 原配置不变 > <Tabs.Screen name="Home" children={renderHomeScreen} /> // 其他Screen配置不变 </Tabs.Navigator> ); };
方法3:用React Context拆分状态
创建两个独立的Context分别管理stateA和stateB,让HomeScreen和AwayScreen各自订阅对应的Context,修改stateB时只有订阅该Context的组件重渲染:
// 创建stateA的Context const StateAContext = React.createContext({ stateA: 0, setStateA: () => {}, }); // 创建stateB的Context const StateBContext = React.createContext({ stateB: 0, setStateB: () => {}, }); // 修改MyTabs提供Context const MyTabs = () => { console.log("Tabs are rendering"); const [stateA, setStateA] = useState<number>(0); const [stateB, setStateB] = useState<number>(0); return ( <StateAContext.Provider value={{ stateA, setStateA }}> <StateBContext.Provider value={{ stateB, setStateB }}> <Tabs.Navigator // 原配置不变 > <Tabs.Screen name="Scan" component={ScanScreen} /> <Tabs.Screen name="Home" component={HomeScreen} /> <Tabs.Screen name="Away" component={AwayScreen} /> <Tabs.Screen name="About" component={AboutScreen} /> </Tabs.Navigator> </StateBContext.Provider> </StateAContext.Provider> ); }; // HomeScreen中订阅stateA的Context const HomeScreen = () => { const { stateA } = React.useContext(StateAContext); console.log("HomeScreen rendering"); // 组件内容 }; // AwayScreen中订阅stateB的Context const AwayScreen = () => { const { stateB, setStateB } = React.useContext(StateBContext); console.log("AwayScreen rendering"); // 组件内容 };
内容的提问来源于stack exchange,提问作者Will Thrive

