【实战】 九、深入React 状态管理与Redux机制(一) —— React17+React Hook

本文详细探讨了React项目中的用户体验优化,包括加载中状态处理、Hook与路由管理,以及如何使用useCallback和useMemo进行状态管理和避免内存泄漏。还介绍了状态提升和组件组合的方法,以降低耦合度并优化组件交互。
摘要由CSDN通过智能技术生成


七、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 并继续“下钻”):

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Linux运维工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Linux运维知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip1024b (备注Linux运维获取)
img

为了做好运维面试路上的助攻手,特整理了上百道 【运维技术栈面试题集锦】 ,让你面试不慌心不跳,高薪offer怀里抱!

这次整理的面试题,小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。

本份面试集锦涵盖了

  • 174 道运维工程师面试题
  • 128道k8s面试题
  • 108道shell脚本面试题
  • 200道Linux面试题
  • 51道docker面试题
  • 35道Jenkis面试题
  • 78道MongoDB面试题
  • 17道ansible面试题
  • 60道dubbo面试题
  • 53道kafka面试
  • 18道mysql面试题
  • 40道nginx面试题
  • 77道redis面试题
  • 28道zookeeper

总计 1000+ 道面试题, 内容 又全含金量又高

  • 174道运维工程师面试题

1、什么是运维?

2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

3、现在给你三百台服务器,你怎么对他们进行管理?

4、简述raid0 raid1raid5二种工作模式的工作原理及特点

5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

7、Tomcat和Resin有什么区别,工作中你怎么选择?

8、什么是中间件?什么是jdk?

9、讲述一下Tomcat8005、8009、8080三个端口的含义?

10、什么叫CDN?

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

3、现在给你三百台服务器,你怎么对他们进行管理?

4、简述raid0 raid1raid5二种工作模式的工作原理及特点

5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

7、Tomcat和Resin有什么区别,工作中你怎么选择?

8、什么是中间件?什么是jdk?

9、讲述一下Tomcat8005、8009、8080三个端口的含义?

10、什么叫CDN?

11、什么叫网站灰度发布?

12、简述DNS进行域名解析的过程?

13、RabbitMQ是什么东西?

14、讲一下Keepalived的工作原理?

15、讲述一下LVS三种模式的工作过程?

16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

17、如何重置mysql root密码?

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-u6FGYUIc-1712878560069)]

  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值