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 适合主题、语言、当前用户,不适合高频变化的大列表状态。
4. React Navigation 心智模型
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();
}, []);
用于处理:
- 前后台切换。
- 恢复时刷新数据。
- 暂停计时器。
- 安全页面遮挡。
14. Linking 与 Deep Link
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. 权限体验扩展
权限请求流程:
- 先用业务 UI 解释为什么需要权限。
- 用户确认后调用系统权限。
- 拒绝时提供无权限模式。
- 永久拒绝时引导去设置。
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. 移动端状态知识点索引
- 页面状态属于 Screen。
- 导航参数只传可序列化数据。
- 登录成功应 reset navigation。
- 登出要清理缓存和导航栈。
- Deep Link 要支持冷启动。
- Android 返回键可能覆盖业务状态。
- 权限请求要延迟。
- 权限拒绝要降级。
- 网络状态影响数据策略。
- AppState 影响刷新和暂停。
- 键盘影响布局。
- 本地存储不适合敏感数据。
- 离线操作要进 outbox。
- mutation 要幂等。
- 恢复网络要同步。
- token 过期要统一处理。
- 推送点击应进入指定页面。
- 定位、相机、相册都需要权限说明。
- 表单要防重复提交。
- 服务端状态不应散落在组件里。
27. 设备能力反模式
- 启动时一次性请求所有权限。
- Deep Link 只能在 app 已启动时工作。
- route params 传完整对象。
- 离线时直接禁止所有操作。
- 网络恢复后重复提交。
- App 回前台不刷新敏感数据。
- 键盘遮住输入框。
- Android 返回键退出了本应关闭的选择模式。
面试题完整答案总集:React Native 状态、导航、数据与设备
route params 是否只传 ID 和简单参数?
应该如此。导航参数需要可序列化、可恢复,适合传 ID、筛选条件、来源标记等简单值。不应传完整对象或函数,因为对象可能过期、无法持久化,也会影响 deep link、状态恢复和调试。
登录/登出是否需要 reset navigation?
需要。登录后应重置到主应用栈,避免用户返回登录页;登出后应重置到认证栈,避免返回受保护页面。同时应清理 token、用户缓存、query cache、本地敏感数据和导航状态。
Deep Link 是否能冷启动进入正确页面?
合格的 Deep Link 必须支持 app 未启动、后台、前台三种状态。冷启动时需要解析初始 URL,建立导航状态,并根据参数加载对应数据。只处理运行时 URL 监听是不完整的。
权限拒绝后是否有降级体验?
必须有。权限可能被拒绝或永久拒绝。应用应先解释用途,再请求权限;拒绝后提供无权限模式;永久拒绝时引导去系统设置。不能因为用户拒绝相机、定位或通知就让整个应用不可用。
离线操作是否会丢失?
不应丢失。离线操作应写入本地 outbox,并在恢复网络后同步。每个操作应有唯一 ID、时间、重试次数和状态。服务端接口应幂等,避免重复提交造成错误。
恢复网络后是否会重复提交?
如果没有幂等 ID 和同步状态,就可能重复提交。正确方案是每个 outbox 操作带 clientMutationId,服务端按 ID 去重;客户端成功后标记 synced,失败后增加 retryCount 并保留错误。
App 回前台是否刷新关键数据?
应根据业务刷新。App 从后台回到 active 时,用户、权限、通知、支付状态、敏感数据和过期缓存可能需要刷新。但不要无脑刷新所有接口,应根据数据过期时间和页面重要性决定。
Android 返回键如何处理?
Android 返回键默认走导航返回。如果当前页面有选择模式、弹窗、搜索激活等临时状态,可以拦截返回键先关闭这些状态,并返回 true。否则返回 false 交给导航系统处理。