- 作者:老汪软件技巧
- 发表时间:2024-09-09 04:01
- 浏览量:
大家好,我是 OQ(Open Quoll),是一名 React Query 的爱好者,也是 React Mug 的作者。React Query 是很实用的前端同步数据的方案,其核心是以 Query Key 为索引的缓存机制,可以说,管理好了 Query Key 就等于管理好了缓存,就等于管理好了数据同步。React Mug 则是一款简洁的函数式状态库。今天通过实现案例为大家带来一种简单而高效管理 Query Key 的实践。
关于案例
这个案例来源于我过去开发的实际项目,包含比较常见的数据同步逻辑。界面的中心区域是分页列表,每个条目展示一个文档的元数据。顶部是设置搜索条件的区域,默认为空,当修改搜索条件时列表随之刷新。点击列表条目右侧会弹出一个抽屉加载预览当前文档,并且可以编辑文档元数据。
这里有一个跨前后端的数据结构,即文档的元数据:
interface Doc {
id: string;
title: string;
author: string;
labels: string[];
createdAt: number;
}
案例的主要挑战在于每个界面区域看似独立、实际上数据是相互关联的。尽管有多种方法实现,但是想要做到简单而高效却不容易。
下面着手实现。
页面层中的 Query Key
先从页面层的 分页列表 和 搜索条件 写起,查询文档元数据列表的接口调用如下:
async function getDocList(params: {
title: string;
authors: string[];
labels: string[];
pageIndex: number;
}): Promise<{ docList: Doc[]; pageCount: number }> {
// 调用后端接口获取数据
}
对应的 Query Hook 如下:
import { keepPreviousData, useQuery } from '@tanstack/react-query';
function useDocListQuery(...args: Parameters<typeof getDocList>) {
return useQuery({
queryKey: ['docList', ...args],
queryFn: async () => {
return await getDocList(...args);
},
placeholderData: keepPreviousData,
});
}
然后稍微改造一下 Query Hook,用 React Mug 将 Query Key 转变成状态管理起来。这简化了 Query Hook 的调用,方便了当前 Query Key 的查询,而且让状态变化能够直接触发新的查询:
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { check, construction, Mug, useOperator } from 'react-mug';
const keyOfDocListQueryMug: Mug<['docList', ...Parameters<typeof getDocList>]> = {
[construction]: ['docList', { title: '', authors: [], labels: [], pageIndex: 0 }],
};
export function useDocListQuery() {
const queryKey = useOperator(check, keyOfDocListQueryMug);
return useQuery({
queryKey,
queryFn: async () => {
const [, ...args] = queryKey;
return await getDocList(...args);
},
placeholderData: keepPreviousData,
});
}
这里的 Mug,马克杯,是 React Mug 盛装状态的基本模块,[construction]字段既表示了对象是一个 Mug 也设置了 Mug 所盛装状态的初始值,而 Mug 帮助类型只是辅助定义类型,useOperator 和 check 则用来持续读取 Mug 里的状态。这里 keyOfDocListQueryMug 盛装的状态便是 Query Key,一个以字面量 'docList' 为首个元素的 ,后面这个 Mug 就可以代表这个动态变化的 Query Key 用在任何地方了。
之后是分别实现 分页列表 和 搜索条件 的逻辑主体:
import { check, swirl, useOperator } from 'react-mug';
function DocPaginatedList() {
const { data, isLoading } = useDocListQuery();
const [, { pageIndex }] = useOperator(check, keyOfDocListQueryMug);
return (
<>
{isLoading && <LoadingSpinner />}
<Table rows={data?.docList ?? []} />
<Pagination
pageIndex={pageIndex}
pageCount={data?.pageCount ?? 0}
onPageIndexChange={(pageIndex) => swirl(keyOfDocListQueryMug, [, { pageIndex }])}
/>
>
);
}
import { check, swirl, useOperator } from 'react-mug';
function DocSearchCriteria() {
const [, { title, authors, labels }] = useOperator(check, keyOfDocListQueryMug);
return (
<>
<TitleTextInput
value={title}
onThrottledChange={(title) => swirl(keyOfDocListQueryMug, [, { title }])}
/>
<AuthorMultiSelect
value={authors}
onChange={(authors) => swirl(keyOfDocListQueryMug, [, { authors }])}
/>
<LabelMultiSelect
value={labels}
onChange={(labels) => swirl(keyOfDocListQueryMug, [, { labels }])}
/>
>
);
}
这里 swirl 能够以 合并逻辑 修改目标 Mug 里的状态,空着的字段则会保持原值。当 TitleTextInput、AuthorMultiSelect、LabelMultiSelect 或 Pagination 改变 keyOfDocListQueryMug 里的 Query Key 时,useDocListQuery 中的 Query Key 和查询参数会随之发生变化,进而触发新的查询更新列表,十分便捷。
弹出层里的 Query Key
接下来开始写弹出层的 文档元数据 和 文档预览,更新文档元数据和查询文档二进制内容的接口调用如下:
async function putDoc(doc: Doc): Promise<void> {
// 调用后端接口推送数据
}
async function getDocBinaryContent(docId: string): Promise<string> {
// 调用后端接口获取数据
}
现在处理一下文档元数据条目的点击事件:
import { construction, Mug } from 'react-mug';
const selectedDocIdMug: Mug<string | null> = {
[construction]: null,
};
import { swirl } from 'react-mug';
function DocPaginatedList() {
// ...
return (
<>
{/* ... */}
<Table rows={data?.docList ?? []} onRowClick={(docId) => swirl(selectedDocIdMug, docId)} />
{/* ... */}
>
);
}
这里声明了 selectedDocIdMug 来盛装状态 “被点中条目的文档 ID”,方便后面抽屉组件的访问。
最后就是抽屉的逻辑主体了:
import { useMemo, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { check, construction, useOperator } from 'react-mug';
function DocDrawer() {
const queryClient = useQueryClient();
const selectedDocId = useOperator(check, selectedDocIdMug);
const [editingDoc, setEditingDoc] = useState(false);
const drawerOpen = !!selectedDocId;
const selectedDoc = useMemo(() => {
const queryData = queryClient.getQueryData<Awaited<ReturnType<typeof getDocList>>>(
check(keyOfDocListQueryMug)
);
return queryData?.docList.find((doc) => doc.id === selectedDocId);
}, [selectedDocId]);
const { data: docBinaryContent } = useQuery({
queryKey: ['docBinaryContent', selectedDocId],
queryFn: async () => {
if (!selectedDocId) {
return;
}
return await getDocBinaryContent(selectedDocId);
},
staleTime: Infinity,
});
return (
<Drawer open={drawerOpen}>
{editingDoc
? selectedDoc && <DocDisplay doc={selectedDoc} onClick={() => setEditingDoc(true)} />
: selectedDoc && (
<DocEdit
initialDoc={selectedDoc}
onCancel={() => setEditingDoc(false)}
onSubmit={async (doc) => {
await putDoc(doc);
queryClient.invalidateQueries({ queryKey: check(keyOfDocListQueryMug) });
setEditingDoc(false);
}}
/>
)}
{docBinaryContent && <DocPreview docBinaryContent={docBinaryContent} />}
Drawer>
);
}
这里 check 用来读取目标 Mug 里的状态。由于 keyOfDocListQueryMug 中的状态就是当前文档元数据列表查询的 Query Key ,所以对应的 check 返回值就可以直接分别用作 queryClient.getQueryData 和 queryClient.invalidateQueries 的参数来查询和更新 React Query 缓存了,非常简单。
结语
以上便是简单而高效管理 Query Key 进而管理 React Query 缓存的实践了,欢迎 jym 多多交流。对 React Mug 感兴趣的话还可以移步至 把状态装进马克杯里,给你一个不烫手的状态库 进一步阅读,谢谢!