【实战】 九、深入React 状态管理与Redux机制(一) —— 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
prettier2.8.4
json-server0.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机制

1.useCallback应用,优化异步请求

当前项目中使用 useAsync 进行异步请求,但是其中有一个隐藏 bug,若是在页面中发起一个请求,这个请求需要较长时间3s(可以使用开发控制台设置请求最短时间来预设场景),在这个时间段内,退出登录,此时就会有报错:

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.

原因是虽然退出登录,组件销毁,但是异步函数还在执行,当它执行完进行下一步操作 setXXX 或是 更新组件都找不到对应已销毁的组件。

接下来解决一下这个问题。

编辑 src\utils\index.ts

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

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

  return mountedRef
}

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) => {...});
  };
  ...
};

还有个遗留问题,在 useEffect 中使用的变量若是没有在依赖数组中添加就会报错,添加上又会造成死循环,因此之前用 eslint-disable-next-line 解决

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

现在换个方案,使用 useMemo 当然可以解决,这里推荐使用特殊版本的 useMemo, useCallback

修改 src\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],
  )
  ...
};

可以按照提示配置依赖: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

尽管如此,但还是难免会出现,在 useCallback 中改变 依赖值的行为,比如依赖值 XXX 对应的 setXXX,这时需要用到 setXXX 的函数用法(这样也可以省去一个依赖):

继续修改 src\utils\use-async.ts

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

修改 src\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]);
  ...
};
...

修改 src\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]);
};

总结:非状态类型需要作为依赖 就要将其使用 useMemo 或者 useCallback 包裹(依赖细化 + 新旧关联),常见于 Custom Hook 中函数类型数据的返回

2.状态提升,组合组件与控制反转

接下来定制化一个项目编辑模态框(编辑+新建项目),PageHeader hover 后可以打开(新建),ProjectList 中可以打开模态框(新建),里面的 List 的每行也可以打开模态框(编辑)

src\components\lib.tsx 中新增 padding0Button

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

新建 src\screens\ProjectList\components\ProjectModal.tsx(模态框):

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>
}

新建 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;
`

编辑 src\authenticated-app.tsx(引入 ButtonNoPaddingProjectPopoverProjectModal 自定义组件,并将模态框的状态管理方法传到对应组件 PageHeaderProjectList,注意接收方要定义好类型):

...
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>
  );
};
...

由于涉及登录后多个组件会发起调用,因此 ProjectModal 组件需要放在 AuthenticatedAppContainer

编辑 src\screens\ProjectList\index.tsx(引入 模态框的状态管理方法):

...
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>
  );
};
...

编辑 src\screens\ProjectList\components\List.tsx(引入 模态框的状态管理方法):

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 drilling)若是间隔层数较多时(定义和使用相隔太远),不仅有“下钻”问题,而且耦合度太高

下面使用 组件组合(component composition)的方式解耦

组件组合(component composition) | Context – React

编辑 src\authenticated-app.tsx(将 绑定了模态框 打开方法的 ButtonNoPadding 作为属性传给需要用到的组件):

...
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>
  );
};
...

编辑 src\screens\ProjectList\components\ProjectPopover.tsx(使用传入的属性组件代替之前的 绑定了模态框 打开方法的 ButtonNoPadding ):

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

编辑 src\screens\ProjectList\index.tsx(使用传入的属性组件代替之前的 绑定了模态框 打开方法的 ButtonNoPadding 并继续“下钻”):

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

编辑 src\screens\ProjectList\components\List.tsx(使用传入的属性组件代替之前的 绑定了模态框 打开方法的 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>
  );
};

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

  • 64
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 114
    评论
React 17是最新发布的React版本,它带来了一些重要的改进和优化,提高了React应用的性能和可维护性。React Hooks是一种新的特性,它可以替代Class组件中的生命周期方法和状态管理方式,使代码更简洁、易读和可测试。TypeScript是一种静态类型检查的编程语言,结合ReactReact Hooks可以提供更好的类型安全和代码提示,减少潜在的bug。 在仿Jira企业级项目中,使用React 17React Hooks和TypeScript的最佳实践可以如下: 1. 使用函数组件和React Hooks来构建UI组件,避免使用Class组件和生命周期方法。这样可以降低组件的复杂度,并且让代码更加易于维护和扩展。 2. 在使用React Hooks时,尽可能将逻辑分离成可复用的自定义Hooks,以提高代码的可维护性和重用性。 3. 使用TypeScript来为组件和函数添加类型声明,以提供更好的类型安全和代码提示。通过使用接口和类型别名,可以明确指定组件的props和状态的类型,并及时发现并修复类型错误。 4. 使用React Context和useContext Hook来实现全局状态管理。这对于企业级项目中的共享数据是非常有用的,可以避免通过Props层层传递数据。 5. 使用React Router来管理路由,以便实现页面间的导航和切换。 6. 使用Axios或其他合适的网络库来处理与服务器的数据通信。 7. 使用CSS模块化或CSS-in-JS技术来管理组件的样式,以确保样式的隔离性和可重用性。 8. 使用ESLint和Prettier等代码检查工具,确保代码的一致性和质量。 通过遵循以上最佳实践,可以使仿Jira企业级项目更加现代化、高效、可维护,帮助开发者更好地进行团队协作和项目开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 114
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序边界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值