在 React Native 中实现带版本的持久化状态的简单模式

发布: (2026年1月16日 GMT+8 07:40)
6 min read
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line unchanged and preserve all formatting as requested.

我遇到的问题(非常快)

在我的第一个 React Native 项目中,我需要使用 AsyncStorage 来持久化用户偏好设置。起初这很简单:

await AsyncStorage.setItem("state", JSON.stringify(data));

但随着项目的增长,我遇到了几个问题:

  • 添加了一个新字段
  • 重命名了另一个字段
  • 需要为现有用户提供默认值
  • 希望 TypeScript 真正帮助我
  • 不想悄悄清除用户数据

于是我开始寻找解决方案……

我发现的情况

  • 状态库只能 serialize 状态,却不能帮助你 evolve
  • 迁移系统要么与框架紧密耦合,要么仅限运行时使用且类型松散

我想要的是 平凡、明确且安全

于是我写了自己的实现。
今天我把它分享出来,供大家直接使用,或借鉴其中对自己代码库有用的思路。

思路:将持久化状态视为模式,而非二进制块

帮助实现这一转变的思考方式是:

持久化状态不是“仅仅是 JSON”——它是具有 版本化模式 的数据。

一旦接受了这一点,后面的工作就相对机械化了:

  1. 定义一个模式(并提供默认值)
  2. 在数据旁边存储一个版本号
  3. 当模式发生变化时,迁移 旧数据到新结构
  4. 对所有内容进行验证

就这么简单。

核心原则(我的不可妥协点)

  • 端到端的类型安全
  • 显式迁移(无魔法推断)
  • 确定性的升级(无“尽力而为”)
  • 存储无关(AsyncStorage、localStorage、内存)
  • 以后容易删除,如果我决定不喜欢它

那一点比人们承认的更重要。

一个最小示例

1️⃣ 定义 schema(Zod)

import { z } from "zod";

export const persistedSchema = z.object({
  _version: z.number(),
  preferences: z.object({
    colorScheme: z.enum(["system", "light", "dark"]).default("system"),
  }).default({ colorScheme: "system" }),
});

export type PersistedState = z.infer<typeof persistedSchema>;

Zod 为我提供:

  • 运行时校验
  • 编译时类型
  • 免费的默认值

2️⃣ 创建存储(AsyncStorage、localStorage 或内存)

import { createPersistedState } from "@sebastianthiebaud/schema-versioned-storage";
import { createAsyncStorageAdapter } from
  "@sebastianthiebaud/schema-versioned-storage/adapters/async-storage";

const storage = createPersistedState({
  schema: persistedSchema,
  storageKey: "APP_STATE",
  storage: createAsyncStorageAdapter(),
  migrations: [],
  getCurrentVersion: () => 1,
});

await storage.init(); // 加载、校验、应用默认值、运行迁移

3️⃣ 使用它(完整类型)

// 读取
const theme = storage.get("preferences").colorScheme;

// 写入
await storage.set("preferences", {
  colorScheme: "dark",
});

如果我拼错了键或值,TypeScript 会在 运行时之前 报错。

迁移是显式且乏味的(设计如此)

当模式更改时,我会添加一个迁移:

import type { Migration } from "@sebastianthiebaud/schema-versioned-storage";

const migration: Migration = {
  metadata: {
    version: 2,
    description: "Add language preference",
  },
  migrate: (state: unknown) => {
    // `as any` required here since the old schema is no more :-( 
    const old = state as any;
    return {
      ...old,
      _version: 2,
      preferences: {
        ...old.preferences,
        language: "en",
      },
    };
  },
};

export default migration;

没有推断。没有猜测。没有“也许可以”。
如果迁移缺失或无效,初始化会大声失败。

适配器

interface StorageAdapter {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
}

于是我得到了:

  • AsyncStorage 用于 React Native
  • localStorage 用于网页
  • 一个 内存 适配器用于测试

使用内存适配器进行迁移测试,结果比模拟 AsyncStorage 要好得多。

React 集成(可选)

一个轻量的 context + hook 可以避免属性钻取:

const storage = useStorage();

没有魔法——只是对同一个存储实例的轻量包装。

CLI – 消除摩擦

一个小型 CLI 为我生成样板代码:

  • 生成迁移文件
  • 生成迁移索引
  • 对模式进行哈希以检测更改

示例:

npx svs generate-migration --name add-field --from 1 --to 2

没有花哨——只是更少的失误。

这算是“最佳”方案吗?

可能不是。

但它具备:

  • 一次阅读即可理解
  • 如果不喜欢,容易删除
  • 明确展示数据的演变方式
  • 在关键地方提供类型安全

如果这对你有帮助,太好了——如果没有,随意借鉴这些想法。

我之所以分享,主要是因为当我开始时找不到符合这种思维模型的方案。祝编码愉快!

如果你直接使用它,复制其中的部分,或仅仅偷走迁移模式,那在我看来就是成功。

代码在这里:

👉 schema-versioned-storage on GitHub

很想听听其他人在 React Native 中如何处理持久化状态的演进——我自己也在学习中。

Back to Blog

相关文章

阅读更多 »