使用 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!");
}}
而不是可读性较差的替代方案
onPress={() => {
createCommentForArticle(article.id, "This doesn't feel good");
}}
或者更复杂的版本
onPress={() => {
dispatch(createCommentForArticle(getArticleIdSelector(article), "I'm sorry Mark, I had to"));
}}
迁移到 React Query 意味着获得全新改进的useQuery
Hook,但却失去了我们离不开的 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
},
}));
接下来我们将定义商店来保存这些资源的集合。
const BookStore = model("BookStore", {
map: map(Book),
});
const AuthorStore = model("AuthorStore", {
map: map(Author),
});
让我们添加一个process
操作来规范化数据并返回 MST 实例。我为该操作添加了一些逻辑,使其能够同时处理数组和单个资源,并额外将新数据与旧数据合并——这样,我们就可以避免不同 API 端点返回不同资源形状时出现的潜在错误(例如,获取资源列表时返回部分数据,而获取单个资源时返回完整数据)。
我们还将添加一个操作来执行 HTTP 请求并返回处理后的数据。稍后我们会将此函数传递给useInfiniteQuery
或useQuery
执行 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,
});
基本上就是这样,我们现在可以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>
);
}
剩下要做的就是实际尝试运行查询。
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>
);
}
我们获得了 React Query 的灵活性,同时又不牺牲 MobX State Tree 的优势。
您可以在此处查看 Code Sandbox 上的完整示例:
示例中的 API 调用是模拟的。在实际生产环境中,这些调用将被替换为真实的 fetch 调用。您可以注意到,当您勾选“显示作者列表”复选框时,它会在“图书列表”部分更新作者信息。author-2
应用中只有一个 实例,所有内容保持同步。我们无需重新获取整个列表。
概括
React Query 和 MobX State Tree 都是很棒的工具。但它们结合在一起,势不可挡。React Query 让我们能够灵活地以我们想要的方式从服务器获取数据。MST + TypeScript 提供了类型安全 + 直观的方式,可以在数据模型上添加方法和计算属性。它们共同提供了卓越的开发者体验,并帮助您构建出色的应用程序。
感谢您阅读!如果您觉得本文有趣,不妨留下❤️、🦄,当然,也欢迎分享和评论您的想法!
劳埃德银行欢迎合作伙伴,并接受新项目。如果您想了解更多关于我们的信息,请访问。
另外,别忘了在Instagram和Facebook上关注我们!
鏂囩珷鏉ユ簮锛�https://dev.to/lloyds-digital/normalize-your-react-query-data-with-mobx-state-tree-17fa