【Actual Combat】9. In-depth React state management and Redux mechanism (1) —— React17+React Hook+TS4 best practice, imitating Jira enterprise-level project (16)


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

1. useCallback application to optimize asynchronous requests

In the current project, use to useAsyncmake asynchronous requests, but there is a hidden one bug. If a request is initiated on the page, the request will take a long time of 3s (you can use the development console to set the minimum request time to preset the scene), during this time period , log out, and an error will be reported at this time:

Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

setXXXThe reason is that although the asynchronous function is still executing after logging out and the component is destroyed, the corresponding destroyed component cannot be found when it completes the next operation or updates the component.

Let's solve this problem next.

EDIT src\utils\index.ts:

...
/**
 * 返回组件的挂载状态,如果还没有挂载或者已经卸载,返回 false; 反之,返回 true;
 */
export const useMountedRef = () => {
    
    
  const mountedRef = useRef(false)

  useEffect(() => {
    
    
    mountedRef.current = true
    return () => {
    
    
      mountedRef.current = false
    }
  }, [])

  return mountedRef
}

Apply on src\utils\use-async.ts:

...
import {
    
     useMountedRef } from "utils";
...
export const useAsync = <D>(...) => {
    
    
  ...
  const mountedRef = useMountedRef()
  ...
  const run = (...) => {
    
    
    ...
    return promise
      .then((data) => {
    
    
        if(mountedRef.current)
          setData(data);
        return data;
      })
      .catch((error) => {
    
    ...});
  };
  ...
};

There is still a remaining problem. useEffectIf the variables used in are not added in the dependent array , an error will be reported, and adding them will cause an infinite loop, so it was eslint-disable-next-linesolved with before

// eslint-disable-next-line react-hooks/exhaustive-deps

Now to change the solution, use useMemocan of course be solved, here it is recommended to use a special version useMemo, useCallback:

Revisesrc\utils\use-async.ts

import {
    
     useCallback, useState } from "react";
...

export const useAsync = <D>(...) => {
    
    
  ...

  const setData = useCallback((data: D) =>
    setState({
    
    
      data,
      stat: "success",
      error: null,
    }), [])

  const setError = useCallback((error: Error) =>
    setState({
    
    
      error,
      stat: "error",
      data: null,
    }), [])

  // run 来触发异步请求
  const run = useCallback((...) => {
    
    
      ...
    }, [config.throwOnError, mountedRef, setData, state, setError],
  )
  ...
};

You can follow the prompts to configure dependencies:React Hook useCallback has missing dependencies: 'config.throwOnError', 'mountedRef', 'setData', and 'state'. Either include them or remove the dependency array. You can also do a functional update 'setState(s => ...)' if you only need 'state' in the 'setState' call.e

Even so, it is inevitable useCallbackto change the behavior of the dependent value in , for example, XXXcorresponding to the dependent value setXXX, you need to use setXXXthe function usage of (this can also save a dependency):

Continue to modifysrc\utils\use-async.ts

...
export const useAsync = <D>(...) => {
    
    
  ...
  const run = useCallback((...) => {
    
    
      ...
      setState(prevState => ({
    
     ...prevState, stat: "loading" }));
      ...
    }, [config.throwOnError, mountedRef, setData, setError],
  )
  ...
};

Revisesrc\utils\project.ts

...
import {
    
     useCallback, useEffect } from "react";
...

export const useProjects = (...) => {
    
    
  ...
  const fetchProject = useCallback(() =>
    client("projects", {
    
     data: cleanObject(param || {
    
    })
  }), [client, param])

  useEffect(() => {
    
    
    run(fetchProject(), {
    
     rerun: fetchProject });
  }, [param, fetchProject, run]);
  ...
};
...

Revisesrc\utils\http.ts

...
import {
    
     useCallback } from "react";
...
export const useHttp = () => {
    
    
  ...
  return useCallback((...[funcPath, customConfig]: Parameters<typeof http>) =>
    http(funcPath, {
    
     ...customConfig, token: user?.token }), [user?.token]);
};

Summary: Non-state types need to be used as dependencies useMemoor useCallbackpackaged (dependency refinement + old and new associations), which are common in Custom Hookthe return of function type data in

2. State promotion, composite components and inversion of control

Next, customize a project editing modal box (edit + new project), PageHeader hoverand then open it (new), and ProjectListopen the modal box in it (new), and Listeach line in it can also open the modal box (edit)

src\components\lib.tsxAdded in paddingas 0of Button:

...
export const ButtonNoPadding = styled(Button)`
  padding: 0;
`

New src\screens\ProjectList\components\ProjectModal.tsx(modal box):

import {
    
     Button, Drawer } from "antd"

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

New src\screens\ProjectList\components\ProjectPopover.tsx:

import styled from "@emotion/styled"
import {
    
     Divider, List, Popover, Typography } from "antd"
import {
    
     ButtonNoPadding } from "components/lib"
import {
    
     useProjects } from "utils/project"


export const ProjectPopover = ({
    
     setIsOpen }: {
    
     setIsOpen: (isOpen: boolean) => void }) => {
    
    
  const {
    
     data: projects } = useProjects()
  const starProjects = projects?.filter(i => i.star)

  const content = <ContentContainer>
    <Typography.Text type="secondary">收藏项目</Typography.Text>
    <List>  
      {
    
    
        starProjects?.map(project => <List.Item>
          <List.Item.Meta title={
    
    project.name}/>
        </List.Item>)
      }
    </List>
    <Divider/>
    <ButtonNoPadding type='link' onClick={
    
    () => setIsOpen(true)}>创建项目</ButtonNoPadding>
  </ContentContainer>
  return <Popover placement="bottom" content={
    
    content}>
    项目
  </Popover>
}

const ContentContainer = styled.div`
  width: 30rem;
`

Edit src\authenticated-app.tsx(introduce ButtonNoPadding, ProjectPopover, ProjectModalcustom components, and pass the state management method of the modal box to the corresponding component PageHeaderand ProjectList, note that the receiver must define the type):

...
import {
    
     ButtonNoPadding, Row } from "components/lib";
...
import {
    
     ProjectModal } from "screens/ProjectList/components/ProjectModal";
import {
    
     useState } from "react";
import {
    
     ProjectPopover } from "screens/ProjectList/components/ProjectPopover";

export const AuthenticatedApp = () => {
    
    
  const [isOpen, setIsOpen] = useState(false)
  ...

  return (
    <Container>
      <PageHeader setIsOpen={
    
    setIsOpen}/>
      <Main>
        <Router>
          <Routes>
            <Route path="/projects" element={
    
    <ProjectList setIsOpen={
    
    setIsOpen}/>} />
            ...
          </Routes>
        </Router>
      </Main>
      <ProjectModal isOpen={
    
    isOpen} onClose={
    
    () => setIsOpen(false)}/>
    </Container>
  );
};
const PageHeader = ({
    
     setIsOpen }: {
    
     setIsOpen: (isOpen: boolean) => void }) => {
    
    
  ...

  return (
    <Header between={
    
    true}>
      <HeaderLeft gap={
    
    true}>
        <ButtonNoPadding type="link" onClick={
    
    resetRoute}>
          <SoftwareLogo width="18rem" color="rgb(38,132,255)" />
        </ButtonNoPadding>
        <ProjectPopover setIsOpen={
    
    setIsOpen}/>
        <span>用户</span>
      </HeaderLeft>
      <HeaderRight>
        ...
      </HeaderRight>
    </Header>
  );
};
...

Since multiple components will initiate calls after login, ProjectModalthe component needs to be placed AuthenticatedAppunder Containerthe

EDIT src\screens\ProjectList\index.tsx(to introduce state management methods for modal boxes):

...
import {
    
     Row, Typography } from "antd";
...
import {
    
     ButtonNoPadding } from "components/lib";

export const ProjectList = ({
    
     setIsOpen }: {
    
     setIsOpen: (isOpen: boolean) => void }) => {
    
    
  ...
  return (
    <Container>
      <Row justify='space-between'>
        <h1>项目列表</h1>
        <ButtonNoPadding type='link' onClick={
    
    () => setIsOpen(true)}>创建项目</ButtonNoPadding>
      </Row>
      ...
      <List
        setIsOpen={
    
    setIsOpen}
        {
    
    ...}
      />
    </Container>
  );
};
...

EDIT src\screens\ProjectList\components\List.tsx(to introduce state management methods for modal boxes):

import {
    
     Dropdown, MenuProps, Table, TableProps } from "antd";
...
import {
    
     ButtonNoPadding } from "components/lib";
...
interface ListProps extends TableProps<Project> {
    
    
  ...
  setIsOpen: (isOpen: boolean) => void;
}

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

prop drillingIt can be clearly seen that if the state promotion ( ) of this method has a large number of interval layers (the definition and use are too far apart), not only the "drill-down" problem, but also the coupling degree is too high

The following uses component composition to decouple

component composition | Context – React

Edit src\authenticated-app.tsx(pass the modal box opening method bound ButtonNoPaddingas an attribute to the component that needs to be used):

...
export const AuthenticatedApp = () => {
    
    
  ...
  return (
    <Container>
      <PageHeader projectButton={
    
    
        <ButtonNoPadding type="link" onClick={
    
    () => setIsOpen(true)}>
          创建项目
      </ButtonNoPadding>
      } />
      <Main>
        <Router>
          <Routes>
            <Route
              path="/projects"
              element={
    
    <ProjectList projectButton={
    
    
                <ButtonNoPadding type="link" onClick={
    
    () => setIsOpen(true)}>
                  创建项目
              </ButtonNoPadding>
              } />}
            />
            ...
          </Routes>
        </Router>
      </Main>
      ...
    </Container>
  );
};
const PageHeader = (props: {
     
      projectButton: JSX.Element }) => {
    
    
  ...
  return (
    <Header between={
    
    true}>
      <HeaderLeft gap={
    
    true}>
        ...
        <ProjectPopover {
    
     ...props } />
        ...
      </HeaderLeft>
      <HeaderRight>...</HeaderRight>
    </Header>
  );
};
...

EDIT src\screens\ProjectList\components\ProjectPopover.tsx(use the passed-in attribute component instead of the previous one bound to the modal open method ButtonNoPadding):

...
export const ProjectPopover = ({
     
      projectButton }: {
     
      projectButton: JSX.Element }) => {
    
    
  ...
  const content = (
    <ContentContainer>
      ...
      {
    
     projectButton }
    </ContentContainer>
  );
  ...
};
...

EDIT src\screens\ProjectList\index.tsx(use the passed-in attribute component instead of the previous bound modal box opening method ButtonNoPaddingand continue to "drill down"):

...
export const ProjectList = ({
     
      projectButton }: {
     
      projectButton: JSX.Element }) => {
    
    
  ...
  return (
    <Container>
      <Row justify="space-between">
        ...
        {
    
     projectButton }
      </Row>
      ...
      <List
        projectButton={
    
    projectButton}
        {
    
    ...}
      />
    </Container>
  );
};
...

EDIT src\screens\ProjectList\components\List.tsx(use the passed-in attribute component instead of the previous one bound to the modal open method ButtonNoPadding):

...
interface ListProps extends TableProps<Project> {
    
    
  ...
  projectButton: JSX.Element
}

// type PropsType = Omit<ListProps, 'users'>
export const List = ({
     
      users, ...props }: ListProps) => {
    
    
  ...
  return (
    <Table
      pagination={
    
    false}
      columns={
    
    [
        ...
        {
    
    
          render: (text, project) => {
    
    
            return (
              <Dropdown 
                dropdownRender={
    
    () => props.projectButton}>
                <ButtonNoPadding
                  type="link"
                  onClick={
    
    (e) => e.preventDefault()}
                >
                  ...
                </ButtonNoPadding>
              </Dropdown>
            );
          },
        },
      ]}
      {
    
    ...props}
    ></Table>
  );
};

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

Guess you like

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