RN路由与状态管理:打造多页面应用

在前两篇文章中,我们掌握了RN的基础组件、Flex布局和简单交互,能够搭建单页面UI。但实际应用往往是多页面结构,还需要在页面间共享数据,这就需要路由管理状态管理技术。本文作为RN体系化专栏的第三篇,将聚焦React Navigation路由库和Redux状态管理方案,带你实现多页面跳转、传参和全局数据共享,完成从“单页面”到“多页面应用”的进阶。

一、路由管理:React Navigation实现页面跳转

RN本身没有内置路由系统,社区主流方案是React Navigation,它提供了栈路由、标签路由、抽屉路由等多种导航模式,支持跨端一致性和灵活的自定义配置。

1. React Navigation环境搭建

(1)安装核心依赖

首先在项目中安装React Navigation的核心包和对应适配器(以最新的React Navigation 6为例):

# 安装核心依赖
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的核心容器:

import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return (
    <NavigationContainer>
      {/* 路由配置将在这里添加 */}
    </NavigationContainer>
  );
}

2. 栈路由(Stack Navigator):基础页面跳转

栈路由是最常用的导航模式,类似于手机的“页面栈”,新页面压入栈顶,返回时弹出栈顶页面,支持页面跳转和传参。

(1)配置栈路由

首先创建两个页面组件(HomePageDetailPage),再配置栈路由:

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中接收参数:

// 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,
  },
});
// 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)安装标签路由依赖
npm install @react-navigation/bottom-tabs
(2)配置底部标签路由
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. 嵌套路由:组合栈路由与标签路由

实际应用中常需要“标签路由+栈路由”嵌套(如首页内可跳转详情页),只需在标签页面中嵌套栈路由即可:

// 为首页配置嵌套栈路由
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)安装依赖
# 安装Redux核心包和React绑定库
npm install redux react-redux @reduxjs/toolkit

@reduxjs/toolkit是Redux官方推荐的工具包,简化了Redux的配置和编写。

(2)创建Redux Store

首先创建store目录,包含index.js(创建Store)和reducers(状态处理器):

// 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的编写:

// 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,用于管理应用的亮色/暗黑模式:

// 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注入应用,使所有组件可访问全局状态:

// 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中读取状态:

// 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更新用户状态:

// 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
// 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
// 登录页面中替换为异步登录
const handleLogin = async () => {
  if (!name) return;
  dispatch(loginAsync({ username: name, password: '123456' }));
};

三、综合实战:多页面应用完整流程

结合路由和状态管理,实现一个包含“首页→登录→我的”的完整流程:

  1. 未登录时,“我的”页面显示登录按钮,点击跳转登录页;
  2. 登录页输入用户名,触发Redux异步Action更新用户状态;
  3. 登录成功后跳转回“我的”页面,显示用户信息;
  4. 支持切换暗黑/亮色主题,全局页面同步主题样式;
  5. 点击退出登录,重置用户状态,返回未登录界面。

四、小结与下一阶段预告

本文完成了RN多页面应用的核心技术闭环:通过React Navigation实现了页面跳转与导航管理,通过Redux实现了全局状态共享与异步数据处理。你已具备开发中小型RN应用的能力,能够应对页面导航和数据共享的核心需求。

下一篇文章《RN原生模块交互:打通JS与原生的桥梁》,将带你突破RN的“能力边界”,学习如何调用Android/iOS原生模块、集成第三方SDK,实现RN与原生的深度通信,解决复杂的原生能力扩展需求。

如果你在路由配置或状态管理中遇到困惑,可随时留言,我会为你提供针对性的解决方案!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐