React hooks

73 阅读6分钟

获取上一轮的state或props

使用loadsh提供的isEqual或其他工具库函数进行深比较。

// 获取上一轮数据的函数
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
​
const [count, setCount] = useState(0);  // 目前的count
const prevCount = usePrevious(count); // 上一轮的count

useEffect

useEffect可以帮助我们在DOM更新完成后执行某些副作用操作,如数据获取,设置订阅以及手动更改 React 组件中的 DOM 等。

有了useEffect,我们可以在函数组件中实现 像类组件中的生命周期那样某个阶段做某件事情 (具有componentDidMountcomponentDidUpdatecomponentWillUnmount的功能。

useEffect()规则:

  • 没有传第二个参数时,在每次 render 之后都会执行 useEffect中的内容
  • useEffect接受第二个参数来控制跳过执行,下次 render 后如果指定的值没有变化就不会执行
  • useEffect 是在 render 之后浏览器已经渲染结束才执行

useEffect 的第二个参数是可选的,类型是一个数组。

  第二个参数是空数组:

  • useEffect 只在第一次渲染时执行,由于空数组中没有值,始终没有改变,所以后续render不执行,相当于生命周期中的componentDidMount

    useEffect(() => { console.log('只在第一次渲染时执行') }, []); 
    

  第二个参数为非空数组:

  • 无论数组中有几个元素,数组中只要有任意一项发生了改变,useEffect 都会调用。

  useEffect用作componentWillUnmount

  • useEffect可以像让我们在组件即将卸载前做一些清除操作,如清空数据,清除计时器
  • 使用方法 :只需在现有的useEffect中返回一个函数,函数中为组件即将卸载前要做的操作
useEffect(() => { 
    getStuInfo({ id: stuId }); 
    // 返回一个函数,在组件即将卸载前执行
    return ()=> {
        clearTimeout(Timer);   // 清除定时器
        data = null;   // 清空页面数据,当我们希望页面切换回来时不显示之前的内容时在组件卸载前清空数据,常用于搜索页面,切回时显示空内容,需重新搜索
    }
}, [getStuInfo, stuId]); 

useCallback

基本语法:

const memoizedCallback = useCallback(
    () => {
        doSomething(a,b);
    },
    [a,b]
);
  • 返回一个memoized回调函数。
  • 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
  • 当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
useCallback(fn, deps) 
相当于 
useMemo(() => fn, deps)。

useMemo

项目中useMemo的大部分使用场景是在实现table的可变列的时候使用,所以学习一下useMemo( )的主要用法。

之前没有注意useMemo( )的使用时,遇到的问题:

tableFilters改变时 对应的columns也要改变并且重新渲染 当时没有注意到传入table的columns经过useMemo的限制导致每次tableFilters改变之后组件重新渲染filters的值被重新刷新清空。 解决问题:useMemo的依赖值添加上tableFilters

image.png

const columns: Column[] = [
    {
      title: '状态',
      dataIndex: 'status',
      filteredValue: tableFilters.status,
      filters: [
        {
          text: '运行中',
          value: DOMAIN_STATUS_TYPE.RUNING,
        },
        {
          text: '配置中',
          value: DOMAIN_STATUS_TYPE.CONFIGING,
        },
      ],
      width: 120,
      render: (col: string, item: DomainBaseInfo) => <DomainStatus detail={item} />,
    },
    {
      title: 'CNAME状态',
      dataIndex: 'cname_status',
      filteredValue: tableFilters.cname_status,
      filters: [
        {
          text: '待配置',
          value: CNAME_STATUS_TYPE.PENDING_CONFIG,
        },
        {
          text: '已配置',
          value: CNAME_STATUS_TYPE.RESOLVE_CONFIG,
        },
      ],
      render: (col: number, item: DomainBaseInfo) => (
        <div className="flex-start flex-vertical-center ">
          <Tag className="m-r-8" color={CNAME_STATUS[col].color}>
            {CNAME_STATUS[col].text}
          </Tag>
          {item?.ownership_status === OWNERSHIP_STATUS_TYPE.CONFIG_PASSED && <PeddingSetting domain={item?.domain} />}
        </div>
      ),
    },
    ...
  ];
​
  const [checkedColumn, setCheckedColumn] = useState<any[]>(columns.map(item => item?.dataIndex));
​
  const showedColumns = useMemo(
    () => columns.filter(item => checkedColumn.includes(item?.dataIndex)),
    [checkedColumn, tableFilters],
  );

基本语法:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 返回一个 memoized 值。
  • 把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
  • 记住,传入 useMemo 的函数会在渲染期间执行。
  • 如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useCallback 和 useMemo的区别:

  • 相同点 :useCallback 和 useMemo 都是性能优化的手段,类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。
  • 区别 :useCallback 和 useMemo 的区别是useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件。
const renderButton = useCallback(
     () => (
         <Button type="link">
            {buttonText}
         </Button>
     ),
     [buttonText]    // 当buttonText改变时才重新渲染renderButton
);
​
​
// useMemo返回的的是一个值,用于避免在每次渲染时都进行高开销的计算。
// 仅当num改变时才重新计算结果
const result = useMemo(() => {
    for (let i = 0; i < 100000; i++) {
      (num * Math.pow(2, 15)) / 9;
    }
}, [num]);

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。

注:useState的值的改变,会引起render函数的渲染。useRef的.current的属性改变时 组件不会重新渲染。

useLayoutEffect

最近在学习react-router-dom的源码时,看到了对useLayoutEffect( )的使用,所以学习一下。

React.useLayoutEffect(() => history.listen(setState), [history]);

useLayoutEffect使用方法、所传参数和useEffect完全相同。

  不同点:

  • useEffect是异步执行的,而useLayoutEffect是同步执行的。
  • useLayoutEffect等同于componentDidMount、componentDidUpdate,因为他们调用阶段是相同的。useEffect是在componentDidMount、componentDidUpdate调用之后才会触发的。

当组件所有DOM都渲染完成后,同步调用useLayoutEffect,然后再调用useEffect。useLayoutEffect永远要比useEffect先触发完成。

那通常在useLayoutEffect阶段我们可以做什么呢?

  • 当页面挂载或渲染完成时,再给你一次机会对页面进行修改。
  • 在触发useLayoutEffect阶段时,页面全部DOM已经渲染完成,此时可以获取当前页面所有信息,包括页面显示布局等,你可以根据需求修改调整页面。
  • useLayoutEffect对页面的某些修改调整可能会触发组件重新渲染。如果是对DOM进行一些样式调整是不会触发重新渲染的,这点和useEffect是相同的。

在react官方文档中,明确表示只有在useEffect不能满足你组件需求的情况下,才应该考虑使用useLayoutEffect。 官方推荐优先使用useEffect。

useEffect和useLayoutEffect在服务器端渲染时,都不行,需要寻求别的解决方案。

 具体表现:

  • 使用useEffect的渲染是异步的,所以有时候会导致0先渲染在页面上,再出现随机数。这里与执行的快慢电脑的运行有关,所以我们可以阻塞一会能更清楚看出来效果。
  • useLayoutEffect是同步的,直接渲染出随机数出来。
import React, { useEffect, useLayoutEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (count === 0) {
      const randomNum = Math.random() * 100; //随机生成一个数字

      const now = performance.now();

      while (performance.now() - now < 50) {
        //阻塞一段时间
        console.log("blocking...");
      }

      setCount(randomNum); //重新设置状态,设置成随机数
    }
  }, [count]);

  //用 useLayoutEffect 试试
  // useLayoutEffect(() => {
  //   if (count === 0) {
  //     const randomNum = Math.random() * 100; //随机生成一个数字

  //     const now = performance.now();

  //     while (performance.now() - now < 50) {
  //       //阻塞一段时间
  //       console.log("blocking...");
  //     }

  //     setCount(randomNum); //重新设置状态,设置成随机数
  //   }
  // }, [count]);

  return <div onClick={() => setCount(0)}>{count}</div>;
}

1-20220817220239-sj5et8n.gif

2-20220817220333-t1703ug.gif

useClickOutside

开发火山项目的过程中 想要实现点击外部关闭气泡确认框 Popconfirm的效果:

使用到了@byted/hooks中的useClickOutside()函数

useClickOutside()函数的实现:

// 鼠标点击事件,click 不会监听右键
var defaultEvent = 'click';

function useClickOutside() {
  var dom = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined;
  var onClickAway = arguments.length > 1 ? arguments[1] : undefined;
  var eventName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : defaultEvent;
  var element = (0, _react.useRef)();
  var handler = (0, _react.useCallback)(function (event) {
    var targetElement = typeof dom === 'function' ? dom() : dom;
    var el = targetElement || element.current; // https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath
    // 1. 包裹元素包含点击元素
    // 2. 点击元素曾在包裹元素内

    if (!el || el.contains(event.target) || event.composedPath && event.composedPath().includes(el)) {
      return;
    }

    onClickAway(event);
  }, [element.current, onClickAway, dom]);
  (0, _react.useEffect)(function () {
    document.addEventListener(eventName, handler);
    return function () {
      document.removeEventListener(eventName, handler);
    };
  }, [eventName, handler]);
  return element;
}

useClickOutside()函数的使用:

useClickOutside(
    () => document.getElementsByClassName('net-fe-edit-modal-container')[0] as HTMLElement,
    (event: KeyboardEvent) => {
      console.log(event);
      const nodeName = (event.target as HTMLElement).nodeName;
      if (!['svg', 'path'].includes(nodeName)) {
        setPopupVisible(false);
      }
    }
  );