【实战】十、用 react-query 获取数据,管理缓存(上) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十一)


学习内容来源:React + React Hook + TS 最佳实践-慕课网


相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:

版本
react & react-dom ^18.2.0
react-router & react-router-dom ^6.11.2
antd ^4.24.8
@commitlint/cli & @commitlint/config-conventional ^17.4.4
eslint-config-prettier ^8.6.0
husky ^8.0.3
lint-staged ^13.1.2
prettier 2.8.4
json-server 0.17.2
craco-less ^2.0.0
@craco/craco ^7.1.0
qs ^6.11.0
dayjs ^1.11.7
react-helmet ^6.1.0
@types/react-helmet ^6.1.6
react-query ^6.1.0
@welldone-software/why-did-you-render ^7.0.1
@emotion/react & @emotion/styled ^11.10.6

具体配置、操作和内容会有差异,“坑”也会有所不同。。。


一、项目起航:项目初始化与配置

二、React 与 Hook 应用:实现项目列表

三、TS 应用:JS神助攻 - 强类型

四、JWT、用户认证与异步请求


五、CSS 其实很简单 - 用 CSS-in-JS 添加样式


六、用户体验优化 - 加载中和错误状态处理



七、Hook,路由,与 URL 状态管理



八、用户选择器与项目编辑功能


九、深入React 状态管理与Redux机制





十、用 react-query 获取数据,管理缓存

刷完副本,返回主分支

1.用url参数管理项目模态框状态

为了用 url 参数管理项目模态框状态,在 src\screens\ProjectList\utils.ts 中新增一个 Custom Hook

export const useProjectModal = () => {
    
    
  const [{
    
    projectCreate}, setProjectCreate] = useUrlQueryParam(['projectCreate'])

  const open = () => setProjectCreate({
    
    projectCreate: true})
  const close = () => setProjectCreate({
    
    projectCreate: false}) // 若在url显示 false,可以改为 undefined 隐藏

  return {
    
    
    projectModalOpen: projectCreate === 'true',
    open,
    close
  }
}

接下来去掉之前 组合组件的使用,在对应位置直接使用组件并调用 useProjectModal 中的状态和方法

编辑 src\authenticated-app.tsx

...
export const AuthenticatedApp = () => {
-   const [isOpen, setIsOpen] = useState(false);
  useDocumentTitle("项目列表", false);

  return (
    <Container>
-       <PageHeader
-         projectButton={
-           <ButtonNoPadding type="link" onClick={() => setIsOpen(true)}>
-             创建项目
-           </ButtonNoPadding>
-         }
-       />
+       <PageHeader/>
      <Main>
        <Router>
          <Routes>
-             <Route
-               path="/projects"
-               element={
-                 <ProjectList
-                   projectButton={
-                     <ButtonNoPadding
-                       type="link"
-                       onClick={() => setIsOpen(true)}
-                     >
-                       创建项目
-                     </ButtonNoPadding>
-                   }
-                 />
-               }
-             />
+             <Route path="/projects" element={<ProjectList/>}/>
            ...
          </Routes>
        </Router>
      </Main>
-       <ProjectModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
+       <ProjectModal/>
    </Container>
  );
};
- const PageHeader = (props: { projectButton: JSX.Element }) => {
+ const PageHeader = () => {
  ...

  return (
    <Header between={true}>
      <HeaderLeft gap={true}>
        ...
-         <ProjectPopover {...props} />
+         <ProjectPopover/>
        <span>用户</span>
      </HeaderLeft>
      ...
    </Header>
  );
};
...

编辑 src\screens\ProjectList\index.tsx

...
- import { useProjectsSearchParams } from "./utils";
+ import { useProjectModal, useProjectsSearchParams } from "./utils";
...
+ import { ButtonNoPadding } from "components/lib";

- export const ProjectList = ({ projectButton }: { projectButton: JSX.Element }) => {
+ export const ProjectList = () => {
+   const {open} = useProjectModal()
  ...

  return (
    <Container>
      <Row justify="space-between">
        <h1>项目列表</h1>
-         {projectButton}
+         <ButtonNoPadding type="link" onClick={open}>创建项目</ButtonNoPadding>
      </Row>
      <SearchPanel users={users || []} param={param} setParam={setParam} />
      {error ? (
        <Typography.Text type="danger">{error.message}</Typography.Text>
      ) : null}
      <List
-         projectButton={projectButton}
        ...
      />
    </Container>
  );
};
...

编辑 src\screens\ProjectList\components\List.tsx

...
+ import { useProjectModal } from "../utils";

...
interface ListProps extends TableProps<Project> {
  users: User[];
  refresh?: () => void;
-   projectButton: JSX.Element;
}

// type PropsType = Omit<ListProps, 'users'>
export const List = ({ users, ...props }: ListProps) => {
+   const {open} = useProjectModal()
  ...
  return (
    <Table
      pagination={false}
      columns={[
        ...
        {
          render: (text, project) => {
            const items: MenuProps["items"] = [
              {
                key: "edit",
                label: "编辑",
+                 onClick: open
              },
            ];
            return (
-               <Dropdown dropdownRender={() => props.projectButton}>
+               <Dropdown menu={
   
   {items}}>
                <ButtonNoPadding
                  type="link"
                  onClick={(e) => e.preventDefault()}
                >
                  ...
                </ButtonNoPadding>
              </Dropdown>
            );
          },
        },
      ]}
      {...props}
    ></Table>
  );
};

编辑 src\screens\ProjectList\components\ProjectModal.tsx

...
+ import { useProjectModal } from "../utils";

- export const ProjectModal = ({isOpen, onClose}: {isOpen: boolean; onClose: () => void;}) => {
+ export const ProjectModal = () => {
+   const {projectModalOpen, close} = useProjectModal()
  return (
-     <Drawer onClose={onClose} open={isOpen} width="100%">
+     <Drawer onClose={close} open={projectModalOpen} width="100%">
      <h1>Project Modal</h1>
-       <Button onClick={onClose}>关闭</Button>
+       <Button onClick={close}>关闭</Button>
    </Drawer>
  );
};

编辑 src\screens\ProjectList\components\ProjectPopover.tsx

...
+ import { ButtonNoPadding } from "components/lib";
+ import { useProjectModal } from "../utils";

- export const ProjectPopover = ({projectButton}: {projectButton: JSX.Element;}) => {
+ export const ProjectPopover = () => {
+   const {open} = useProjectModal()
  ...

  const content = (
    <ContentContainer>
      ...
-       {projectButton}
+       <ButtonNoPadding type="link" onClick={open}>创建项目</ButtonNoPadding>
    </ContentContainer>
  );
  ...
};
...

更改完后查看页面效果,报错:

useLocation() may be used only in the context of a <Router> component.

查看代码发现 src\authenticated-app.tsxPageHeaderProjectModal 没有包在 Router 里面,而使用 useUrlQueryParam 的相关功能需要放在 Router 里面,修改一下:

...
export const AuthenticatedApp = () => {
  ...
  return (
    <Container>
+       <Router>
        <PageHeader/>
        <Main>
-         <Router>
          <Routes>
            <Route path="/projects" element={<ProjectList/>}/>
            <Route path="/projects/:projectId/*" element={<ProjectDetail />} />
            <Route index element={<Navigate to="projects" />} />
          </Routes>
-         </Router>
        </Main>
        <ProjectModal/>
+       </Router>
    </Container>
  );
};
...

再次查看页面,功能正常!

2.用 react-query 来处理 服务端缓存

概览 | react-query 中文文档

3.类型守卫,用useQuery缓存工程列表

部分视频在线查看地址:https://space.bilibili.com/2814498/video

react-query 在之前 安装另一个版本的 jira-dev-tool 时已经安装过了,安装命令:

npm i react-query # --force

接下来 使用 react-query 改造 Project 相关接口调用

编辑 src\utils\project.ts

...
import {
    
     useQuery } from "react-query";

export const useProjects = (param?: Partial<Project>) => {
    
    
  const client = useHttp();
  return useQuery<Project[]>(['projects', param], () => client("projects", {
    
     data: cleanObject(param || {
    
    }) }))
};
...
  • useQuery 如果命中缓存,那么 onSuccess 对应的方法不会被触发
  • 之前这部分功能就是模仿 react-query 写的,一些功能替换后可直接使用,一些不能:
    • 之前的 rerun(视频中 retry)功能可以将 useQuery 中的第一个参数 QueryKey 改为 数组形式 [key, ...otherParams],数组首项作为传统意义上的 key ,后续参数可作为触发隐性 “rerun” 的“依赖”,当参数有变化时即使命中缓存依旧触发 onSuccess 对应的方法
    • 对于请求会出现报错的情况,虽然 useQuery 中也有相同 Error 数据返回,但类型(unknown 而非 Error)并不是我们想要的, 后续使用会有类型报错,两种解决办法:
      • 1.显示声明返回值类型:useQuery<Project[], Error>()
      • 2.使用类型守卫,具体见下

编辑 src\components\lib.tsx 新增类型守卫和 ErrorBox 组件:

...
// 类型守卫
const isError = (value: any): value is Error => value?.message

export const ErrorBox = ({
     
     error}: {
     
     error: unknown}) => {
    
    
  if (isError(error)) {
    
    
    return <Typography.Text type="danger">{
    
    error.message}</Typography.Text>
  }
  return null
}
...

is 是一个 类型谓词,用来帮助 ts 编译器 收窄 变量的类型

其它有需要使用到 ErrorBox 的地方依次替换:

编辑 src\unauthenticated-app\index.tsx

...
import {
    
     ErrorBox } from "components/lib";

export const UnauthenticatedApp = () => {
    
    
  ...
  return (
    <Container>
      ...
      <ShadowCard>
        <Title>{
    
    isRegister ? "请注册" : "请登录"}</Title>
        <ErrorBox error={
    
    error}/>
        ...
      </ShadowCard>
    </Container>
  );
};
...

编辑 src\screens\ProjectList\index.tsx

...
- import { ButtonNoPadding } from "components/lib";
+ import { ButtonNoPadding, ErrorBox } from "components/lib";

export const ProjectList = () => {
  ...
  const {
    isLoading,
    error,
    data: list,
-     rerun
  } = useProjects(useDebounce(param));
  const { data: users } = useUsers();

  return (
    <Container>
      ...
      <SearchPanel users={users || []} param={param} setParam={setParam} />
-       {error ? (
-         <Typography.Text type="danger">{error.message}</Typography.Text>
-       ) : null}
+       <ErrorBox error={error}/>
      <List
-         refresh={rerun}
        loading={isLoading}
        users={users || []}
        dataSource={list || []}
      />
    </Container>
  );
};
...

同时去掉使用 rerun (视频中 retry )的部分

编辑 src\screens\ProjectList\components\List.tsx

...
export const List = ({ users, ...props }: ListProps) => {
  ...
  const starProject = (id: number) => (star: boolean) =>
-    mutate({ id, star }).then(props.refresh);
+    mutate({ id, star })
  return (
    <Table
      pagination={false}
      columns={[
        ...
        {
          render: (text, project) => {
            const items: MenuProps["items"] = [
              {
                key: "edit",
                label: "编辑",
                onClick: open,
-               },
+               }, {
+                 key: "delete",
+                 label: "删除",
+                 // onClick: open,
+               },
            ];
            ...
          },
        },
      ]}
      {...props}
    ></Table>
  );
};

查看页面效果,查询功能正常,报错信息展示正常


部分引用笔记还在草稿阶段,敬请期待。。。

猜你喜欢

转载自blog.csdn.net/qq_32682301/article/details/132227011