02 React Native状态、导航、数据流与设备能力

React Native 应用的复杂度通常来自四个方向:状态管理、页面导航、异步数据、设备能力。本章把这些能力串成移动端应用的核心数据流。

1. 状态分类

类型 示例 推荐位置
组件状态 输入框、展开、弹窗 当前组件
页面状态 搜索条件、当前 Tab Screen
领域状态 收藏、学习进度、草稿 Feature model
服务端状态 课程列表、用户信息 Query/cache 层
设备状态 网络、权限、定位、AppState Platform/device Hook
全局配置 主题、语言、登录用户 Provider

2. useState 与 useReducer

简单状态:

jsx 复制代码
const [query, setQuery] = useState('');

复杂状态:

jsx 复制代码
const initialState = {
  query: '',
  level: '全部',
  favorites: [],
  completed: [],
};

function reducer(state, action) {
  switch (action.type) {
    case 'query-changed':
      return { ...state, query: action.query };
    case 'favorite-toggled':
      return {
        ...state,
        favorites: state.favorites.includes(action.id)
          ? state.favorites.filter((id) => id !== action.id)
          : [...state.favorites, action.id],
      };
    default:
      return state;
  }
}

3. Context

jsx 复制代码
const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const value = useMemo(() => ({ theme, setTheme }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

Context 适合主题、语言、当前用户,不适合高频变化的大列表状态。

React Native 没有浏览器 URL 作为默认页面模型。常见导航结构:

  • Stack:页面压栈、返回。
  • Tabs:底部/顶部标签。
  • Drawer:侧边抽屉。
  • Modal:模态页面。
  • Deep Link:外部链接进入指定页面。

概念示例:

jsx 复制代码
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

function AppNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

页面跳转:

jsx 复制代码
navigation.navigate('Detail', { id: item.id });

读取参数:

jsx 复制代码
function DetailScreen({ route }) {
  const { id } = route.params;
  return <LessonDetail id={id} />;
}

5. 导航设计原则

  • 页面参数只放可序列化数据,如 ID、筛选条件。
  • 不要通过 route params 传大对象或函数。
  • 详情页通过 ID 自己读取数据。
  • 权限和登录态在导航层统一处理。
  • Deep Link 与页面参数结构保持一致。

6. Android 返回键

jsx 复制代码
useEffect(() => {
  const subscription = BackHandler.addEventListener(
    'hardwareBackPress',
    () => {
      if (selectionMode) {
        exitSelectionMode();
        return true;
      }
      return false;
    },
  );

  return () => subscription.remove();
}, [selectionMode]);

返回键处理要避免破坏导航默认行为。

7. 异步数据

基础请求 Hook:

jsx 复制代码
function useLessons() {
  const [state, setState] = useState({
    status: 'loading',
    data: [],
    error: null,
  });

  useEffect(() => {
    let ignore = false;

    async function load() {
      try {
        const response = await fetch('https://example.com/api/lessons');
        const data = await response.json();
        if (!ignore) {
          setState({ status: 'success', data, error: null });
        }
      } catch (error) {
        if (!ignore) {
          setState({ status: 'error', data: [], error });
        }
      }
    }

    load();

    return () => {
      ignore = true;
    };
  }, []);

  return state;
}

真实项目推荐 TanStack Query、SWR 或框架数据能力。

8. Loading、Error、Empty

jsx 复制代码
if (state.status === 'loading') {
  return <ActivityIndicator />;
}

if (state.status === 'error') {
  return <ErrorState onRetry={reload} />;
}

if (state.data.length === 0) {
  return <EmptyState />;
}

return <LessonList items={state.data} />;

移动端必须设计:

  • 骨架屏。
  • 下拉刷新。
  • 失败重试。
  • 离线提示。
  • 空状态。

9. RefreshControl

jsx 复制代码
<FlatList
  data={items}
  renderItem={renderItem}
  refreshControl={
    <RefreshControl refreshing={refreshing} onRefresh={reload} />
  }
/>

下拉刷新是移动端常见交互。

10. 分页与无限滚动

jsx 复制代码
<FlatList
  data={items}
  renderItem={renderItem}
  onEndReached={loadMore}
  onEndReachedThreshold={0.4}
  ListFooterComponent={loadingMore ? <ActivityIndicator /> : null}
/>

要处理:

  • 重复触发。
  • 最后一页。
  • 加载中锁。
  • 错误重试。
  • 列表项稳定 key。

11. 本地存储

React Native 核心不再内置 AsyncStorage,常用社区包:

text 复制代码
@react-native-async-storage/async-storage

概念:

jsx 复制代码
async function saveProgress(progress) {
  await AsyncStorage.setItem('progress', JSON.stringify(progress));
}

async function loadProgress() {
  const raw = await AsyncStorage.getItem('progress');
  return raw ? JSON.parse(raw) : null;
}

本地存储适合:

  • 用户偏好。
  • 草稿。
  • 简单缓存。

不适合:

  • 大量结构化数据。
  • 高安全数据。
  • 复杂查询。

12. 离线状态

常用社区库:

text 复制代码
@react-native-community/netinfo

概念:

jsx 复制代码
function useOnlineStatus() {
  const [online, setOnline] = useState(true);

  useEffect(() => {
    const unsubscribe = NetInfo.addEventListener((state) => {
      setOnline(Boolean(state.isConnected));
    });

    return unsubscribe;
  }, []);

  return online;
}

离线策略:

  • 只读缓存。
  • 草稿本地保存。
  • 操作排队。
  • 恢复网络后同步。
  • 冲突解决。

13. AppState

jsx 复制代码
useEffect(() => {
  const subscription = AppState.addEventListener('change', (nextState) => {
    if (nextState === 'active') {
      refreshSensitiveData();
    }
  });

  return () => subscription.remove();
}, []);

用于处理:

  • 前后台切换。
  • 恢复时刷新数据。
  • 暂停计时器。
  • 安全页面遮挡。
jsx 复制代码
Linking.openURL('https://example.com/help');

监听链接:

jsx 复制代码
useEffect(() => {
  const subscription = Linking.addEventListener('url', ({ url }) => {
    handleDeepLink(url);
  });

  return () => subscription.remove();
}, []);

Deep Link 设计要和导航参数对应。

15. 权限

Android 权限:

jsx 复制代码
const granted = await PermissionsAndroid.request(
  PermissionsAndroid.PERMISSIONS.CAMERA,
);

Expo 权限通常由对应模块提供,例如 Camera、Location、Notifications。

权限体验:

  • 请求前说明用途。
  • 被拒绝后给出替代路径。
  • 永久拒绝时引导去设置。
  • 不在应用启动时一次性请求所有权限。

16. 键盘处理

jsx 复制代码
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
  <LessonForm />
</KeyboardAvoidingView>

输入页要处理:

  • 键盘遮挡。
  • 点击空白收起键盘。
  • ScrollView 内表单。
  • returnKeyType
  • 多字段跳转。

17. 表单策略

轻量表单:

jsx 复制代码
const [title, setTitle] = useState('');

复杂表单:

  • React Hook Form。
  • Zod/Yup schema。
  • 字段数组。
  • dirty/touched。
  • 提交防抖和防重复。

移动端表单还要考虑:

  • 键盘类型。
  • 自动大写。
  • 输入法兼容。
  • 错误提示位置。

18. 设备 API 总表

能力 常用方案
摄像头 Expo Camera / vision-camera
相册 Expo ImagePicker
定位 Expo Location
推送 Expo Notifications / FCM/APNs
文件 Expo FileSystem / react-native-fs
安全存储 Expo SecureStore / Keychain
网络状态 NetInfo
设备信息 expo-device / react-native-device-info
生物识别 Expo LocalAuthentication

19. 专家视角:移动端数据流

成熟移动端数据流需要回答:

  • 页面状态和领域状态如何分离?
  • 服务端状态如何缓存和失效?
  • 离线期间用户操作如何保存?
  • 失败后如何重试和回滚?
  • 权限拒绝如何降级?
  • App 从后台恢复时刷新哪些数据?
  • Deep Link 是否能准确还原页面?

20. 导航扩展专题

Stack 适合详情流,Tabs 适合主功能分区,Drawer 适合低频导航。复杂 App 常见结构:

text 复制代码
RootNavigator
├── AuthStack
└── MainTabs
    ├── HomeStack
    ├── LearningStack
    └── SettingsStack

不要把所有页面塞进一个 Stack。按业务域拆 Stack,更容易处理 header、权限和 deep link。

Deep Link 配置要和路由参数一致:

js 复制代码
const linking = {
  prefixes: ['myapp://', 'https://example.com'],
  config: {
    screens: {
      LessonDetail: 'lessons/:id',
    },
  },
};

21. 登录态与导航重置

登录成功后不要简单 navigate 到首页,通常需要 reset navigation state:

jsx 复制代码
navigation.reset({
  index: 0,
  routes: [{ name: 'MainTabs' }],
});

退出登录也要清理:

  • token。
  • query cache。
  • 本地敏感缓存。
  • navigation state。

22. 权限体验扩展

权限请求流程:

  1. 先用业务 UI 解释为什么需要权限。
  2. 用户确认后调用系统权限。
  3. 拒绝时提供无权限模式。
  4. 永久拒绝时引导去设置。
jsx 复制代码
function PermissionGate({ granted, request, children }) {
  if (granted) return children;

  return (
    <View>
      <Text>需要相机权限用于扫码学习</Text>
      <Pressable onPress={request}>
        <Text>授权</Text>
      </Pressable>
    </View>
  );
}

23. 离线同步扩展

离线 outbox:

ts 复制代码
type OutboxItem = {
  id: string;
  action: 'complete' | 'favorite';
  lessonId: string;
  payload: unknown;
  createdAt: number;
  retryCount: number;
};

同步策略:

  • 网络恢复后按时间顺序同步。
  • 幂等接口避免重复提交。
  • 失败增加 retryCount。
  • 达到上限进入人工处理或提示用户。
  • 冲突时展示服务端版本和本地版本。

24. 设备状态组合

真实移动端经常要组合多个设备状态:

  • 网络断开。
  • App 进入后台。
  • token 过期。
  • 权限被系统收回。
  • 定位服务关闭。
  • 用户开启省电模式。

示例策略:

text 复制代码
App active + online + authenticated -> 刷新关键数据
App background -> 暂停轮询
offline -> 读取缓存并保存 outbox
permission denied -> 显示降级 UI

25. 数据流评审题

  • route params 是否只传 ID 和简单参数?
  • 登录/登出是否 reset navigation?
  • Deep Link 是否能冷启动进入正确页面?
  • 权限拒绝后是否有降级体验?
  • 离线操作是否会丢失?
  • 恢复网络后是否会重复提交?
  • App 回前台是否刷新关键数据?

26. 移动端状态知识点索引

  1. 页面状态属于 Screen。
  2. 导航参数只传可序列化数据。
  3. 登录成功应 reset navigation。
  4. 登出要清理缓存和导航栈。
  5. Deep Link 要支持冷启动。
  6. Android 返回键可能覆盖业务状态。
  7. 权限请求要延迟。
  8. 权限拒绝要降级。
  9. 网络状态影响数据策略。
  10. AppState 影响刷新和暂停。
  11. 键盘影响布局。
  12. 本地存储不适合敏感数据。
  13. 离线操作要进 outbox。
  14. mutation 要幂等。
  15. 恢复网络要同步。
  16. token 过期要统一处理。
  17. 推送点击应进入指定页面。
  18. 定位、相机、相册都需要权限说明。
  19. 表单要防重复提交。
  20. 服务端状态不应散落在组件里。

27. 设备能力反模式

  • 启动时一次性请求所有权限。
  • Deep Link 只能在 app 已启动时工作。
  • route params 传完整对象。
  • 离线时直接禁止所有操作。
  • 网络恢复后重复提交。
  • App 回前台不刷新敏感数据。
  • 键盘遮住输入框。
  • Android 返回键退出了本应关闭的选择模式。

面试题完整答案总集:React Native 状态、导航、数据与设备

route params 是否只传 ID 和简单参数?

应该如此。导航参数需要可序列化、可恢复,适合传 ID、筛选条件、来源标记等简单值。不应传完整对象或函数,因为对象可能过期、无法持久化,也会影响 deep link、状态恢复和调试。

需要。登录后应重置到主应用栈,避免用户返回登录页;登出后应重置到认证栈,避免返回受保护页面。同时应清理 token、用户缓存、query cache、本地敏感数据和导航状态。

合格的 Deep Link 必须支持 app 未启动、后台、前台三种状态。冷启动时需要解析初始 URL,建立导航状态,并根据参数加载对应数据。只处理运行时 URL 监听是不完整的。

权限拒绝后是否有降级体验?

必须有。权限可能被拒绝或永久拒绝。应用应先解释用途,再请求权限;拒绝后提供无权限模式;永久拒绝时引导去系统设置。不能因为用户拒绝相机、定位或通知就让整个应用不可用。

离线操作是否会丢失?

不应丢失。离线操作应写入本地 outbox,并在恢复网络后同步。每个操作应有唯一 ID、时间、重试次数和状态。服务端接口应幂等,避免重复提交造成错误。

恢复网络后是否会重复提交?

如果没有幂等 ID 和同步状态,就可能重复提交。正确方案是每个 outbox 操作带 clientMutationId,服务端按 ID 去重;客户端成功后标记 synced,失败后增加 retryCount 并保留错误。

App 回前台是否刷新关键数据?

应根据业务刷新。App 从后台回到 active 时,用户、权限、通知、支付状态、敏感数据和过期缓存可能需要刷新。但不要无脑刷新所有接口,应根据数据过期时间和页面重要性决定。

Android 返回键如何处理?

Android 返回键默认走导航返回。如果当前页面有选择模式、弹窗、搜索激活等临时状态,可以拦截返回键先关闭这些状态,并返回 true。否则返回 false 交给导航系统处理。

相关推荐
空中海2 小时前
02 状态、Hooks、副作用与数据流
开发语言·javascript·ecmascript
空中海2 小时前
04 React Native工程化、质量、发布与生态选型
javascript·react native·react.js
杨超凡3 小时前
豆包收费了?我特么自己用“意念”搓了一个!
javascript
threelab4 小时前
Three.js 咖啡杯烟雾效果 | 三维可视化 / AI 提示词
开发语言·javascript·人工智能
Heo4 小时前
14_React 中的更新队列 updateQueue
前端·javascript·面试
前端 贾公子4 小时前
解决浏览器端 globalThis is not defined 报错
前端·javascript·vue.js
之歆5 小时前
DAY12_CSS3选择器全攻略 + 盒子新特性完全指南(下)
前端·javascript·css3
kyriewen115 小时前
代码写成一锅粥?3个设计模式让你的项目“起死回生”
开发语言·前端·javascript·设计模式·ecmascript