开篇:让数据活过重启

前九课的应用有一个共同的"缺陷":数据活不过一次重启。

待办事项,用户添加了十项,关掉再打开,全没了。登录页面,用户登录成功,关掉再打开,又要重新登录。设置页面,用户切成暗色模式,关掉再打开,又变回亮色。

这就是"持久化"要解决的问题:把数据保存到设备上,让它在 App 关闭后依然存在。

React Native 提供了多种方案,各有各的适用场景。这一课的核心可以浓缩成一句话:

// 概念表达
机密用 SecureStore,速度用 MMKV,兼容用 AsyncStorage,大文件用 FileSystem

一、四种存储方案对比

特性AsyncStorageSecureStoreFileSystemMMKV
安全性明文加密(Keychain/Keystore)取决于内容明文
容量几 MB 到几十 MB约 2KB取决于设备大
读写异步异步异步同步
适合数据偏好、设置、缓存令牌、密码、API Key图片、日志、大 JSON高频读写状态
Expo Go支持支持支持不支持

两个关键警告:

  • SecureStore 有约 2KB 的大小限制。不要把大 JSON、大缓存放进去。
  • AsyncStorage 不加密。令牌、密码等敏感信息绝不能放在里面。

2026 年的选型建议

  • Auth tokens、API keys:用 expo-secure-store;
  • 主题、语言、Zustand/Jotai 持久化状态:优先考虑 react-native-mmkv;
  • 兼容旧代码、Expo Go 快速原型:用 AsyncStorage;
  • 图片、日志、大文件:用 expo-file-system。

MMKV 速度约为 AsyncStorage 的 30 倍,而且是同步的。但它需要开发构建,不能在 Expo Go 中使用。


二、AsyncStorage:最通用的键值存储

2.1 安装

# Bash 终端命令
npx expo install @react-native-async-storage/async-storage          # 安装 AsyncStorage

注意:Expo SDK 54+ 的某些版本可能存在兼容性问题。如果遇到问题,可以锁定到 2.2.0,或者迁移到 MMKV。

2.2 基本用法

所有操作都是异步的,需要 await。

// JavaScript + React Native
import AsyncStorage from '@react-native-async-storage/async-storage'; // 引入 AsyncStorage

await AsyncStorage.setItem('theme', 'dark');                        // 保存主题
const theme = await AsyncStorage.getItem('theme');                  // 读取主题
await AsyncStorage.removeItem('theme');                             // 删除主题
await AsyncStorage.clear();                                         // 清空所有

2.3 保存对象

AsyncStorage 只能保存字符串。对象需要序列化。

// JavaScript + React Native
const user = { name: '小明', age: 18 };                             // 用户对象

await AsyncStorage.setItem('user', JSON.stringify(user));           // 保存对象

const json = await AsyncStorage.getItem('user');                    // 读取字符串
const user = json ? JSON.parse(json) : null;                        // 先判断再解析

如果 getItem 返回 null,JSON.parse(null) 会出错,所以要先判断。

2.4 封装成 Hook

更 React 的方式是封装成 Hook,自动在挂载时读取、变化时保存:

// JavaScript + React Native
import { useState, useEffect, useRef } from 'react';                // 引入 Hook
import AsyncStorage from '@react-native-async-storage/async-storage'; // 引入 AsyncStorage

export function useAsyncStorage(key, defaultValue) {                // 定义 Hook
  const [value, setValue] = useState(defaultValue);                 // 创建状态
  const [loaded, setLoaded] = useState(false);                      // 加载状态
  const isFirst = useRef(true);                                     // 记录是否首次渲染

  useEffect(() => {                                                 // 挂载时读取
    async function load() {                                         // 定义加载函数
      try {                                                         // 开始 try
        const json = await AsyncStorage.getItem(key);               // 读取字符串
        if (json !== null) setValue(JSON.parse(json));              // 有值就更新状态
      } catch (e) {                                                 // 捕获错误
        console.error('读取失败', e);                                // 打印错误
      } finally {                                                   // 收尾
        setLoaded(true);                                            // 标记已加载
      }                                                             // 结束 try
    }                                                               // 结束 load
    load();                                                         // 调用加载
  }, [key]);                                                        // 依赖 key

  useEffect(() => {                                                 // 值变化时保存
    if (!loaded) return;                                            // 未加载完成不保存
    if (isFirst.current) {                                          // 首次跳过
      isFirst.current = false;                                      // 标记
      return;                                                       // 返回
    }                                                               // 结束判断
    AsyncStorage.setItem(key, JSON.stringify(value)).catch(console.error);  // 保存值
  }, [key, value, loaded]);                                         // 依赖

  return [value, setValue, loaded];                                 // 返回接口
}                                                                   // 结束 useAsyncStorage

使用时:

// JavaScript + React Native
const [theme, setTheme, loaded] = useAsyncStorage('theme', 'light');  // 使用 Hook

if (!loaded) return <ActivityIndicator />;                          // 未加载时显示指示器

loaded 状态避免"闪烁":数据还没读出来之前,不要显示默认值。


三、SecureStore:安全存储敏感数据

3.1 安装

# Bash 终端命令
npx expo install expo-secure-store                                 # 安装 SecureStore

3.2 基本用法

方法名带 Async 后缀,只支持字符串。

// JavaScript + React Native
import * as SecureStore from 'expo-secure-store';                   // 引入 SecureStore

await SecureStore.setItemAsync('authToken', token);                 // 保存令牌
const token = await SecureStore.getItemAsync('authToken');          // 读取令牌
await SecureStore.deleteItemAsync('authToken');                     // 删除令牌

3.3 认证 Context 完整封装

// JavaScript + React Native
import { createContext, useContext, useState, useEffect } from 'react';  // 引入 Hook
import * as SecureStore from 'expo-secure-store';                   // 引入 SecureStore

const AuthContext = createContext(null);                            // 创建 Context

export function AuthProvider({ children }) {                        // 定义 Provider
  const [token, setToken] = useState(null);                         // 令牌状态
  const [isLoading, setIsLoading] = useState(true);                 // 加载状态

  useEffect(() => {                                                 // 挂载时读取令牌
    async function load() {                                         // 定义加载函数
      const stored = await SecureStore.getItemAsync('authToken');   // 读取令牌
      setToken(stored);                                             // 更新状态
      setIsLoading(false);                                          // 关闭加载
    }                                                               // 结束 load
    load();                                                         // 调用加载
  }, []);                                                           // 空数组

  const signIn = async (newToken) => {                              // 登录
    await SecureStore.setItemAsync('authToken', newToken);          // 保存令牌
    setToken(newToken);                                             // 更新状态
  };                                                                // 结束 signIn

  const signOut = async () => {                                     // 退出
    await SecureStore.deleteItemAsync('authToken');                 // 删除令牌
    setToken(null);                                                 // 清空状态
  };                                                                // 结束 signOut

  return (                                                          // 返回界面描述
    <AuthContext.Provider value={{ token, isLoading, signIn, signOut }}>  // 提供接口
      {children}                                                    // 渲染子组件
    </AuthContext.Provider>                                         // 结束 Provider
  );                                                                // 结束 return
}                                                                   // 结束 AuthProvider

export function useAuth() {                                         // 定义 Hook
  const ctx = useContext(AuthContext);                              // 读取 Context
  if (!ctx) throw new Error('useAuth 必须在 AuthProvider 内使用');  // 检查
  return ctx;                                                       // 返回 Context
}                                                                   // 结束 useAuth

3.4 生物认证

SecureStore 支持 requireAuthentication,要求用户通过 Face ID 或指纹解锁后才能读取:

// JavaScript + React Native
await SecureStore.setItemAsync('secret', value, {                   // 保存时设置
  requireAuthentication: true,                                      // 需要生物认证
  authenticationPrompt: '请验证身份以访问',                          // 提示文字
});                                                                 // 结束

Expo Go 中可能不支持生物认证,需要开发构建才能完整测试。


四、FileSystem:读写文件

4.1 安装

# Bash 终端命令
npx expo install expo-file-system                                 # 安装 FileSystem

4.2 新旧 API

旧版 API 保留在 expo-file-system/legacy。本文介绍新版 API。

4.3 目录路径

// JavaScript + React Native
import { Paths } from 'expo-file-system';                           // 引入 Paths

Paths.document                                                     // 文档目录,持久化,不会被系统清除
Paths.cache                                                        // 缓存目录,系统可能在空间不足时清除

4.4 写入和读取

// JavaScript + React Native
import { File, Paths } from 'expo-file-system';                     // 引入 File 和 Paths

const file = new File(Paths.document, 'notes.txt');                 // 创建文件引用
file.write('这是笔记内容');                                          // 写入内容

const content = file.text();                                        // 读取内容
if (file.exists) console.log('文件存在');                            // 检查是否存在
file.delete();                                                      // 删除文件

4.5 保存和读取 JSON

// JavaScript + React Native
import { File, Paths } from 'expo-file-system';                     // 引入 File 和 Paths

const data = { name: '小明', age: 18 };                             // 要保存的数据

const file = new File(Paths.document, 'user.json');                 // 创建文件引用
file.write(JSON.stringify(data));                                   // 写入 JSON

const json = file.text();                                           // 读取文本
const user = JSON.parse(json);                                      // 解析对象

4.6 下载文件

// JavaScript + React Native
import { File, Paths } from 'expo-file-system';                     // 引入 File 和 Paths

const url = 'https://example.com/image.jpg';                        // 下载地址
const destination = new File(Paths.document, 'downloaded.jpg');     // 目标文件

const downloaded = await File.downloadFileAsync(url, destination);  // 下载文件
console.log('下载完成', downloaded.uri);                             // 打印 URI

4.7 适合的场景

  • 下载的图片、音频、视频;
  • 日志文件;
  • 超过 2KB 的敏感数据(加密后存储);
  • 缓存大量 JSON。

五、综合示例:持久化设置页

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 Hook
import { View, Text, Switch, Button, ActivityIndicator, StyleSheet } from 'react-native';  // 引入组件
import AsyncStorage from '@react-native-async-storage/async-storage';  // 引入 AsyncStorage
import * as SecureStore from 'expo-secure-store';                   // 引入 SecureStore

export default function SettingsScreen() {                          // 定义设置组件
  const [darkMode, setDarkMode] = useState(false);                  // 暗色模式状态
  const [notifications, setNotifications] = useState(true);         // 通知状态
  const [token, setToken] = useState(null);                         // 令牌状态
  const [loading, setLoading] = useState(true);                     // 加载状态

  useEffect(() => {                                                 // 挂载时加载
    async function load() {                                         // 定义加载函数
      try {                                                         // 开始 try
        const [themeJson, notifJson, storedToken] = await Promise.all([  // 并行读取
          AsyncStorage.getItem('darkMode'),                         // 读取暗色模式
          AsyncStorage.getItem('notifications'),                    // 读取通知设置
          SecureStore.getItemAsync('authToken'),                    // 读取令牌
        ]);                                                         // 结束并行
        if (themeJson !== null) setDarkMode(JSON.parse(themeJson)); // 更新暗色模式
        if (notifJson !== null) setNotifications(JSON.parse(notifJson));  // 更新通知
        setToken(storedToken);                                      // 更新令牌
      } catch (e) {                                                 // 捕获错误
        console.error('加载设置失败', e);                            // 打印错误
      } finally {                                                   // 收尾
        setLoading(false);                                          // 关闭加载
      }                                                             // 结束 try
    }                                                               // 结束 load
    load();                                                         // 调用加载
  }, []);                                                           // 空数组

  const toggleDarkMode = async (value) => {                         // 切换暗色模式
    setDarkMode(value);                                             // 更新状态
    await AsyncStorage.setItem('darkMode', JSON.stringify(value));  // 保存值
  };                                                                // 结束 toggleDarkMode

  const handleSignOut = async () => {                               // 退出登录
    await SecureStore.deleteItemAsync('authToken');                 // 删除令牌
    setToken(null);                                                 // 清空状态
  };                                                                // 结束 handleSignOut

  if (loading) {                                                    // 加载中
    return <View style={styles.center}><ActivityIndicator size="large" /></View>;  // 显示指示器
  }                                                                 // 结束判断

  return (                                                          // 返回界面描述
    <View style={[styles.container, darkMode && styles.dark]}>      // 外层容器
      <Text style={[styles.heading, darkMode && styles.darkText]}>设置</Text>  // 标题

      <View style={styles.row}>                                     // 暗色模式行
        <Text style={[styles.label, darkMode && styles.darkText]}>暗色模式</Text>  // 标签
        <Switch value={darkMode} onValueChange={toggleDarkMode} />  // 开关
      </View>                                                       // 结束行

      <View style={styles.section}>                                 // 登录状态区
        <Text style={[styles.label, darkMode && styles.darkText]}>   // 状态文字
          登录状态:{token ? '已登录' : '未登录'}                    // 显示状态
        </Text>                                                     // 结束文字
        {token && <Button title="退出登录" onPress={handleSignOut} />}  // 退出按钮
      </View>                                                       // 结束登录状态区
    </View>                                                         // 结束容器
  );                                                                // 结束 return
}                                                                   // 结束 SettingsScreen

const styles = StyleSheet.create({                                  // 创建样式表
  container: { flex: 1, padding: 20, paddingTop: 60, backgroundColor: '#fff' },  // 容器样式
  dark: { backgroundColor: '#222' },                                // 暗色模式
  center: { flex: 1, justifyContent: 'center', alignItems: 'center' },  // 居中容器
  heading: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 },  // 标题样式
  darkText: { color: '#fff' },                                      // 暗色文字
  row: {                                                            // 行样式
    flexDirection: 'row',                                           // 水平排列
    justifyContent: 'space-between',                                // 两端对齐
    alignItems: 'center',                                           // 垂直居中
    paddingVertical: 16,                                            // 垂直内边距
    borderBottomWidth: 1,                                           // 底部分隔线
    borderBottomColor: '#eee',                                      // 分隔线颜色
  },                                                                // 结束行样式
  label: { fontSize: 16 },                                          // 标签样式
  section: { marginTop: 32, gap: 12 },                              // 区块样式
});                                                                 // 结束样式表

两种存储的配合:主题用 AsyncStorage,令牌用 SecureStore。


六、存储与"视图 = 函数(状态)"

本地存储没有打破"视图 = 函数(状态)“,它只是给状态增加了一个"持久层”:

// 概念表达
本地存储 <-> 状态 <-> 视图                                           // 三者循环
  1. 组件挂载时,从本地存储读取数据,更新状态;
  2. 状态变化,界面更新;
  3. 用户操作改变状态;
  4. 状态变化时,把新值写入本地存储。

本地存储是状态的"外部记忆"。它让状态在 App 关闭后依然存在。下次启动时,状态从存储中恢复,界面也恢复。


七、练习题

练习题1:选择题——存储方案选型

题目:下面哪种数据最适合用 expo-secure-store 存储?

A. 用户的暗色模式偏好
B. 登录后的认证令牌
C. 一张下载的图片
D. 一篇很长的文章缓存

参考答案:B

解读:SecureStore 适合存储令牌、密码、API Key 等敏感的小数据。


练习题2:填空题——AsyncStorage 保存对象

题目:补全代码,把对象 user 保存到 AsyncStorage。

// JavaScript + React Native
await AsyncStorage.setItem('user', ____);                           // 补全保存逻辑

参考答案:JSON.stringify(user)

解读:AsyncStorage 只能保存字符串。对象需要先 JSON.stringify 转换。


练习题3:改错题——读取对象时的空值处理

题目:下面代码在键不存在时会崩溃。请指出问题并修正。

// JavaScript + React Native
const json = await AsyncStorage.getItem('user');                    // 读取字符串
const user = JSON.parse(json);                                      // 解析对象

参考答案:getItem 在键不存在时返回 null。应该写成 const user = json ? JSON.parse(json) : null;。

解读:读取存储数据时,一定要处理键不存在的情况。


练习题4:代码阅读题——SecureStore 大小限制

题目:为什么不应该把一张图片的 base64 字符串存到 SecureStore 里?

参考答案:SecureStore 有约 2KB 的大小限制,过大的值可能被底层平台拒绝。

解读:大文件应该用 FileSystem,文件路径可以存在 SecureStore 里。


练习题5:动手题——保存主题偏好

题目:用 AsyncStorage 保存用户的主题偏好('light' 或 'dark'),并在组件挂载时读取。

参考答案:

// JavaScript + React Native
export function useTheme() {                                        // 定义 Hook
  const [theme, setTheme] = useState('light');                      // 主题状态
  const [loaded, setLoaded] = useState(false);                      // 加载状态

  useEffect(() => {                                                 // 挂载时读取
    async function load() {                                         // 定义加载函数
      const saved = await AsyncStorage.getItem('theme');            // 读取主题
      if (saved) setTheme(saved);                                   // 有值就更新
      setLoaded(true);                                              // 标记已加载
    }                                                               // 结束 load
    load();                                                         // 调用加载
  }, []);                                                           // 空数组

  const toggleTheme = async () => {                                 // 切换主题
    const next = theme === 'light' ? 'dark' : 'light';              // 计算下一个主题
    setTheme(next);                                                 // 更新状态
    await AsyncStorage.setItem('theme', next);                      // 保存主题
  };                                                                // 结束 toggleTheme

  return { theme, toggleTheme, loaded };                            // 返回接口
}                                                                   // 结束 useTheme

解读:这是持久化偏好设置的标准模式。loaded 避免闪烁。


练习题6:简答题——AsyncStorage 和 SecureStore 的区别

题目:简述 AsyncStorage 和 SecureStore 在安全性和适用场景上的区别。

参考答案:AsyncStorage 是明文存储,适合非敏感数据。SecureStore 使用设备的安全硬件加密,适合敏感数据,但有约 2KB 大小限制。

解读:选择存储方案时,安全性是第一考量。


练习题7:SecureStore 基础题

题目:用 SecureStore 保存、读取和删除一个令牌。

参考答案:

// JavaScript + React Native
await SecureStore.setItemAsync('authToken', token);                 // 保存
const token = await SecureStore.getItemAsync('authToken');          // 读取
await SecureStore.deleteItemAsync('authToken');                     // 删除

解读:SecureStore 的 API 类似 AsyncStorage,但方法名有 Async 后缀。


练习题8:封装自定义 Hook 题

题目:封装一个 useAsyncStorage Hook,接收 key 和 defaultValue。

参考答案:

// JavaScript + React Native
function useAsyncStorage(key, defaultValue) {                       // 定义 Hook
  const [value, setValue] = useState(defaultValue);                 // 创建状态
  const [loaded, setLoaded] = useState(false);                      // 加载状态

  useEffect(() => {                                                 // 挂载时读取
    async function load() {                                         // 定义加载函数
      const json = await AsyncStorage.getItem(key);                 // 读取
      if (json !== null) setValue(JSON.parse(json));                // 有值就更新
      setLoaded(true);                                              // 标记已加载
    }                                                               // 结束 load
    load();                                                         // 调用加载
  }, [key]);                                                        // 依赖 key

  useEffect(() => {                                                 // 值变化时保存
    if (loaded) AsyncStorage.setItem(key, JSON.stringify(value)).catch(console.error);  // 保存
  }, [key, value, loaded]);                                         // 依赖

  return [value, setValue, loaded];                                 // 返回接口
}                                                                   // 结束 useAsyncStorage

解读:这个 Hook 把"读取-状态-保存"流程封装起来,使用时像 useState 一样自然。


练习题9:FileSystem 写入题

题目:用 expo-file-system 把一段文本写入文档目录下的 notes.txt,然后读出来。

参考答案:

// JavaScript + React Native
import { File, Paths } from 'expo-file-system';                     // 引入组件

const file = new File(Paths.document, 'notes.txt');                 // 创建文件引用
file.write('这是一条笔记');                                          // 写入

const content = file.text();                                        // 读取
console.log(content);                                               // 打印

解读:Paths.document 是持久化目录,文件不会被系统自动清除。


练习题10:FileSystem JSON 题

题目:用 FileSystem 保存一个 settings 对象,并读取回来。

参考答案:

// JavaScript + React Native
const settings = { theme: 'dark', fontSize: 16 };                   // 设置对象

const file = new File(Paths.document, 'settings.json');             // 创建文件引用
file.write(JSON.stringify(settings));                               // 写入 JSON

const json = file.text();                                           // 读取文本
const loaded = JSON.parse(json);                                    // 解析对象

解读:FileSystem 适合存储较大的 JSON 数据。


练习题11:认证持久化题

题目:用 SecureStore 和 Context 实现认证状态的持久化。

参考答案:

// JavaScript + React Native
export function AuthProvider({ children }) {                        // 定义 Provider
  const [token, setToken] = useState(null);                         // 令牌状态
  const [loading, setLoading] = useState(true);                     // 加载状态

  useEffect(() => {                                                 // 挂载时读取
    SecureStore.getItemAsync('authToken')                           // 读取令牌
      .then((t) => setToken(t))                                     // 更新状态
      .finally(() => setLoading(false));                            // 关闭加载
  }, []);                                                           // 空数组

  const signIn = async (t) => {                                     // 登录
    await SecureStore.setItemAsync('authToken', t);                 // 保存令牌
    setToken(t);                                                    // 更新状态
  };                                                                // 结束 signIn

  const signOut = async () => {                                     // 退出
    await SecureStore.deleteItemAsync('authToken');                 // 删除令牌
    setToken(null);                                                 // 清空状态
  };                                                                // 结束 signOut

  return (                                                          // 返回界面描述
    <AuthContext.Provider value={{ token, loading, signIn, signOut }}>  // 提供接口
      {children}                                                    // 渲染子组件
    </AuthContext.Provider>                                         // 结束 Provider
  );                                                                // 结束 return
}                                                                   // 结束 AuthProvider

解读:这是认证持久化的标准模式。loading 状态在令牌读取完成前阻止渲染。


练习题12:安全性判断题

题目:下面的做法有什么安全问题?

// JavaScript + React Native
await AsyncStorage.setItem('authToken', token);                     // 保存令牌

参考答案:AsyncStorage 是明文存储的,认证令牌可以被提取。应该使用 expo-secure-store。

解读:攻击者可以从设备上提取令牌并重放会话。


练习题13:存储选型题

题目:为下面的数据选择最合适的存储方案:

  1. 用户的暗色模式偏好;
  2. 登录后的 JWT 令牌;
  3. 用户下载的 10MB 视频文件;
  4. 一个需要频繁读写的购物车状态。

参考答案:

  1. AsyncStorage(或 MMKV);
  2. SecureStore;
  3. FileSystem;
  4. MMKV(如果使用开发构建),否则 AsyncStorage。

解读:非敏感小数据用 AsyncStorage,敏感小数据用 SecureStore,大文件用 FileSystem,高频读写状态用 MMKV。


练习题14:综合题——带持久化的待办事项

题目:改造第3课的待办事项应用,用 AsyncStorage 持久化 todos 数组。

参考答案:

// JavaScript + React Native
const STORAGE_KEY = 'todos';                                        // 存储键名

export default function App() {                                     // 定义并默认导出 App
  const [todos, setTodos] = useState([]);                           // 待办状态
  const [loaded, setLoaded] = useState(false);                      // 加载状态
  const isFirst = useRef(true);                                     // 是否首次渲染

  useEffect(() => {                                                 // 挂载时读取
    async function load() {                                         // 定义加载函数
      const json = await AsyncStorage.getItem(STORAGE_KEY);         // 读取
      if (json) setTodos(JSON.parse(json));                         // 有值就更新
      setLoaded(true);                                              // 标记已加载
    }                                                               // 结束 load
    load();                                                         // 调用加载
  }, []);                                                           // 空数组

  useEffect(() => {                                                 // 值变化时保存
    if (!loaded) return;                                            // 未加载完成不保存
    if (isFirst.current) {                                          // 首次跳过
      isFirst.current = false;                                      // 标记
      return;                                                       // 返回
    }                                                               // 结束判断
    AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(todos)).catch(console.error);  // 保存
  }, [todos, loaded]);                                              // 依赖

  // ... 其余 UI 代码
}                                                                   // 结束 App

解读:两个 useEffect 分别负责读取和保存。isFirst 避免首次读取后立即触发保存。


练习题15:综合应用题——带持久化的用户资料编辑器

题目:创建一个用户资料编辑器,包含:

  1. 昵称(非敏感,用 AsyncStorage);
  2. API Key(敏感,用 SecureStore);
  3. 一个"保存"按钮,同时保存两项;
  4. App 启动时自动恢复。

参考答案:

// JavaScript + React Native
import { useState, useEffect } from 'react';                        // 引入 Hook
import { View, Text, TextInput, Button, ActivityIndicator, StyleSheet } from 'react-native';  // 引入组件
import AsyncStorage from '@react-native-async-storage/async-storage';  // 引入 AsyncStorage
import * as SecureStore from 'expo-secure-store';                   // 引入 SecureStore

export default function App() {                                     // 定义并默认导出 App
  const [nickname, setNickname] = useState('');                     // 昵称状态
  const [apiKey, setApiKey] = useState('');                         // API Key 状态
  const [loading, setLoading] = useState(true);                     // 加载状态
  const [saved, setSaved] = useState(false);                        // 保存成功状态

  useEffect(() => {                                                 // 挂载时读取
    async function load() {                                         // 定义加载函数
      try {                                                         // 开始 try
        const [name, key] = await Promise.all([                     // 并行读取
          AsyncStorage.getItem('nickname'),                         // 读取昵称
          SecureStore.getItemAsync('apiKey'),                       // 读取 API Key
        ]);                                                         // 结束并行
        if (name) setNickname(name);                                // 更新昵称
        if (key) setApiKey(key);                                    // 更新 API Key
      } catch (e) {                                                 // 捕获错误
        console.error('加载失败', e);                                // 打印错误
      } finally {                                                   // 收尾
        setLoading(false);                                          // 关闭加载
      }                                                             // 结束 try
    }                                                               // 结束 load
    load();                                                         // 调用加载
  }, []);                                                           // 空数组

  const handleSave = async () => {                                  // 定义保存函数
    try {                                                           // 开始 try
      await Promise.all([                                           // 并行保存
        AsyncStorage.setItem('nickname', nickname),                 // 保存昵称
        SecureStore.setItemAsync('apiKey', apiKey),                 // 保存 API Key
      ]);                                                           // 结束并行
      setSaved(true);                                               // 显示保存成功
      setTimeout(() => setSaved(false), 2000);                      // 2 秒后隐藏
    } catch (e) {                                                   // 捕获错误
      console.error('保存失败', e);                                  // 打印错误
    }                                                               // 结束 try
  };                                                                // 结束 handleSave

  if (loading) {                                                    // 加载中
    return <View style={styles.center}><ActivityIndicator size="large" /></View>;  // 显示指示器
  }                                                                 // 结束判断

  return (                                                          // 返回界面描述
    <View style={styles.container}>                                 // 外层容器
      <Text style={styles.heading}>用户资料</Text>                   // 标题

      <Text style={styles.label}>昵称(非敏感)</Text>                // 昵称标签
      <TextInput                                                    // 昵称输入框
        style={styles.input}                                        // 应用样式
        value={nickname}                                            // 值由 nickname 决定
        onChangeText={setNickname}                                  // 输入变化时更新
        placeholder="请输入昵称"                                     // 占位提示
      />                                                            // 结束输入框

      <Text style={styles.label}>API Key(敏感)</Text>              // API Key 标签
      <TextInput                                                    // API Key 输入框
        style={styles.input}                                        // 应用样式
        value={apiKey}                                              // 值由 apiKey 决定
        onChangeText={setApiKey}                                    // 输入变化时更新
        placeholder="请输入 API Key"                                 // 占位提示
        secureTextEntry                                             // 隐藏输入
        autoCapitalize="none"                                       // 不自动大写
      />                                                            // 结束输入框

      <Button title="保存" onPress={handleSave} />                   // 保存按钮
      {saved && <Text style={styles.saved}>已保存</Text>}            // 保存成功提示
    </View>                                                         // 结束容器
  );                                                                // 结束 return
}                                                                   // 结束 App

const styles = StyleSheet.create({                                  // 创建样式表
  container: { flex: 1, padding: 20, paddingTop: 60 },              // 容器样式
  center: { flex: 1, justifyContent: 'center', alignItems: 'center' },  // 居中容器
  heading: { fontSize: 28, fontWeight: 'bold', marginBottom: 24 },  // 标题样式
  label: { fontSize: 14, color: '#666', marginBottom: 6, marginTop: 16 },  // 标签样式
  input: {                                                          // 输入框样式
    borderWidth: 1,                                                 // 边框宽度
    borderColor: '#ccc',                                            // 边框颜色
    borderRadius: 6,                                                // 圆角
    paddingHorizontal: 12,                                          // 水平内边距
    height: 44,                                                     // 高度
    fontSize: 16,                                                   // 字号
  },                                                                // 结束输入框样式
  saved: { color: 'green', marginTop: 12, textAlign: 'center' },    // 保存成功样式
});                                                                 // 结束样式表

解读:昵称用 AsyncStorage,API Key 用 SecureStore。启动时用 Promise.all 并行读取,保存时并行写入。敏感和非敏感数据分开存储,是持久化设计的基本原则。


八、总结

这一课我们学习了 React Native 中数据持久化的核心方案:

  1. AsyncStorage:异步、明文的键值存储,适合非敏感数据;
  2. SecureStore:加密的键值存储,适合令牌、密码等敏感数据,有约 2KB 的大小限制;
  3. FileSystem:文件系统访问,适合图片、日志、大 JSON 文件;
  4. MMKV:高性能同步存储,适合频繁读写的状态,但需要开发构建,不支持 Expo Go;
  5. 选型规则:机密用 SecureStore,速度用 MMKV,兼容用 AsyncStorage,大文件用 FileSystem;
  6. 封装成 useAsyncStorage Hook,可以让持久化状态的使用和普通 useState 一样自然;
  7. 认证持久化用 SecureStore + Context,启动时读取,登录时保存,退出时删除;
  8. 读取存储数据时,一定要处理键不存在的情况;
  9. 持久化是状态的"外部记忆":存储 ↔ 状态 ↔ 视图,三者循环。

记住一句话:

本地存储给状态加了一层"记忆",让"视图 = 函数(状态)"在 App 重启后依然成立。


第11课预告

下一课,我们进入 【React Native娓娓道来】第11课 错误处理与调试:Error Boundary、日志与性能分析。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐