• 作者:老汪软件技巧
  • 发表时间: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 感兴趣的话还可以移步至 把状态装进马克杯里,给你一个不烫手的状态库 进一步阅读,谢谢!