1. React HooksReact 16.8 引入的一项重要特性,它使函数组件能够拥有类组件的一些特性,例如状态管理和生命周期方法的使用;

  2. 通过 Hooks 可以更加简洁和灵活地编写 React 组件;

什么是 React Hooks

  1. React Hooks 是一种函数式组件的增强机制,它允许在不编写类组件的情况下使用 React 的特性;

  2. 主要的 Hooks 包括 useState、useEffect、useContext、useReducer、useCallback、useMemo、useRefuseImperativeHandle 等,这些 Hooks 提供了访问 React 特性的方式,可以更好地组织和重用代码;

class 组件的问题

  1. 复杂组件变得难以理解

    1. 在最初编写一个 class 组件时,往往逻辑比较简单,并不会非常复杂,但是随着业务的增多,class 组件会变得越来越复杂;
    2. 比如 componentDidMount 中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听 (还需要在 componentWillUnmount 中移除),而对于这样的 class 实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
  2. 难以理解的 class,处理 this 比较麻烦;

  3. 组件复用状态很难;

使用 Hooks 的好处

  1. 更简洁的组件逻辑:无需编写类组件,可以使用函数组件和 Hooks 来管理状态和生命周期;

  2. 提高代码复用性:Hooks 可以将逻辑提取到可重用的函数中,减少重复代码;

  3. 更好的性能优化:使用 useEffect、useCallback、useMemoHooks 可以更精确地控制副作用和性能消耗;

注意事项

  1. 仅在顶层使用 Hooks:不要在循环、条件或嵌套函数中调用 Hook,确保 Hooks 在每次渲染时都以相同的顺序被调用;

  2. 使用 ESLint 插件:React 官方提供了 eslint-plugin-react-hooks 插件来帮助检查 Hook 的使用是否正确;

Effect Hook

  1. Effect Hook 用于在函数组件中处理副作用:

    1. ajax 请求
    2. 计时器
    3. 其他异步操作
    4. 更改真实 DOM 对象
    5. 本地存储
    6. 其他会对外部产生影响的操作
  2. 现在有一个需求:页面的 title 总是显示 counter 的数字 (生命周期 VS Effect Hook)

    JSX
    JSX
    // 使用生命周期函数实现
    import React, { PureComponent } from "react";
    
    export default class extends PureComponent {
      constructor(props) {
        super(props);
    
        this.state = {
          counter: 0,
        };
      }
    
      componentDidMount() {
        document.title = `当前计数: ${this.state.counter}`;
      }
      componentDidUpdate() {
        document.title = `当前计数: ${this.state.counter}`;
      }
    
      render() {
        console.log("111");
        return (
          <div>
            <h2>当前计数: {this.state.counter}</h2>
            <button onClick={(e) => this.increment()}>+1</button>
            <button onClick={(e) => this.decrement()}>-1</button>
          </div>
        );
      }
    
      increment() {
        this.setState({ counter: this.state.counter + 1 });
      }
      decrement() {
        this.setState({ counter: this.state.counter - 1 });
      }
    }
    
    // 使用 Effect Hook 实现
    import React, { useState, useEffect } from "react";
    
    export default function () {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        document.title = `当前计数: ${count}`;
      });
    
      return (
        <div>
          <h2>当前计数: {count}</h2>
          <button onClick={(e) => setCount(count + 1)}>+1</button>
          <button onClick={(e) => setCount(count - 1)}>-1</button>
        </div>
      );
    }
    

语法

  1. useEffect(setup, dependencies?)

    useEffect(
      () => {
        // 组件挂载、更新的时候执行的副作用操作
    
        return () => {/* 在组件卸载时,执行的清理操作(可选) */ };
      },
      [/* 依赖数组 */]
    );
    
  2. 细节

    1. 副作用函数的运行时间点,是在页面完成真实的 UI 渲染之后。因此它的执行是异步的,并且不会阻塞浏览器:
      1. componentDidMountcomponentDidUpdate 更改了真实 DOM,但是用户还没有看到 UI 更新,同步的;
      2. useEffect 中的副作用函数,更改了真实 DOM,并且用户已经看到了 UI 更新,异步的;
    2. 每个函数组件中,可以多次使用 useEffect,但不要放入判断或循环等代码块中;
    3. useEffect 中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数,组件被销毁时一定会运行;
    4. useEffect 函数,可以传递第二个参数:
      1. 第二个参数是一个数组,记录该副作用的依赖数据,当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用;
      2. 所以,当传递了依赖数据之后,如果数据没有发生变化,副作用函数仅在第一次渲染后运行,清理函数仅在卸载组件后运行;
    5. 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化;
    6. 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂;

依赖值为对象

经常会将一个对象作为依赖,一般都是希望对象的内容发生变化时,去执行某些操作,在实际的业务开发中,会遇到一些莫名其妙的坑,列举几个常见的现象:

  1. 明明对象的内容已经发生了变化,但是为什么没有触发 useEffect
  2. 明明对象的内容没有发生变化,但是为什么一直触发 useEffect
  1. 改变对象中的属性值,未触发 useEffect

    import React, { useState, useEffect } from "react";
    
    export default function () {
      const [info, setInfo] = useState({
        name: "张三",
        age: 18,
      });
    
      useEffect(() => {
        console.log("info", info);
      }, [info]);
    
      function handleChangeName(e) {
        const value = e.target.value;
    
        // 错误写法
        setInfo((info) => {
          info.name = value;
          return info;
        });
    
        // 正确写法 1
        setInfo({ ...info, name: value });
        // 正确写法 2
        setInfo((info) => ({ ...info, name: value }));
      }
    
      return <input onChange={handleChangeName} />;
    }
    
  2. 接受父组件的对象属性作为依赖,useEffect 频繁触发;

    import { useState, useEffect } from "react";
    
    const SubCom = (props) => {
      // 当父组件更新时,会重新渲染子组件,每次渲染,props.list 都被赋予了新的引用, 虽然看起来都是空数组,但是useEffect 是判断list的引用发生了变化,所以就会执行
      // 在用到的地方去做兼容处理,而不是直接赋予默认值
      const { list = [], count } = props;
    
      useEffect(() => {
        console.log(list);
      }, [list]);
    
      return <div>子组件{count}</div>;
    };
    
    export default () => {
      const [count, setCount] = useState(0);
    
      function hanleOnClick() {
        setCount((count) => count + 1);
      }
    
      return (
        <div>
          <button onClick={hanleOnClick}>add</button>
          {/* 如果父组件没有传递 list 属性,每当父组件重新渲染时,子组件会跟随重新渲染,每次渲染都会触发 useEffect */}
          <SubCom count={count} />
        </div>
      );
    };
    
  3. 对象内容未变化时,不希望触发 useEffect

    1. 业务层经常会对一些状态进行重置,setState([]) 或者 setState({}),有可能本身 state 的值就是 [] 或者 {},重置后,内容未发生变化,但是引用已经改变,从而导致触发 useEffect
    2. 解决方案:将对象转为字符串后再作为 useEffect 的依赖;
  4. 第一次渲染时,不希望触发 useEffect,可通过创建一个标志位来解决;

    import { useState, useEffect, useRef } from "react";
    
    export default () => {
      const [count, setCount] = useState(0);
      const isMounted = useRef(false);
    
      // 第一次渲染置为false
      useEffect(() => {
        isMounted.current = false;
      }, []);
    
      // 第一次渲染将标志位置为 true
      useEffect(() => {
        if (!isMounted.current) {
          isMounted.current = true;
        } else {
          console.log("第一渲染时不会执行,后续更新才会执行");
        }
      }, [count]);
    
      return (
        <div>
          <button onClick={() => setCount((c) => c + 1)}>+1</button>
        </div>
      );
    };
    
  5. 两个 useEffect 更新相互依赖,无限更新导致白屏;

    import { useState, useEffect } from "react";
    
    export default () => {
      const { value, defaultValue = 0.5, onChange } = props; // 自定义的表单组件
    
      const [innerValue, setInnerValue] = useState < number > defaultValue;
    
      // 取名为 useEffect1
      // 正确写法:应该使用 onChange 去设置 setInnerValue
      useEffect(() => {
        if (value !== undefined) {
          setInnerValue(value);
        }
      }, [value]);
    
      // 取名为 useEffect2
      useEffect(() => {
        onChange?.(innerValue);
      }, [innerValue]);
    };
    
  6. 不要将普通变量作为依赖;

    import { useState, useEffect } from "react";
    
    export default () => {
      const [count, setCount] = useState(0);
    
      // 组件在每次更新时,会对 list 赋予新的值,与 案例2 原理相同
      const list = [];
    
      useEffect(() => {
        console.log("触发useEffect", count);
      }, [list]);
    
      return (
        <div>
          <p>{count}</p>
          <button onClick={() => setCount((c) => c + 1)}>+1</button>
        </div>
      );
    };
    
  7. 依赖监听 useRef 的值,有时可以触发更新,有时无法触发更新;

    1. 现象:
      • 点击 button1 时,会触发 useEffect1
      • 点击 button2 时,不会触发 useEffect2
      • 再次点击 button1 时,会触发 useEffect1useEffect2
    2. 问题原因:只有状态变更的时候,才会触发更新,而状态变更,只有 useStateuseReducer 可以触发更新;
    3. 使用指南:建议不要使用 useRef 的值作为依赖,除非十分确定当 useRef 的值改变时,有 state 发生了改变;
    import { useState, useEffect, useRef } from "react";
    
    export default () => {
      const [count, setCount] = useState(0);
      const countRef = useRef(0);
    
      // 取名为 useEffect1
      useEffect(() => {
        console.log("count", count);
      }, [count]);
    
      // 取名为 useEffect2
      useEffect(() => {
        console.log("countRef", countRef);
      }, [countRef.current]);
    
      return (
        <div>
          <p>{count}</p>
          <button onClick={() => setCount((c) => c + 1)}>button1</button>
          <button onClick={() => (countRef.current += 1)}>button2</button>
        </div>
      );
    };
    

Context Hook

  1. 在之前的开发中,要在组件中使用共享的 Context 共享数据,但是多个 Context 共享时的方式会存在大量的嵌套,有两种方式:

    1. 类组件可以通过 类名.contextType = MyContext 方式,在类中获取 context
    2. 多个 Context 或者在 函数式 组件中通过 MyContext.Consumer 方式共享 context
  2. Context Hook 允许通过 Hook 来直接获取某个 Context 的值;

    JSX
    JSX
    // App.js
    import React, { createContext } from "react";
    import ContextHook from "./ContextHook";
    
    export const UserContext = createContext();
    export const ThemeContext = createContext();
    
    export default function App() {
      return (
        <div>
          <UserContext.Provider value={{ name: "why", age: 18 }}>
            <ThemeContext.Provider value={{ color: "red", fontSize: "20px" }}>
              <ContextHook />
            </ThemeContext.Provider>
          </UserContext.Provider>
        </div>
      );
    }
    
    // ContextHook.js
    import React, { useContext } from "react";
    import { UserContext, ThemeContext } from "./App";
    
    export default function ContextHook() {
      // 通过 Hook 来直接获取某个 UserContext、ThemeContext 的值
      // 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext provider 的 context value 值
      const user = useContext(UserContext);
      const theme = useContext(ThemeContext);
      console.log(user);
      console.log(theme);
    
      return <div>ContextHook</div>;
    }
    

useReducer

  1. useReducer* 仅仅是 useState 的一种替代方案:

    1. 在某些场景下,如果 state 的处理逻辑比较复杂,可以通过 useReducer 来对其进行拆分;
    2. 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;
    JSX
    JSX
    // counter.js
    export function counterReducer(state, action) {
      switch (action.type) {
        case "increment":
          return { ...state, counter: state.counter + 1 };
        case "decrement":
          return { ...state, counter: state.counter - 1 };
        default:
          return state;
      }
    }
    
    // App.js
    import React, { useReducer } from "react";
    import { counterReducer } from "./counter";
    
    export default function Home() {
      const [state, dispatch] = useReducer(counterReducer, { counter: 100 });
    
      return (
        <div>
          <h2>当前计数: {state.counter}</h2>
          <button onClick={(e) => dispatch({ type: "increment" })}>+1</button>
          <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button>
        </div>
      );
    }
    

useCallback

useMemo

useRef

useImperativeHandle

useLayoutEffect

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 什么是 React Hooks
    1. 1.1. class 组件的问题
    2. 1.2. 使用 Hooks 的好处
    3. 1.3. 注意事项
  2. 2. Effect Hook
    1. 2.1. 语法
    2. 2.2. 依赖值为对象
  3. 3. Context Hook
  4. 4. useReducer
  5. 5. useCallback
  6. 6. useMemo
  7. 7. useRef
  8. 8. useImperativeHandle
  9. 9. useLayoutEffect