[Practice] 10. Use react-query to obtain data and manage cache (Part 2) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level projects (22)


Source of learning content: React + React Hook + TS Best Practice - MOOC


Compared with the original tutorial, I used the latest version at the beginning of my study (2023.03):

item Version
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

The specific configuration, operation and content will be different, and the "pit" will also be different. . .


1. Project launch: project initialization and configuration

2. React and Hook application: implement the project list

3. TS Application: JS God Assist - Strong Type

4. JWT, user authentication and asynchronous request


5. CSS is actually very simple - add styles with CSS-in-JS


6. User experience optimization - loading and error state handling



7. Hook, routing, and URL state management



8. User selector and item editing function


9. In-depth React state management and Redux mechanism





10. Use react-query to get data and manage cache

1~3

4. Edit and add project function

Next, use react-querythe transformation editing and adding project functions, and add to get project detailshook

editsrc\utils\project.ts

...
import {
    
     useMutation, useQuery, useQueryClient } from "react-query";

export const useEditProject = () => {
    
    
  const client = useHttp();
  const queryClient = useQueryClient();
  return useMutation(
    (params: Partial<Project>) =>
      client(`projects/${
      
      params.id}`, {
    
    
        method: "PATCH",
        data: params,
      }),
    {
    
    
      onSuccess: () => queryClient.invalidateQueries("projects"),
    }
  );
};

export const useAddProject = () => {
    
    
  const client = useHttp();
  const queryClient = useQueryClient();
  return useMutation(
    (params: Partial<Project>) =>
      client(`projects`, {
    
    
        method: "POST",
        data: params,
      }),
    {
    
    
      onSuccess: () => queryClient.invalidateQueries("projects"),
    }
  );
};

export const useProject = (id?: number) => {
    
    
  const client = useHttp();
  return useQuery<Project>(['project', id], () => client(`projects/${
      
      id}`), {
    
     enabled: Boolean(id) });
};
  • invalidateQueries: Expires the data of the specified key (if not specified, all expire)
  • useMutation: handle write operations, such as create/update/delete, etc.;
  • useIsMutation and useIsFetching: Whether there is a fetch request or a mutation request in progress in the application.

Next, improve the new/edit modal box

EDIT src\screens\ProjectList\components\ProjectModal.tsx(was equivalent to an empty page before):

import {
    
     Button, Drawer, Form, Input, Spin } from "antd";
import {
    
     useProjectModal } from "../utils";
import {
    
     UserSelect } from "components/user-select";
import {
    
     useAddProject, useEditProject } from "utils/project";
import {
    
     useForm } from "antd/lib/form/Form";
import {
    
     useEffect } from "react";
import {
    
     ErrorBox } from "components/lib";
import styled from "@emotion/styled";

export const ProjectModal = () => {
    
    
  const {
    
     projectModalOpen, close, editingProject, isLoading } = useProjectModal();
  const useMutateProject = editingProject ? useEditProject : useAddProject

  const {
    
     mutateAsync, error, isLoading: mutateLoading} = useMutateProject()
  const [form] = useForm()
  const onFinish = (values: any) => {
    
    
    mutateAsync({
    
    ...editingProject, ...values}).then(() => {
    
    
      form.resetFields()
      close()
    })
  }

  const title = editingProject ? '编辑项目' : '创建项目'

  useEffect(() => {
    
    
    form.setFieldsValue(editingProject)
    return () => form.resetFields()
  }, [editingProject, form])

  return (
    <Drawer forceRender onClose={
    
    close} open={
    
    projectModalOpen} width="100%">
      <Container>
        {
    
    
          isLoading ? <Spin size="large"/> : <>
            <h1>{
    
    title}</h1>
            <ErrorBox error={
    
    error}/>
            <Form form={
    
    form} layout="vertical" style={
    
    {
    
    width: '40rem'}} onFinish={
    
    onFinish}>
              <Form.Item label='名称' name='name' rules={
    
    [{
    
    required: true, message: '请输入项目名称'}]}>
                <Input placeholder="请输入项目名称"/>
              </Form.Item>
              <Form.Item label='部门' name='organization' rules={
    
    [{
    
    required: true, message: '请输入部门名称'}]}>
                <Input placeholder="请输入部门名称"/>
              </Form.Item>
              <Form.Item label='负责人' name='personId'>
                <UserSelect defaultOptionName="负责人"/>
              </Form.Item>
              <Form.Item style={
    
    {
    
    textAlign: 'right'}}>
                <Button loading={
    
    mutateLoading} type="primary" htmlType="submit">提交</Button>
              </Form.Item>
            </Form>
          </>
        }
      </Container>
    </Drawer>
  );
};

const Container = styled.div`
  height: 80vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
`

Notice:

  • DrawerWhen using useForm()the extracted formbinding in the component Form, you need to add the property, otherwise an errorforceRender will be reported if the binding is not found when the page is opened. any Form element. Forget...
  • useEditProjectThe parameters are useAddProjectthe same and mutually exclusive, so they can be combined into one hookusing
  • In the video, after editing and closing the modal box, the form will be cleared. The blogger here has done the useEffectprocessing callbackin advance.

Since the person in charge is an optional item, the parameters of UserSelectthe subcomponent of the corresponding component IdSelectare changed to optional src\components\id-select.tsx:

...
// 类型不是简单的后来者居上,而是寻求"最大公约数"的方式
interface IdSelectProps extends Omit<SelectProps, "value" | "onChange" | "options"> {
    
    
  value?: SN | null | undefined;
  onChange?: (value?: number) => void;
  ...
}

...
export const IdSelect = (props: IdSelectProps) => {
    
    
  const {
    
     value, onChange, defaultOptionName, options, ...restProps } = props;
  return (
    <Select
      value={
    
    options?.length ? toNumber(value) : 0}
      onChange={
    
    (value) => onChange?.(toNumber(value) || undefined)}
      {
    
    ...restProps}
    >
      ...
    </Select>
  );
};
...

The page is set up, and the next step is to improve the data logic

EDIT src\screens\ProjectList\utils.ts:

import {
    
     useMemo } from "react";
import {
    
     useProject } from "utils/project";
import {
    
     useUrlQueryParam } from "utils/url";
...
export const useProjectModal = () => {
    
    
  const [{
    
     projectCreate, editingProjectId }, setUrlParams] = useUrlQueryParam([
    "projectCreate", 'editingProjectId'
  ]);
  const {
    
     data: editingProject, isLoading } = useProject(
    Number(editingProjectId)
  );

  const open = () => setUrlParams({
    
     projectCreate: true });
  const close = () => setUrlParams({
    
     projectCreate: false, editingProjectId: undefined }); // 若在url显示 false,可以改为 undefined 隐藏
  const startEdit = (id: number) => setUrlParams({
    
     editingProjectId: id });

  return {
    
    
    projectModalOpen: projectCreate === "true" || Boolean(editingProjectId),
    open,
    close,
    startEdit,
    editingProject,
    isLoading
  };
};

Use the editing function to the List component to complete the last step

EDIT src\screens\ProjectList\components\List.tsx:

...
export const List = ({
     
      users, ...props }: ListProps) => {
    
    
  const {
    
     startEdit } = useProjectModal();
  ...
  const editProject = (id: number) => () => startEdit(id);
  return (
    <Table
      pagination={
    
    false}
      columns={
    
    [
        ...
        {
    
    
          render: (text, project) => {
    
    
            const items: MenuProps["items"] = [
              {
    
    
                key: "edit",
                label: "编辑",
                onClick: editProject(project.id),
              },
              ...
            ];
            ...
          },
        },
      ]}
      {
    
    ...props}
    ></Table>
  );
};

View the page, add and edit functions are normal

5. Implement optimistic update with react-query

When the network situation is not very good, in order to improve the user experience, you can use " optimistic update ", that is, directly update the local cache data according to the user's wishes before the request succeeds, and automatically perform data rollback if the request fails

Optimistic Updates | TanStack Query Chinese Documentation

EDIT src\utils\project.ts:

...
import {
    
     useProjectsSearchParams } from "screens/ProjectList/utils";
...
export const useEditProject = () => {
    
    
  const client = useHttp();
  const queryClient = useQueryClient();
  const [searchParams] = useProjectsSearchParams()
  const queryKey = ['projects', searchParams]
  return useMutation(
    (params: Partial<Project>) =>
      client(`projects/${
      
      params.id}`, {
    
    
        method: "PATCH",
        data: params,
      }),
    {
    
    
      onSuccess: () => queryClient.invalidateQueries(queryKey),
      // async 
      onMutate: (target) => {
    
    
        const previousItems = queryClient.getQueryData(queryKey)
        queryClient.setQueryData(queryKey, (old: Project[] = []) => {
    
    
          return old?.map(project => project.id === target.id ? {
    
     ...project, ...target } : project)
        })
        return {
    
    previousItems}
      },
      onError: (error, newItem, context) => {
    
    
        queryClient.setQueryData(queryKey, context?.previousItems)
      }
    }
  );
};
...

View the page, adjust the response time of the development tool 2000ms, star a certain project, and check the effect;
then adjust the failure rate to 100%, star a certain project again, and check the effect

The function is easy to use, but I found that a lot of code (the second parameter) of this part useMutationcan be reused, and then I will extract a special treatment for optimistic updatehook

New src\utils\use-optimistic-options.ts:

import {
    
     QueryKey, useQueryClient } from 'react-query'

export const useConfig = (queryKey: QueryKey, callback: (target: any, old?: any[]) => any[]) => {
    
    
  const queryClient = useQueryClient()
  return {
    
    
    onSuccess: () => queryClient.invalidateQueries(queryKey),
    // async 
    onMutate: (target: any) => {
    
    
      const previousItems = queryClient.getQueryData(queryKey)
      queryClient.setQueryData(queryKey, (old?: any[]) => callback(target, old))
      return {
    
    previousItems}
    },
    onError: (error: any, newItem: any, context: any) => {
    
    
      queryClient.setQueryData(queryKey, context?.previousItems)
    }
  }
}

export const useDeleteConfig = (queryKey: QueryKey) => 
  useConfig(queryKey, (id: any, old?:any[]) => old?.filter(item => item.id !== id) || [])
export const useEditConfig = (queryKey: QueryKey) => 
  useConfig(queryKey, (target: any, old?:any[]) => old?.map(item => item.id === target.id ? {
    
     ...item, ...target } : item) || [])
export const useAddConfig = (queryKey: QueryKey) => 
  useConfig(queryKey, (target: any, old?:any[]) => old ? [...old, target] : [])
  • Since react-querythe type mechanism of is relatively complex, mandatory adaptation is not cost-effective, and this part is highly independent, so multiple types are usedany

Because the parameters of the item list search useProjectsSearchParamsare ProjectListbound to the page, and useEditProjectthis is likely to be used on other pages, in order to maintain its versatility, queryKeyit is extracted separately and passed in as a parameter when used

editsrc\screens\ProjectList\utils.ts

...
import {
    
     QueryKey, useMutation, useQuery } from "react-query";
import {
    
     useAddConfig, useEditConfig } from "./use-optimistic-options";
...
export const useEditProject = (queryKey: QueryKey) => {
    
    
  const client = useHttp();
  return useMutation(
    ...,
    useEditConfig(queryKey)
  );
};

export const useAddProject = (queryKey: QueryKey) => {
    
    
  const client = useHttp();
  return useMutation(
    ...,
      useAddConfig(queryKey)
  );
};
...

editsrc\screens\ProjectList\utils.ts

...
export const useProjectQueryKey = () => ['projects', useProjectsSearchParams()[0]]
...

Pass in where it is usedqueryKey

editsrc\screens\ProjectList\components\List.tsx

...
import {
    
     useProjectModal, useProjectQueryKey } from "../utils";
...
export const List = (...) => {
    
    
  ...
  const {
    
     mutate } = useEditProject(useProjectQueryKey());
  ...
};

editsrc\screens\ProjectList\components\ProjectModal.tsx

...
import {
    
     useProjectModal, useProjectQueryKey } from "../utils";

export const ProjectModal = () => {
    
    
  ...
  const {
    
     mutateAsync, error, isLoading: mutateLoading } = useMutateProject(useProjectQueryKey());
  ...
};
...

Test the function again, it works fine!

Next, complete the delete function

editsrc\utils\project.ts

...
import {
    
     useAddConfig, useDeleteConfig, useEditConfig } from "./use-optimistic-options";
...
export const useDeleteProject = (queryKey: QueryKey) => {
    
    
  const client = useHttp();
  return useMutation(
    (id?: number) =>
      client(`projects/${
      
      id}`, {
    
    
        method: "DELETE"
      }),
      useDeleteConfig(queryKey)
  );
};
...

editsrc\screens\ProjectList\components\List.tsx

...
import {
    
     Dropdown, MenuProps, Modal, Table, TableProps } from "antd";
import {
    
     useProjectModal, useProjectQueryKey } from "../utils";
...
export const List = ({
     
      users, ...props }: ListProps) => {
    
    
  const {
    
     mutate } = useEditProject(useProjectQueryKey());
  // 函数式编程 柯里化
  const starProject = (id: number) => (star: boolean) => mutate({
    
     id, star });
  return (
    <Table
      pagination={
    
    false}
      columns={
    
    [
        ...
        {
    
    
          render: (text, project) => <More project={
    
    project}/>
        },
      ]}
      {
    
    ...props}
    ></Table>
  );
};

const More = ({
     
     project}: {
     
     project: Project}) => {
    
    
  const {
    
     startEdit } = useProjectModal();
  const editProject = (id: number) => () => startEdit(id);
  const {
    
    mutate: deleteProject} = useDeleteProject(useProjectQueryKey())
  const confirmDeleteProject = (id: number) => {
    
    
    Modal.confirm({
    
    
      title: '确定删除这个项目吗?',
      content: '点击确定删除',
      onOk: () => deleteProject(id)
    })
  }
  const items: MenuProps["items"] = [
    {
    
    
      key: "edit",
      label: "编辑",
      onClick: editProject(project.id),
    },
    {
    
    
      key: "delete",
      label: "删除",
      onClick: () => confirmDeleteProject(project.id),
    },
  ];
  return (
    <Dropdown menu={
    
    {
    
     items }}>
      <ButtonNoPadding
        type="link"
        onClick={
    
    (e) => e.preventDefault()}
      >
        ...
      </ButtonNoPadding>
    </Dropdown>
  );
}
  • Edit and delete this part of the function can be extracted separately
  • Note that it is not passed in according to the deletion in the video {id}, but is used directly in useDeleteConfig id( target)

Delete a test function (Note: testing optimistic updates requires adjusting the development tool response time!)

Added optimistic updates are not obvious

6. Separately extract URL parameter logic

According to the previous writing method of the video, there is a problem, so here we need to separate the extraction urlparameter logic separately

editsrc\utils\url.ts

...
export const useUrlQueryParam = <K extends string>(keys: K[]) => {
    
    
  const [searchParams] = useSearchParams();
  const setSearchParams = useSetUrlSearchParam()
  return [
    useMemo(
      () =>
        keys.reduce((prev, key) => {
    
    
          // searchParams.get 可能会返回 null,需要预设值来兼容
          return {
    
     ...prev, [key]: searchParams.get(key) || "" };
          // 初始值会对类型造成影响,需要手动指定
        }, {
    
    } as {
    
     [key in K]: string }),
      // eslint-disable-next-line react-hooks/exhaustive-deps
      [searchParams]
    ),
    (params: Partial<{
     
      [key in K]: unknown }>) => setSearchParams(params)
  ] as const;
};

export const useSetUrlSearchParam = () => {
    
    
  const [searchParams, setSearchParams] = useSearchParams();
  return (params: {
     
     [key in string]: unknown}) => {
    
    
    const o = cleanObject({
    
    
      ...Object.fromEntries(searchParams),
      ...params,
    }) as URLSearchParamsInit;
    return setSearchParams(o);
  }
}

editsrc\screens\ProjectList\utils.ts

...
import {
    
     useSetUrlSearchParam, useUrlQueryParam } from "utils/url";
...
export const useProjectModal = () => {
    
    
  const [{
    
     projectCreate, editingProjectId }] = useUrlQueryParam([
    "projectCreate",
    "editingProjectId",
  ]);
  const setUrlParams = useSetUrlSearchParam()
  const {
    
     data: editingProject, isLoading } = useProject(
    Number(editingProjectId)
  );

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

In fact, according to the blogger’s understanding, the key point is not to separate, but to set setUrlParamsthe time projectCreateand editingProjectIdtime at the same time, instead of setting them one by one according to the previous video (it still feels weird, I don’t know if this is the reason why the previous video operation projectCreatecould not be set to )false

Finally, remember to clear the cache after logging out react-queryto avoid the bug that the old data is still displayed before the new data request comes back after the next user logs in

EDIT src\context\auth-context.tsx:

...
import {
    
     useQueryClient } from "react-query";
...
export const AuthProvider = ({
     
      children }: {
     
      children: ReactNode }) => {
    
    
  ...
  const queryClient = useQueryClient()

  ...
  const logout = () => auth.logout().then(() => {
    
    
    setUser(null)
    queryClient.clear()
  });

  ...
};

7. Summary of cross-component state management solutions

small scene

  • State promotion, composite components

cache status

  • react-query、swr

client status

  • url、redux、context

Some reference notes are still in draft stage, so stay tuned. . .

Guess you like

Origin blog.csdn.net/qq_32682301/article/details/132269313