使用 MobX State Tree 规范化你的 React 查询数据

2025-06-09

使用 MobX State Tree 规范化你的 React 查询数据

在 React 中获取数据看似简单,其实很难。你一开始只是用一个简单的useEffect+useState组合键,就以为搞定了。

“太棒了!”你心想……
但随后你意识到你没有处理错误。于是你添加了一堆代码来处理。
然后你意识到你必须添加一个刷新按钮。于是你又添加了一堆代码来处理。
然后你的后端开发人员告诉你数据是分页的。于是你又添加了一堆代码来处理。
然后你想每N秒自动触发一次刷新。于是你又添加了一堆代码来处理。
到那时,你的数据获取代码简直就是一场噩梦,管理它更是令人头疼,而我们甚至还没有触及缓存这个话题。

我想说的是,React Query 太棒了。它不仅能处理上面列出的所有复杂问题,还能提供更多功能。所以,如果你还没试过,一定要试试。

然而,在劳埃德银行,我们并非一直都在使用 React Query。不久前,我们尝试了一个自定义useQuery钩子来满足我们所有的数据获取需求。它虽然不错,但远不及 React Query。不过,由于我们的 useQuery 与 MobX State Tree 紧密耦合,我们确实获得了一些非常满意的好处:

  • 类型模型
  • 响应时数据规范化
  • 访问时数据非规范化
  • 对模型的操作

注意 - 您可以在此处查看我关于如何使用 MST 的文章:为什么应该使用 MST

类型模型

使用 MobX 状态树,您需要定义数据的形状。MST 使用此方案在运行时验证您的数据。此外,由于 MST 使用 TypeScript,您可以在编写代码时享受 IntelliSense 自动补全数据模型上所有属性的优势。

数据规范化和非规范化

这是什么意思呢?简单来说,这确保了我们的应用中任何给定数据资源只有一个副本。例如,如果我们更新了个人资料数据,这确保了更新将在整个应用中可见,不会产生过时的数据。

对模型的操作

这是一个很棒的 MST 功能。它使我们能够在应用中的数据模型上附加操作。例如,我们可以编写类似

  onPress={() => {
      article.createComment("I love this!");
  }}
Enter fullscreen mode Exit fullscreen mode

而不是可读性较差的替代方案

  onPress={() => {
      createCommentForArticle(article.id, "This doesn't feel good");
  }}
Enter fullscreen mode Exit fullscreen mode

或者更复杂的版本

  onPress={() => {
      dispatch(createCommentForArticle(getArticleIdSelector(article), "I'm sorry Mark, I had to"));
  }}
Enter fullscreen mode Exit fullscreen mode

迁移到 React Query 意味着获得全新改进的useQueryHook,但却失去了我们离不开的 MST 优秀功能。我们只有一个选择……

结合 React Query 和 MST

事实证明,两全其美是可能的,而且代码也并不复杂。
关键在于,查询响应从服务器返回后,应立即进行规范化,并从查询函数返回 MST 实例,而不是原始资源数据。

我们将使用 MST 存储来定义数据获取方法以及将原始网络响应数据转换为 MobX 实例的方法。

举个例子……首先,我们定义两个模型。它们将定义我们要获取的资源的形状。

const Author = model("Author", {
  id: identifier,
  name: string,
});

const Book = model("Book", {
  id: identifier,
  title: string,
  author: safeReference(Author),
}).actions((self) => ({
  makeFavorite() {
    // ... other code
  },
}));
Enter fullscreen mode Exit fullscreen mode

接下来我们将定义商店来保存这些资源的集合。

const BookStore = model("BookStore", {
  map: map(Book),
});

const AuthorStore = model("AuthorStore", {
  map: map(Author),
});
Enter fullscreen mode Exit fullscreen mode

让我们添加一个process操作来规范化数据并返回 MST 实例。我为该操作添加了一些逻辑,使其能够同时处理数组和单个资源,并额外将新数据与旧数据合并——这样,我们就可以避免不同 API 端点返回不同资源形状时出现的潜在错误(例如,获取资源列表时返回部分数据,而获取单个资源时返回完整数据)。

我们还将添加一个操作来执行 HTTP 请求并返回处理后的数据。稍后我们会将此函数传递给useInfiniteQueryuseQuery执行 API 调用。

const BookStore = model("BookStore", {
  map: map(Book),
})
  .actions((self) => ({
    process(data) {
      const root: StoreInstance = getRoot(self);
      const dataList = _.castArray(data);
      const mapped = dataList.map((book) => {
        if (isPrimitive(book)) return book;

        book.author = getInstanceId(root.authorStore.process(book.author));

        const existing = self.map.get(getInstanceId(book));
        return existing
          ? _.mergeWith(existing, book, (_, next) => {
              if (Array.isArray(next)) return next; // Treat arrays like atoms
            })
          : self.map.put(book);
      });

      return Array.isArray(data) ? mapped : mapped[0];
    },
  }))
  .actions((self) => ({
    readBookList: flow(function* (params) {
      const env = getEnv(self);
      const bookListRaw = yield env.http.get(`/books`, {
        params,
      });
      return self.process(bookListRaw);
    }),
  }));

const AuthorStore = model("AuthorStore", {
  map: map(Author),
}).actions((self) => ({
  process(data) {
    const dataList = _.castArray(data);
    const mapped = dataList.map((author) => {
      if (isPrimitive(author)) return author;

      const existing = self.map.get(getInstanceId(author));
      return existing
        ? _.mergeWith(existing, author, (_, next) => {
            if (Array.isArray(next)) return next; // Treat arrays like atoms
          })
        : self.map.put(author);
    });
    return Array.isArray(data) ? mapped : mapped[0];
  },
}));

const Store = model("Store", {
  bookStore: BookStore,
  authorStore: AuthorStore,
});
Enter fullscreen mode Exit fullscreen mode

基本上就是这样,我们现在可以readBookList在组件中使用该方法了useQuery……useInfiniteQuery差不多了。
如果此时尝试,会报错。这是因为 React Query 内部使用了一种叫做结构共享的技术来检测数据是否发生了变化。然而,这与 MobX 状态树不兼容,所以我们需要禁用它。我们可以使用顶级查询客户端提供程序来配置它。

import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      structuralSharing: false,
      // ... other options
    },
  },
});

function App() {
  // ... other code

  return (
    <QueryClientProvider client={queryCache}>
      {/* ... other providers ... */}
      <Router />
    </QueryClientProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

剩下要做的就是实际尝试运行查询。

function BookListView() {
  const store = useStore();
  const query = useQuery("bookList", (_key, page = 1) =>
    store.bookStore.readBookList({ page })
  );

  // Convert array of responses to a single array of books.
  const bookList = _.flatMap(query.data, (response) => response.data);

  return (
    <div>
      {bookList.map((book) => {
        return (
          <BookView
            book={book}
            onPress={book.makeFavorite} // We have access to methods on the Book model
          />
        );
      })}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

我们获得了 React Query 的灵活性,同时又不牺牲 MobX State Tree 的优势。

您可以在此处查看 Code Sandbox 上的完整示例:

代码沙盒链接

示例中的 API 调用是模拟的。在实际生产环境中,这些调用将被替换为真实的 fetch 调用。您可以注意到,当您勾选“显示作者列表”复选框时,它会在“图书列表”部分更新作者信息。author-2应用中只有一个 实例,所有内容保持同步。我们无需重新获取整个列表。

概括

React Query 和 MobX State Tree 都是很棒的工具。但它们结合在一起,势不可挡。React Query 让我们能够灵活地以我们想要的方式从服务器获取数据。MST + TypeScript 提供了类型安全 + 直观的方式,可以在数据模型上添加方法和计算属性。它们共同提供了卓越的开发者体验,并帮助您构建出色的应用程序。

感谢您阅读!如果您觉得本文有趣,不妨留下❤️、🦄,当然,也欢迎分享和评论您的想法!

劳埃德银行欢迎合作伙伴,并接受新项目。如果您想了解更多关于我们的信息,请访问

另外,别忘了在InstagramFacebook上关注我们!

鏂囩珷鏉ユ簮锛�https://dev.to/lloyds-digital/normalize-your-react-query-data-with-mobx-state-tree-17fa
PREV
在 GitLab 中构建 CI/CD 工作流程(Node.js 示例)
NEXT
React Native Carousel 让我们在 React Native AWS Security LIVE 中创建一个轮播!