RN路由与状态管理:打造多页面应用
在前两篇文章中,我们掌握了RN的基础组件、Flex布局和简单交互,能够搭建单页面UI。但实际应用往往是多页面结构,还需要在页面间共享数据,这就需要路由管理 和状态管理技术。本文作为RN体系化专栏的第三篇,将聚焦React Navigation路由库和Redux状态管理方案,带你实现多页面跳转、传参和全局数据共享,完成从"单页面"到"多页面应用"的进阶。
一、路由管理:React Navigation实现页面跳转
RN本身没有内置路由系统,社区主流方案是React Navigation,它提供了栈路由、标签路由、抽屉路由等多种导航模式,支持跨端一致性和灵活的自定义配置。
1. React Navigation环境搭建
(1)安装核心依赖
首先在项目中安装React Navigation的核心包和对应适配器(以最新的React Navigation 6为例):
bash
# 安装核心依赖
npm install @react-navigation/native
# 安装原生依赖(Expo项目可跳过,Expo已内置)
# 非Expo项目需执行以下命令(Android/iOS原生配置)
npm install react-native-screens react-native-safe-area-context
# 安装栈路由(最基础的页面跳转模式)
npm install @react-navigation/native-stack
(2)原生项目额外配置
非Expo的原生CLI项目,需完成Android和iOS的原生配置:
- Android :修改
android/app/src/main/java/com/[项目名]/MainActivity.java,添加enableEdgeToEdge()支持; - iOS :在
ios/[项目名]/Podfile中添加依赖,执行pod install完成安装。
(3)根组件包裹
在项目入口文件(如App.js)中,用NavigationContainer包裹整个应用,这是React Navigation的核心容器:
jsx
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>
{/* 路由配置将在这里添加 */}
</NavigationContainer>
);
}
2. 栈路由(Stack Navigator):基础页面跳转
栈路由是最常用的导航模式,类似于手机的"页面栈",新页面压入栈顶,返回时弹出栈顶页面,支持页面跳转和传参。
(1)配置栈路由
首先创建两个页面组件(HomePage和DetailPage),再配置栈路由:
jsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomePage from './pages/HomePage';
import DetailPage from './pages/DetailPage';
// 创建栈导航器
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="Home" // 初始页面
screenOptions={{
headerStyle: { backgroundColor: '#0066cc' }, // 导航栏样式
headerTintColor: '#fff', // 导航栏文字/图标颜色
headerTitleStyle: { fontSize: 18, fontWeight: '500' }, // 标题样式
}}
>
{/* 首页 */}
<Stack.Screen
name="Home" // 页面唯一标识,跳转时使用
component={HomePage}
options={{ title: '首页' }} // 导航栏标题
/>
{/* 详情页 */}
<Stack.Screen
name="Detail"
component={DetailPage}
// 动态设置标题(接收路由参数)
options={({ route }) => ({ title: route.params?.title || '详情页' })}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
(2)页面跳转与传参
在HomePage中实现跳转并传递参数,在DetailPage中接收参数:
jsx
// HomePage.js
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
export default function HomePage({ navigation }) {
// 跳转至详情页并传递参数
const goToDetail = () => {
navigation.navigate('Detail', {
title: 'RN路由学习',
content: '这是从首页传递的详情内容',
id: 1001,
});
};
return (
<View style={styles.container}>
<Text style={styles.title}>首页</Text>
<TouchableOpacity style={styles.btn} onPress={goToDetail}>
<Text style={styles.btnText}>进入详情页</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 22,
marginBottom: 20,
},
btn: {
backgroundColor: '#0066cc',
paddingHorizontal: 30,
paddingVertical: 12,
borderRadius: 8,
},
btnText: {
color: '#fff',
fontSize: 16,
},
});
jsx
// DetailPage.js
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
export default function DetailPage({ navigation, route }) {
// 接收首页传递的参数
const { title, content, id } = route.params || {};
// 返回首页(弹出栈顶页面)
const goBack = () => {
navigation.goBack();
};
// 返回首页并传递数据(给上一级页面)
const goBackWithData = () => {
navigation.navigate('Home', { fromDetail: '详情页返回的数据' });
};
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.content}>ID:{id}</Text>
<Text style={styles.content}>{content}</Text>
<TouchableOpacity style={[styles.btn, { marginTop: 20 }]} onPress={goBack}>
<Text style={styles.btnText}>返回首页</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.btn, { marginTop: 10 }]} onPress={goBackWithData}>
<Text style={styles.btnText}>带数据返回</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#fff',
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 15,
},
content: {
fontSize: 16,
color: '#666',
marginBottom: 8,
},
btn: {
backgroundColor: '#0066cc',
paddingVertical: 10,
borderRadius: 8,
alignItems: 'center',
},
btnText: {
color: '#fff',
fontSize: 16,
},
});
(3)路由核心API说明
navigation.navigate('RouteName', params):跳转到指定页面,若页面已在栈中则跳转,否则压入栈;navigation.push('RouteName', params):无论页面是否在栈中,都压入新页面(可实现同一页面多次跳转);navigation.goBack():返回上一级页面;navigation.popToTop():返回栈底页面(首页);route.params:接收跳转时传递的参数,需做非空判断。
3. 标签路由(Tab Navigator):底部/顶部导航
标签路由适用于应用的核心功能模块切换(如首页、消息、我的),支持底部或顶部标签栏,点击标签切换页面。
(1)安装标签路由依赖
bash
npm install @react-navigation/bottom-tabs
(2)配置底部标签路由
jsx
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Ionicons from 'react-native-vector-icons/Ionicons'; // 需安装react-native-vector-icons
import HomePage from './pages/HomePage';
import MessagePage from './pages/MessagePage';
import MinePage from './pages/MinePage';
// 创建标签导航器
const Tab = createBottomTabNavigator();
export default function App() {
return (
<NavigationContainer>
<Tab.Navigator
initialRouteName="Home"
screenOptions={({ route }) => ({
// 自定义标签图标
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Message') {
iconName = focused ? 'chatbubbles' : 'chatbubbles-outline';
} else if (route.name === 'Mine') {
iconName = focused ? 'person' : 'person-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#0066cc', // 激活标签颜色
tabBarInactiveTintColor: '#999', // 未激活标签颜色
tabBarLabelStyle: { fontSize: 12 }, // 标签文字大小
})}
>
<Tab.Screen
name="Home"
component={HomePage}
options={{ title: '首页' }}
/>
<Tab.Screen
name="Message"
component={MessagePage}
options={{ title: '消息' }}
/>
<Tab.Screen
name="Mine"
component={MinePage}
options={{ title: '我的' }}
/>
</Tab.Navigator>
</NavigationContainer>
);
}
4. 嵌套路由:组合栈路由与标签路由
实际应用中常需要"标签路由+栈路由"嵌套(如首页内可跳转详情页),只需在标签页面中嵌套栈路由即可:
jsx
// 为首页配置嵌套栈路由
function HomeStack() {
const Stack = createNativeStackNavigator();
return (
<Stack.Navigator>
<Stack.Screen name="HomeIndex" component={HomePage} options={{ title: '首页' }} />
<Stack.Screen name="Detail" component={DetailPage} />
</Stack.Navigator>
);
}
// 标签路由中使用嵌套栈
<Tab.Screen
name="Home"
component={HomeStack}
options={{ headerShown: false }} // 隐藏标签路由的导航栏,使用栈路由的导航栏
/>
二、状态管理:Redux实现全局数据共享
当应用页面增多时,组件间的数据传递会变得复杂(如跨页面共享用户信息、主题配置),此时需要全局状态管理工具。Redux是React生态最主流的状态管理方案,通过"单一数据源"实现全局数据统一管理。
1. Redux核心概念
Redux的核心是单向数据流,包含四个核心概念:
- Store:全局唯一的状态容器,存储应用所有状态;
- Action :描述状态变化的"指令",是普通对象,必须包含
type字段; - Reducer:纯函数,接收旧状态和Action,返回新状态(不能修改旧状态);
- Dispatch :触发Action的方法,通过
store.dispatch(action)更新状态。
2. Redux环境搭建
(1)安装依赖
bash
# 安装Redux核心包和React绑定库
npm install redux react-redux @reduxjs/toolkit
@reduxjs/toolkit是Redux官方推荐的工具包,简化了Redux的配置和编写。
(2)创建Redux Store
首先创建store目录,包含index.js(创建Store)和reducers(状态处理器):
jsx
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './reducers/userReducer';
import themeReducer from './reducers/themeReducer';
// 配置Store,合并多个Reducer
export const store = configureStore({
reducer: {
user: userReducer, // 用户状态
theme: themeReducer, // 主题状态
},
});
(3)定义Reducer和Action
以用户状态(userReducer)为例,使用createSlice简化Reducer和Action的编写:
jsx
// store/reducers/userReducer.js
import { createSlice } from '@reduxjs/toolkit';
// 初始状态
const initialState = {
username: '',
avatar: '',
isLogin: false,
token: '',
};
// 创建Slice(包含Reducer和Action)
const userSlice = createSlice({
name: 'user', // Slice名称,Action类型前缀
initialState,
reducers: {
// 登录:更新用户信息
login: (state, action) => {
// Redux Toolkit允许直接修改state(内部已做不可变处理)
state.username = action.payload.username;
state.avatar = action.payload.avatar;
state.isLogin = true;
state.token = action.payload.token;
},
// 退出登录:重置状态
logout: (state) => {
state.username = '';
state.avatar = '';
state.isLogin = false;
state.token = '';
},
// 更新头像
updateAvatar: (state, action) => {
state.avatar = action.payload;
},
},
});
// 导出Action Creator
export const { login, logout, updateAvatar } = userSlice.actions;
// 导出Reducer
export default userSlice.reducer;
同理创建主题状态themeReducer,用于管理应用的亮色/暗黑模式:
jsx
// store/reducers/themeReducer.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isDark: false,
primaryColor: '#0066cc',
};
const themeSlice = createSlice({
name: 'theme',
initialState,
reducers: {
toggleTheme: (state) => {
state.isDark = !state.isDark;
state.primaryColor = state.isDark ? '#1a1a1a' : '#0066cc';
},
setPrimaryColor: (state, action) => {
state.primaryColor = action.payload;
},
},
});
export const { toggleTheme, setPrimaryColor } = themeSlice.actions;
export default themeSlice.reducer;
(4)全局注入Store
在项目入口文件中,用Provider将Store注入应用,使所有组件可访问全局状态:
jsx
// App.js
import { NavigationContainer } from '@react-navigation/native';
import { Provider } from 'react-redux';
import { store } from './store';
import MainNavigator from './navigators/MainNavigator';
export default function App() {
return (
<Provider store={store}>
<NavigationContainer>
<MainNavigator />
</NavigationContainer>
</Provider>
);
}
3. 组件中使用Redux状态
(1)读取全局状态:useSelector
使用useSelector钩子从Store中读取状态:
jsx
// pages/MinePage.js
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { logout } from '../store/reducers/userReducer';
import { toggleTheme } from '../store/reducers/themeReducer';
export default function MinePage() {
// 读取用户状态
const { username, avatar, isLogin } = useSelector((state) => state.user);
// 读取主题状态
const { isDark, primaryColor } = useSelector((state) => state.theme);
// 获取dispatch方法
const dispatch = useDispatch();
// 退出登录
const handleLogout = () => {
dispatch(logout());
};
// 切换主题
const handleToggleTheme = () => {
dispatch(toggleTheme());
};
return (
<View style={[styles.container, { backgroundColor: isDark ? '#333' : '#fff' }]}>
{isLogin ? (
<View style={styles.userInfo}>
<Image
source={{ uri: avatar || 'https://placehold.co/80x80' }}
style={styles.avatar}
/>
<Text style={[styles.username, { color: isDark ? '#fff' : '#333' }]}>
{username}
</Text>
</View>
) : (
<TouchableOpacity style={styles.loginBtn}>
<Text style={styles.loginBtnText}>请登录</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={[styles.optionBtn, { backgroundColor: primaryColor }]}
onPress={handleToggleTheme}
>
<Text style={styles.optionBtnText}>
切换{isDark ? '亮色' : '暗黑'}模式
</Text>
</TouchableOpacity>
{isLogin && (
<TouchableOpacity style={[styles.optionBtn, { backgroundColor: '#ff6600', marginTop: 10 }]} onPress={handleLogout}>
<Text style={styles.optionBtnText}>退出登录</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
userInfo: {
alignItems: 'center',
marginVertical: 30,
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: 10,
},
username: {
fontSize: 18,
fontWeight: '500',
},
loginBtn: {
backgroundColor: '#0066cc',
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
marginVertical: 30,
},
loginBtnText: {
color: '#fff',
fontSize: 16,
},
optionBtn: {
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
marginTop: 20,
},
optionBtnText: {
color: '#fff',
fontSize: 16,
},
});
(2)更新全局状态:useDispatch
在登录页面中,通过dispatch触发Action更新用户状态:
jsx
// pages/LoginPage.js
import { View, TextInput, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { login } from '../store/reducers/userReducer';
export default function LoginPage({ navigation }) {
const [name, setName] = useState('');
const dispatch = useDispatch();
const handleLogin = () => {
if (!name) return;
// 模拟登录接口返回数据
const userData = {
username: name,
avatar: 'https://placehold.co/80x80/0066cc/fff',
token: 'mock_token_123456',
};
// 触发login Action,更新全局用户状态
dispatch(login(userData));
// 跳转回我的页面
navigation.navigate('Mine');
};
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="请输入用户名"
value={name}
onChangeText={setName}
/>
<TouchableOpacity style={styles.loginBtn} onPress={handleLogin}>
<Text style={styles.btnText}>登录</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
justifyContent: 'center',
backgroundColor: '#fff',
},
input: {
height: 48,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
paddingHorizontal: 15,
fontSize: 16,
marginBottom: 20,
},
loginBtn: {
backgroundColor: '#0066cc',
paddingVertical: 14,
borderRadius: 8,
alignItems: 'center',
},
btnText: {
color: '#fff',
fontSize: 16,
fontWeight: '500',
},
});
4. 异步状态处理:Redux-Thunk
实际应用中,状态更新常依赖异步操作(如网络请求),Redux-Thunk是Redux的中间件,支持在Action中编写异步逻辑(@reduxjs/toolkit已内置Thunk)。
(1)编写异步Action
jsx
// store/reducers/userReducer.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { apiLogin } from '../../api/user'; // 模拟接口请求
// 异步Action:登录请求
export const loginAsync = createAsyncThunk(
'user/loginAsync',
async (userInfo, { rejectWithValue }) => {
try {
const response = await apiLogin(userInfo); // 调用登录接口
return response.data; // 成功则返回数据
} catch (error) {
return rejectWithValue(error.message); // 失败则返回错误信息
}
}
);
// 在Slice中处理异步Action
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
// 同步Action...
},
extraReducers: (builder) => {
builder
// 异步请求中
.addCase(loginAsync.pending, (state) => {
state.loading = true;
})
// 异步请求成功
.addCase(loginAsync.fulfilled, (state, action) => {
state.loading = false;
state.isLogin = true;
state.username = action.payload.username;
state.avatar = action.payload.avatar;
state.token = action.payload.token;
})
// 异步请求失败
.addCase(loginAsync.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
},
});
(2)组件中调用异步Action
jsx
// 登录页面中替换为异步登录
const handleLogin = async () => {
if (!name) return;
dispatch(loginAsync({ username: name, password: '123456' }));
};
三、综合实战:多页面应用完整流程
结合路由和状态管理,实现一个包含"首页→登录→我的"的完整流程:
- 未登录时,"我的"页面显示登录按钮,点击跳转登录页;
- 登录页输入用户名,触发Redux异步Action更新用户状态;
- 登录成功后跳转回"我的"页面,显示用户信息;
- 支持切换暗黑/亮色主题,全局页面同步主题样式;
- 点击退出登录,重置用户状态,返回未登录界面。
四、小结与下一阶段预告
本文完成了RN多页面应用的核心技术闭环:通过React Navigation实现了页面跳转与导航管理,通过Redux实现了全局状态共享与异步数据处理。你已具备开发中小型RN应用的能力,能够应对页面导航和数据共享的核心需求。
下一篇文章《RN原生模块交互:打通JS与原生的桥梁》,将带你突破RN的"能力边界",学习如何调用Android/iOS原生模块、集成第三方SDK,实现RN与原生的深度通信,解决复杂的原生能力扩展需求。
如果你在路由配置或状态管理中遇到困惑,可随时留言,我会为你提供针对性的解决方案!