• 在 React 项目中全量使用 Hooks


    一天时间系列文章是博主精心整理的面试热点问题和难点问题,吸收了大量的技术博客与面试文章,总结多年的面试经历,带你快速并高效地审视前端面试知识。直击技术痛点,主动出击,精密打击,这才是面试拿到高薪的秘诀!

    前言

    此篇文章整理了在 React 项目开发中常用的一些 Hooks

    React Hooks

    Hooks 只能用于函数组件当中

    useState

    import { useState } from 'react';
    
    const Component = () => {
      const [count, setCount] = useState(0);
      
      return (
        <button onClick={() => setCount(count + 1)}>click</button>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    此方法会返回两个值:当期状态和更新状态的函数。效果同 this.statethis.setState,区别是 useState 传入的值并不一定要对象,并且在更新的时候不会把当前的 state 与旧的 state 合并。

    useReducer

    useReducer 接收两个参数,第一个是 reducer 函数,通过该函数可以更新 state,第二个参数为 state 的初始值,是 useReducer返回的数组的第一个值,也是在 reducer 函数第一次被调用时传入的一个参数。

    基础用法

    import { useReducer } from 'react';
    
    const Component = () => {
      const [count, dispatch] = useReducer((count, action) => {
        switch (action) {
          case 'subtract':
            return count - 1;
          case 'add':
            return count + 1;
        }
      }, 0);
    
      return (
        <div>
          <span>{count}</span>
          <button onClick={() => dispatch('subtract')}>subtract</button>
          <button onClick={() => dispatch('add')}>add</button>
        </div>
      );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在基础用法中,返回一个 dispatch 通过 dispatch 触发不同的 action 来加减 state。这里既然能传string action 那么肯定也能传递更复杂的参数来面对更复杂的场景。

    进阶用法

    import { useReducer } from 'react';
    
    const Component = () => {
      const [userInfo, dispatch] = useReducer(
        (state, { type, payload }) => {
          switch (type) {
            case 'setName':
              return {
                ...state,
                name: payload
              };
            case 'setAge':
              return {
                ...state,
                age: payload
              };
          }
        },
        {
          name: 'Jace',
          age: 18
        }
      );
    
      return (
        <button onClick={() => dispatch({ type: 'setName', payload: 'John' })}>
          click
        </button>
      );
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    useContext

    在上述案例 useReducer中,我们将函数的参数改为一个对象,分别有typepayload 两个参数,type用来决定更新什么数据,payload 则是更新的数据。写过 react-redux 的同学可能发这个 reducer 与 react-redux 中的 reducer 很像,我们借助 react-redux 的思想可以实现一个对象部分更改的 reducer ,那么我们便可以使用 React Hooks 的 useContext来实现一个状态管理。

    import { useMemo, createContext, useContext, useReducer } from 'react';
    
    const store = createContext([]);
    
    const App = () => {
      const reducerValue = useReducer(
        (state, { type, payload }) => {
          switch (type) {
            case 'setName':
              return {
                ...state,
                name: payload
              };
            case 'setAge':
              return {
                ...state,
                age: payload
              };
          }
        },
        {
          name: 'Jace',
          age: 18
        }
      );
      const [state, dispatch] = reducerValue;
      
      const storeValue = useMemo(() => reducerValue, reducerValue);
      
      return (
        <store.Provider value={storeValue}>
          <Child />
        </store.Provider>
      );
    };
    
    const Child = () => {
      const [state, dispatch] = useContext(store); // 在子组件中使用
      console.log(state);
      return (
        <button onClick={() => dispatch({ type: 'setName', payload: 'John' })}>
          click
        </button>
      );
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    useEffect

    import { useState, useEffect } from 'react';
    
    let timer = null;
    
    const Component = () => {
      const [count, setCount] = useState(0);
      
      // 类似于 class 组件的 componentDidMount 和 componentDidUpdate:
      useEffect(() => {
        document.title = `You clicked ${count} times`;
        
        timer = setInterval(() => {
          // events ...
        }, 1000)
        
        return () => {
          // 类似 componentWillUnmount
          // unmount events ...
          clearInterval(timer); // 组件卸载、useEffect 更新 移除计时器
        };
      }, [count]);
      
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    如果 useEffect第二个参数数组内的值发生了变化,那么useEffect第一个参数的回调将会被再执行一遍,这里要注意的useEffect 的返回值函数并不只是再组件卸载的时候执行,而是在这个 useEffect 被更新的时候也会调用,例如上述 count 发生变化后,useEffect 返回的方法也会被执行,具体原因见Using the Effect Hook – React (reactjs.org)

    useLayoutEffect

    useLayoutEffectuseEffect的API相同,区别:useEffect在浏览器渲染后执行,useLayoutEffect 在浏览器渲染之前执行,由于JS是单线程,所以 useLayoutEffect 还会阻塞浏览器的渲染。区别就是这,那么应用场景肯定是从区别中得到的,useLayoutEffect在渲染前执行,也就是说我们如果有状态变了需要依据该状态来操作DOM,为了避免状态变化导致组件渲染,然后更新 DOM 后又渲染,给用户肉眼能看到的闪烁,我们可以在这种情况下使用 useLayoutEffect

    当然这个不只是状态的改变,在任何导致组件重新渲染,而且又要改变 DOM的情况下都是 useLayoutEffect的使用场景。当然这种场景不多,useLayoutEffect 也不能多用,且使用时同步操作时长不能过长,不然会给用户带来明显的卡顿。

    useRef

    细心的同学有可能发现我在上面写 useEffect 中有一个 timer 变量,我将其定义在了函数组件外面,这样写简单使用是没问题的,但是如果该组件在同一页面有多个实例,那么组件外部的这个变量将会成共用的,会带来一个冲突,所以我们需要一个能在函数组件声明周期内部的变量,可以使用 useState 中的 state 但是 state 发生变化组件也会随之刷新,在有些情况是不需要刷新的,只是想单纯的存一个值,例如计时器的timer 以及子组件的 Ref 实例等等。

    import React, { useRef, useState, useEffect } from 'react';
    
    const Compnent = () => {
      const timer = useRef(null);
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        clearInterval(timer.current);
        timer.current = setTimeout(() => {
          setCount(count + 1);
        }, 1000);
      }, [count]);
    
      return <div>UseRef count: {count}</div>;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    useRef只接受一个参数,就是 初始值,之后可以通过赋值 ref.current来更改,我们可以将一些不影响组件声明周期的参数放在 ref 中,还可以将 ref 直接传递给子组件 子元素。

    const ref = useRef();
    
    <div ref={ref}>Hello</div>
    // or
    <Child ref={ref} />
    
    • 1
    • 2
    • 3
    • 4
    • 5

    或许有同学这时候会想到,当子组件为 Class 组件时,ref 获取的是 Class 组件的实例,上面包含 Class 的所有方法属性等。但当子组件为 Function 组件时,ref 能拿到什么,总不可能是 function 内定义的方法、变量。

    useImperativeHandle

    import React, { useRef, useState, useImperativeHandle } from 'react';
    
    const App = () => {
      const ref = useRef();
      return (
          <Child ref={ref} />
      );
    };
    
    const Child = React.forwardRef((props, ref) => {
      const inputRef = useRef();
      const [value, setValue] = useState(1);
      
      useImperativeHandle(ref, () => ({
        value, // 内部变量
        setValue, // 方法
        input: inputRef.current // Ref
      }));
      
      return (
        <input value={value} inputRef={inputRef} />
      );
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用 useImperativeHandle 钩子可以自定义将子组件中任何的变量,挂载到 ref 上。React.forwardRef方法可以让组件能接收到 ref ,然后再使用或者透传到更下层。

    useCallback

    import React, { useCallback } from 'react';
    
    const Component = () => {
      const setUserInfo = payload => {}; // request api
    
      const updateUserInfo = useCallback(payload => {
        setUserInfo(Object.assign({}, userInfo, payload));
      }, [userInfo]);
      
      return (
        <UserCard updateUserInfo={updateUserInfo}/>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    useCallback 会在二个参数的依赖项发生改变后才重新更新,如果将此函数传递到子组件时,每次父组件渲染此函数更新,就会导致子组件也重新渲染,可以通过传递第二个参数以避免一些非必要性的渲染。

    useMemo

    import React, { useMemo } from 'react';
    
    const Component = () => {
      const [count, setCount] = useState(0);
     
      const sum = useMemo(() => {
        // 求和逻辑
        return sum;
      }, [count]);
      
      return <div>{sum}</div>
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    useMemo 的用法跟 useCallback 一样,区别就是一个返回的是缓存的方法,一个返回的是缓存的值。上述如果依赖值 count 不发生变化,计算 sum 的逻辑也就只会执行一次,从而性能。


    React Redux Hooks

    useSelector

    import { shallowEqual, useSelector } from 'react-redux';
    
    const Component = () => {
      const userInfo = useSelector(state => state.userInfo, shallowEqual);
      
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    useSelector 的第二个参数是一个比较函数,useSelector 中默认使用的是 ===来判断两次计算的结果是否相同,如果我们返回的是一个对象,那么在 useSelector 中每次调用都会返回一个新对象,所以所以为了减少一些没必要的re-render,我们可以使用一些比较函数,如 react-redux 自带的shallowEqual,或者是 Lodash 的 _.isEqual()、Immutable 的比较功能。

    useDispatch

    import React, { useCallback } from 'react';
    import { useDispatch } from 'react-redux';
    
    const Compnent = () => {
      const dispatch = useDispatch();
      const clearUserInfo = useCallback(
        () => dispatch({ type: 'clearUserInfo' }),
        [dispatch]
      );
      
      return (
        <button onClick={clearUserInfo}>click</buttn>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用 dispatch 来调度操作,加上useCallback来减少不必要的渲染。


    React Router Hooks

    useHistory

    import { useHistory } from 'react-router';
    
    const Compnent = () => {
      const history = useHistory();
      
      return (
        <button onClick={() => history.push('/home')}>go home</buttn>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    useLocation

    import React, { useEffect } from 'react';
    import { useLocation } from 'react-router';
    
    const Compnent = () => {
      const location = useLocation();
      
      useEffect(() => {
        // ...
      }, [location])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    URL一发生变化,将返回新的 location,一般可以用来监听 location.search

    useParams

    import { useParams, useEffect } from 'react-router';
    
    const Component = () => {
      const params = useParams();
      
      const getUserInfo = id => { // request api
        // some event
      };
      useEffect(() => {
        // parms 的 uid 发生变化就会重新请求用户信息
        getUserInfo(params.uid);
      }, [params.uid]);
      
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    useParams 返回 react-router 的参数键值对

    useRouteMatch

    import { useRouteMatch } from 'react-router';
    
    const Component = () => {
      const match = useRouteMatch('/login');
      
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    useRouteMatch 可以传入一个参数path,不传参数则返回当前路由的参数信息,如果传了参数则用来判断当前路由是否能匹配上传递的 path,适用于判断一些全局性组件在不同路由下差异化的展示。

    参考

    结语

    使用 Hooks 能为开发提升不少效率,但并不代表就要抛弃 Class Component,依旧还有很多场景我们还得用到它,比如需要封装一个公共的可继承的组件,当然通过自定义 hooks 也能将一些共用的逻辑进行封装,以便再多个组件内共用。

    下期更新在React 中自定义 Hooks 的应用场景 ,主要讲一些 Hooks 的高阶应用

    🌈文末福利:搜索公众号【前端二次元】回复关键字「前端资料」,领取前端系统课程,涵盖前端所有内容

  • 相关阅读:
    【JS】【掘金】获取关注了里不在关注者里的人
    Squeeze-and-Excitation Networks
    基于Matlab创建跟踪场景、模拟目标运动和模拟雷达检测仿真(附源码)
    前端基础建设与架构05 Vite 实现:从源码分析出发,构建 bundlele 开发工程
    CMake中configure_file的使用
    2024.6.19刷题记录
    Pthon中的文件处理
    【系统架构设计】 架构核心知识: 2 云原生架构
    基于Http协议实现网络爬虫读取数据
    使用jq对json去重的一个小细节
  • 原文地址:https://blog.csdn.net/qq_37215621/article/details/127235699