彻底理解React-Hooks

为什么需要

HookReact 16.8 的新增特性,它可以让我们在不编写class的情况下使用state以及其他的React特性(比如生命周期),先来思考一下class组件相对于函数式组件有什么优势?比较常见的是下面的优势:

  • class组件可以定义自己的state,用来保存组件自己内部的状态;函数式组件不可以,因为函数每次调用都会产生新的临时变量

  • class组件有自己的生命周期,可以在生命周期中完成自己的逻辑

    • 比如在componentDidMount中发送网络请求,并且该生命周期函数只会执行一次

    • 函数式组件在学习hooks之前,如果在函数中发送网络请求,意味着每次重新渲染都会重新发送一次网络请求

  • class组件可以在状态改变时只会重新执行render函数以及希望重新调用的生命周期函数componentDidUpdate

    • 函数式组件在重新渲染时,整个函数都会被执行,似乎没有什么地方可以只让它们调用一次
  • 所以在Hook出现之前,对于上面这些情况我们通常都会编写class组件

  • Class组件存在的问题:

    • 复杂组件变得难以理解:随着业务的增多,class组件会变得越来越复杂

    • 难以理解的class:比如在class中,必须搞清楚this的指向到底是谁

    • 组件复用状态很难:一些状态的复用需要通过高阶组件,类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,代码会存在很多嵌套

  • Hook的出现,可以解决上面提到的这些问题:

    • 可以让我们在不编写class的情况下使用state以及其他的React特性
  • Hook的使用场景:

    • Hook的出现基本可以代替之前所有使用class组件的地方

    • 如果是一个旧的项目,不需要直接将所有的代码重构为Hooks,它完全向下兼容,可以渐进式的来使用它

    • Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用

  • Hook的规则:

    • 只能在函数最外层调用 Hook,不要在循环、条件判断或者子函数中调用

    • 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用

  • 通过一个计数器案例,来对比一下class组件和函数式组件结合hooks

  • create-react-app learn_react_router创建项目练习

  • src/App.jsx整体练习代码如下:

    js 复制代码
    import React, { memo } from 'react'
    import ComUseState from './components/ComUseState'
    import ComUseEffect from './components/ComUseEffect'
    import ComUseContext from './components/ComUseContext'
    import ComUseReducer from './components/ComUseReducer'
    import ComUseCallback from './components/ComUseCallback'
    import ComUseMemo from './components/ComUseMemo'
    import ComUseRef from './components/ComUseRef'
    import ComUseImperativeHandle from './components/ComUseImperativeHandle'
    import ComUseLayoutEffect from './components/ComUseLayoutEffect'
    import { ThemeContext, UserContext } from './context'
    import MyUseLogLift from './components/MyUseLogLift'
    import MyPublicContext from './components/MyPublicContext'
    import MyPageScroll from './components/MyPageScroll'
    import MyUseLocalStorage from './components/MyUseLocalStorage'
    import ComReduxHooks from './components/ComReduxHooks'
    import ComUseTransition from './components/ComUseTransition'
    import ComUseDeferredValue from './components/ComUseDeferredValue'
    import { Provider } from 'react-redux'
    import store from './store'
    import ComUseId from './components/ComUseId'
    
    const App = memo(() => {
      return (
        <div style={{height: '2000px'}}>
          <ComUseState />
          <hr />
          <ComUseEffect />
          <hr />
          <ThemeContext.Provider value={{color: 'red', fontSize: '24px'}}>
            <UserContext.Provider value={{name: 'WuLi'}}>
              <ComUseContext />
            </UserContext.Provider>
          </ThemeContext.Provider>
          <hr />
          <ComUseReducer />
          <hr />
          <ComUseCallback />
          <hr />
          <ComUseMemo />
          <hr />
          <ComUseRef />
          <hr />
          <ComUseImperativeHandle />
          <hr />
          <ComUseLayoutEffect />
          <hr />
          {/* 自定义hooks */}
          <h3>自定义hooks</h3>
          <MyUseLogLift />
          <ThemeContext.Provider value={{color: 'red', fontSize: '24px'}}>
            <UserContext.Provider value={{name: 'WuLi'}}>
              <MyPublicContext />
            </UserContext.Provider>
          </ThemeContext.Provider>
          <MyPageScroll />
          <MyUseLocalStorage />
          <hr />
          {/* redux里面的两个hook */}
          <Provider store={store}>
            <ComReduxHooks />
          </Provider>
          <hr />
          <ComUseId />
          <hr />
          {/* react18新增的两个hook */}
          <div style={{display:'flex'}}>
            <ComUseTransition />
            <ComUseDeferredValue />
          </div>
        </div>
      )
    })
    
    export default App

useState

useStateReact 中的一个 Hook,用于在函数组件中添加状态管理。它允许你在函数组件中声明和更新状态,而无需将组件转换为类组件

  • const [state, setState] = useState(initialState)

  • state:当前的状态值

  • setState:用于更新状态的函数

    • 是异步的更新状态后立即访问状态值可能得到的是旧值

    • 如果新状态依赖于旧状态,可以使用函数式更新 setCount((prevCount) => prevCount + 1)

    • 对于对象或数组状态,避免直接修改,而是创建新的对象或数组

      js 复制代码
      setUser({ ...user, name: 'New Name' });
      setTodos([...todos, newTodo]);
  • initialState:状态的初始值,可以是任何数据类型(如数字、字符串、对象、数组等)

    • 如果初始状态需要复杂计算,可以传递一个函数 const [state, setState] = useState(() => computeInitialState())

    • 如果没有传递参数,那么初始化值为undefined

  • 练习代码如下:

    js 复制代码
    // component/ComUseState.jsx
    import React, { memo } from 'react'
    import { useState } from 'react'
    
    const ComUseState = memo(() => {
      // 初始值只有在组件第一次渲染时才有效,第二次渲染传入什么值都无效
      const [count, setCount] = useState(0)
      return (
        <div>
          <h3>useState的使用</h3>
          <p>count: {count}</p>
          <button onClick={e=>setCount(count+5)}>+5</button>
        </div>
      )
    })
    
    export default ComUseState

Effect Hook

目前已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?Effect Hook 可以让你来完成一些类似于class中生命周期的功能

useEffect

  • useEffectReact 中的一个 Hook,用于在函数组件中执行副作用操作

  • 副作用操作包括数据获取、订阅、手动操作 DOM

  • useEffect 可以看作是类组件中 componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合

  • useEffect要求传入一个回调函数 ,在React执行完更新DOM操作之后,就会回调这个函数,默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个 回调函数

    js 复制代码
    useEffect(() => {
      // 副作用操作
      return () => {
        // 清理操作(可选)
      };
    }, [dependencies])
    • 第一个参数:一个函数,包含需要执行的副作用操作

    • 第二个参数(可选) :一个依赖数组,只有当依赖项发生变化时,才会重新执行副作用操作如果省略,副作用操作会在每次渲染后执行

  • 使用注意事项:

    • 依赖数组

      如果依赖数组为空,副作用操作只会在组件挂载和卸载时执行

      如果依赖数组包含某些状态或属性,副作用操作会在这些依赖项变化时执行

    • 清理操作

      如果副作用操作需要清理(如取消订阅、清除定时器),可以在 useEffect 中返回一个清理函数,会在组件更新和卸载的时候执行清除操作

    • 避免无限循环

      如果副作用操作中更新了状态,且该状态是依赖项之一,可能会导致无限循环。确保依赖项的选择是合理的

    • 多个 useEffect

      可以在一个组件中使用多个 useEffect,将不同的副作用操作分开管理,将按照 effect 声明的顺序依次调用组件中的每一个 effect

  • 代码示例如下:

    js 复制代码
    // cmponent/ComUseEffect.jsx
    import React, { memo } from 'react'
    import { useEffect,useState } from 'react'
    
    const ComUseEffect = memo(() => {
      const [count, setCount] = useState(20)
      /* 
        可以使用多个useEffect,更新完成后他们会按照你代码从上到下的顺序执行,卸载时也是
        useEffect():
        第一个参数:函数,函数中return函数表示更新和卸载之前执行
        第二个参数:数组,[放影响你代码执行的变量],[]空数组表示不受任何影响
       */
      useEffect(()=> {
        // 组件第一次渲染和每次更新完成都会执行此处代码
        document.title = count
        console.log('修改title');
      },[count])
      useEffect(()=> {
        console.log('发送网络请求');
        return ()=> {
          console.log("会在组件被卸载时, 才会执行一次")
          console.log('取消网络请求');
        }
      // 写了第二个参数[],表示不受任何值改变的影响,只会执行一次回调函数
      },[])
      useEffect(()=> {
        console.log('eventBus事件监听');
        return ()=> {
          console.log("会在组件被卸载时, 才会执行一次")
          console.log('取消eventBus事件监听');
        }
      },[])
      return (
        <div>
          <h3>useEffect的使用</h3>
          <p>count: {count}</p>
          <button onClick={e=>setCount(count+10)}>+10</button>
        </div>
      )
    })
    
    export default ComUseEffect

useLayoutEffect

useLayoutEffectReact 提供的一个 同步执行的副作用 Hook ,它的执行时机比 useEffect 更早,用于在浏览器绘制更新前执行代码

  • 语法:

    js 复制代码
    useLayoutEffect(() => {
      // 这里的代码在浏览器绘制前执行
      return () => {
        // 可选的清理函数
      };
    }, [依赖项])
  • useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新

  • useEffect比较: 大多数情况下,应该优先使用 useEffect

  • 练习代码如下:

    js 复制代码
    import React, { memo, /* useEffect, */ useLayoutEffect, useState } from 'react'
    
    const ComUseLayoutEffect = memo(() => {
      const [count, setCount] = useState(0)
      /* useEffect是在dom更新完成时执行,useLayoutEffect在dom更新之前执行 */
      useLayoutEffect(()=> {
        console.log('useLayoutEffect');
        // 点击按钮这里会在渲染成5之前先执行这个代码,不会渲染5会直接渲染下面的随机数,不会有闪烁
        if(count === 5) {
          setCount(Math.random()+9)
        }
      },[count])
      /* useEffect(()=> {
        console.log('useEffect');
        // 点击按钮这里会先渲染完成5然后在执行下面代码,会有闪烁
        if(count === 5) {
          setCount(Math.random()+9)
        }
      },[count]) */
      return (
        <div>
          <h3>useLayoutEffect的使用</h3>
          <p>当前计数:{count}</p>
          <button onClick={e=>setCount(5)}>设置为5</button>
        </div>
      )
    })
    
    export default ComUseLayoutEffect

useContext

在之前的开发中,要在组件中使用共享的Context有两种方式:

  • 类组件可以通过 类名.contextType = MyContext方式,在类中获取context

  • 多个Context或者在函数式组件中通过 MyContext.Consumer 方式共享context

  • 但是多个Context共享时的方式会存在大量的嵌套,Context Hook允许通过Hook来直接获取某个Context的值

  • 当组件上层最近的更新时,该 Hook 会触发重新渲染,并使用最新传递给 MyContext providercontext value

  • 练习代码如下:

    js 复制代码
    // component/ComUseContext.jsx
    import React, { memo } from 'react'
    import { useContext } from 'react'
    import { ThemeContext, UserContext } from '../context'
    
    const ComUseContext = memo(() => {
      // 当他导入的context的数据发生变化时,这个组件也会重新渲染
      const user = useContext(UserContext)
      const theme = useContext(ThemeContext)
      return (
        <div>
          <h3>useContext的使用</h3>
          <p>UserContext -- user.name: {user.name}</p>
          <p>ThemeContext -- theme -- color: {theme.color}-fontSize: {theme.fontSize}</p>
        </div>
      )
    })
    
    export default ComUseContext
    
    
    // src/context.js
    import { createContext } from "react";
    
    const UserContext = createContext()
    const ThemeContext = createContext()
    
    export {
      UserContext,
      ThemeContext
    }

useReducer

很多人看到useReducer的第一反应应该是redux的某个替代品其实并不是,useReducer仅仅是useState的一种替代方案:

  • 在某些场景下,如果state的处理逻辑比较复杂,可以通过useReducer来对其进行拆分

  • 或者这次修改的state需要依赖之前的state时,也可以使用

  • 数据是不会共享的,只是使用了相同的reducer的函数而已,所以useReducer只是useState的一种替代品,并不能替代Redux

  • useState相比较:

    js 复制代码
    import React, { useReducer } from "react";
    
    // 1. 定义 Reducer
    const reducer = (state, action) => {
      return { ...state, [action.field]: action.value };
    };
    
    // 2. 初始状态
    const initialState = { username: "", email: "" };
    
    function Form() {
      const [state, dispatch] = useReducer(reducer, initialState);
    
      return (
        <div>
          <input
            type="text"
            placeholder="用户名"
            value={state.username}
            onChange={(e) => dispatch({ field: "username", value: e.target.value })}
          />
          <input
            type="email"
            placeholder="邮箱"
            value={state.email}
            onChange={(e) => dispatch({ field: "email", value: e.target.value })}
          />
          <p>用户名: {state.username}</p>
          <p>邮箱: {state.email}</p>
        </div>
      );
    }
    
    export default Form;
  • 练习代码如下:

    js 复制代码
    import React, { memo, useReducer /* useState */ } from "react";
    
    function reducer(state, action) {
      switch (action.type) {
        case "addOneCount":
          return { ...state, count: state.count + 1 };
        case "subOneCount":
          return { ...state, count: state.count - 1 };
        case "addCount":
          return { ...state, count: state.count + action.num };
        case "subCount":
          return { ...state, count: state.count - action.num };
        default:
          return { ...state };
      }
    }
    const ComUseReducer = memo(() => {
      // const [count,setCount] = useState(0)
    
      // useReducer当定义多个数据时可以使用,但用的非常少,因为定义的reducer和组件分离了,如果数据很多时还不如直接使用redux省事
      const [state, dispath] = useReducer(reducer, {
        count: 0,
        user: {},
        banners: [],
      });
      return (
        <div>
          <h3>useReducer的使用</h3>
          {/* useReducer类似于useState的使用,逻辑复杂时使用 */}
          {/* <p>当前计数: {count}</p>
          <button onClick={e=>setCount(count + 15)}>+15</button>
          <button onClick={e=>setCount(count - 23)}>-23</button>
          <button onClick={e=>setCount(count + 1)}>+1</button>
          <button onClick={e=>setCount(count - 1)}>-1</button> */}
          <p>当前计数: {state.count}</p>
          <button onClick={(e) => dispath({ type: "addCount", num: 15 })}>
            +15
          </button>
          <button onClick={(e) => dispath({ type: "subCount", num: 23 })}>
            -23
          </button>
          <button onClick={(e) => dispath({ type: "addOneCount", num: 15 })}>
            +1
          </button>
          <button onClick={(e) => dispath({ type: "subOneCount" })}>-1</button>
        </div>
      );
    });
    
    export default ComUseReducer;

useRef

useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变,可以用于:

  • 获取 DOM 元素 (类似 document.getElementById

  • 存储可变值 (修改 .current 不会触发组件重新渲染)

  • 持久化存储数据 (组件重新渲染时数据不会丢失,适用于存储 setTimeout / setInterval ID,避免定时器无法清除)

  • const refContainer = useRef(initialValue)

    • initialValue:初始值

    • refContainer.current:存储的值,可随时读取或修改

  • 练习代码如下:

    js 复制代码
    // component/ComUseRef.jsx
    import React, { memo, useEffect, useRef, useState } from "react";
    
    const ComUseRef = memo(() => {
      const [count, setCount] = useState(0);
      const countRef1 = useRef();
      const countRef2 = useRef(0);
      useEffect(() => {
        console.log(countRef1.current);
        console.log(countRef2.current); // 存储上一次的 count 值
      });
      const handleClick = () => {
        setCount(count + 5); // 触发重新渲染
        countRef2.current += 1; // 不触发重新渲染
        console.log("useRef 计数:", countRef2.current);
      };
      return (
        <div>
          <h3>useRef的使用</h3>
          {/* useRef有两个用法:
              1. 获取dom
              2. 保存一个数据,这个数据在整个生命周期中保持不变,用来解决闭包陷阱(参考ComUseCallback代码)
          */}
          <p ref={countRef1}>当前计数:{count}</p>
          <button onClick={handleClick}>+5</button>
        </div>
      );
    });
    
    export default ComUseRef;

useCallback

useCallbackReact 的一个 性能优化 Hook

  • const memoizedCallback = useCallback(() => { // 你的函数逻辑 }, [依赖项])

    • fn:要缓存的回调函数

    • [deps]:依赖数组,只有 依赖变化时,才会重新创建函数 ,如果 deps 为空 [],函数只创建一次,之后不会再变

  • useCallback会返回一个函数的记忆值,在依赖不变,多次定义的时候,返回的值是相同的

  • 适用于需要传递给子组件或避免不必要重新渲染 的函数

  • 通常使用useCallback目的是不希望子组件进行多次渲染,并不是为了函数进行缓存

  • 只有在组件频繁渲染或者子组件因函数变化导致重新渲染时,才需要 useCallback

  • 思考一: 使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化

    js 复制代码
    // 没有useCallback,每次setCount触发更新时,handleClick都会重新创建影响性能,可能导致不必要的子组件重新渲染
    import React, { useState } from "react";
    
    function App() {
      const [count, setCount] = useState(0);
    
      // 每次组件重新渲染,handleClick 都会被重新创建!
      const handleClick = () => {
        setCount(count + 1);
      };
    
      console.log("组件重新渲染,handleClick 重新创建");
    
      return (
        <div>
          <h1>计数:{count}</h1>
          <button onClick={handleClick}>+1</button>
        </div>
      );
    }
    export default App
    
    
    // handleClick只有count变化时才会更新,否则不会重新创建,提高性能
    import React, { useState, useCallback } from "react";
    
    function App() {
      const [count, setCount] = useState(0);
    
      // 只有 count 变化时,handleClick 才会重新创建
      const handleClick = useCallback(() => {
        setCount(count + 1);
      }, [count]);
    
      console.log("组件重新渲染,但 handleClick 只会在 count 变化时重新创建");
    
      return (
        <div>
          <h1>计数:{count}</h1>
          <button onClick={handleClick}>+1</button>
        </div>
      );
    }
    export default App
  • 思考二: 使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化

    js 复制代码
    // 每次App组件渲染时,handleClick都是一个新函数,即使 `Child` 组件没有变化,仍然会重新渲染
    import React, { useState } from "react";
    
    function Child({ onClick }) {
      console.log("Child 组件重新渲染");
      return <button onClick={onClick}>点击</button>;
    }
    
    function App() {
      const [count, setCount] = useState(0);
    
      // handleClick 每次都会重新创建,导致 Child 组件重新渲染!
      const handleClick = () => {
        setCount(count + 1);
      };
    
      return (
        <div>
          <h1>计数:{count}</h1>
          <Child onClick={handleClick} />
        </div>
      );
    }
    export default App
    
    
    
    import React, { useState, useCallback } from "react";
    
    // 1️⃣ 使用 React.memo 让子组件避免不必要的重新渲染
    const Child = React.memo(({ onClick }) => {
      console.log("Child 组件渲染");
      return <button onClick={onClick}>点击</button>;
    });
    
    function App() {
      const [count, setCount] = useState(0);
    
      // 2️⃣ 使用 useCallback,让 onClick 在 count 不变时不会重新创建
      const handleClick = useCallback(() => {
        setCount((prevCount) => prevCount + 1);
      }, []);
    
      return (
        <div>
          <h1>计数:{count}</h1>
          <Child onClick={handleClick} />
        </div>
      );
    }
    
    export default App
  • useCallback 什么时候不需要用?

    • 函数本身不需要缓存(例如简单 onClick 事件)

    • 组件不会频繁重新渲染(比如只有 useState 而无子组件)

    • 滥用 useCallback 可能会让代码更复杂,而没有明显优化

  • 练习代码如下:

    js 复制代码
    // component/ComUseCallback.jsx
    import React, { memo, useCallback, useRef, useState } from "react";
    
    // 当子组件没有被memo包裹时,父组件的useCallback也是无性能优化的
    const ComUseCallbackSon = memo((props) => {
      console.log("ComUseCallbackSon渲染");
      // 当props中的属性发生变化时,子组件就会重新渲染
      const { changeCountCall } = props;
      return (
        <div>
          <button onClick={changeCountCall}>+5</button>
        </div>
      );
    });
    
    const ComUseCallback = memo(() => {
      console.log("ComUseCallback渲染");
      /*
        useCallback 有以下需要明白:
        1. useCallback会返回一个函数的记忆值
        2. 在依赖不改变的情况下,不返回新的函数地址而返回旧的函数地址,多次定义的时候返回的值是一样的
        3. 他是函数缓存的,但是函数被包裹后无法阻止组件render时函数的重新创建
        4. 在往子组件传入了一个函数并且子组件被momo缓存了的时候使用
        5. 使用的目的不是为了函数进行缓存,而是防止子组件多次渲染
        6. 滥用还会增加寻找指定函数和校验依赖是否改变的负担
        7. 下面我们来一一练习
      */
      const [count, setCount] = useState(150);
      const [message, setMessage] = useState(150);
    
      /* 
        没有任何依赖这个就会有闭包陷阱的问题,count的值只会改变一次
        因为当点击按钮时,回调函数中引用的 count 变量是在 useCallback 内部的闭包中。
        因此,每次count变化时,useCallback创建一个新的回调函数,但该回调函数仍然引用最初创建时的count值
        这时我们就想到了添加依赖[count],这是可以的,
        但是当count发生变化时就会产生一个全新的事件引用,就失去了使用useCallback缓存函数的意义
      */
      /* let changeCountCall = useCallback(()=> {
        setCount(count + 5)
      },[]) */
    
      // 闭包陷阱解决方案,这时传给子组件的就是一个函数,就算点击函数变化时子组件也不会更新
      const countRef = useRef();
      countRef.current = count;
      let changeCountCall = useCallback(() => {
        console.log("changeCountCall");
        setCount(countRef.current + 5);
    
        // 不使用useRef,也可以使用函数式更新setCount来确保使用最新的prevCount值进行递增
        // react新文档做了解释 https://react.dev/learn/queueing-a-series-of-state-updates
        setCount((prevCount) => prevCount + 5);
      }, []);
    
      /*
        只要render更新组件重新渲染,则下面函数每次都会重新构建
        如果吧这个函数传给子组件,那么当每次执行这个函数时,
        就算子组件没有依赖count值时子组件都会渲染,因为子组件的props改变了
      */
      function changeCount(num) {
        setCount(count - num);
      }
      return (
        <div>
          <h3>useCallback的使用</h3>
          <p>当前计数:{count}</p>
          <p>当前message:{message}</p>
          <button onClick={changeCountCall}>+5</button>
          <button onClick={(e) => changeCount(14)}>-14</button>
          <button onClick={(e) => setMessage(Math.random())}>修改message</button>
          <ComUseCallbackSon changeCountCall={changeCountCall} />
        </div>
      );
    });
    
    export default ComUseCallback

useMemo

useMemoReact 的一个性能优化 Hook,用于缓存计算结果,避免不必要的重复计算

  • const memoizedValue = useMemo(() => { return 计算逻辑 }, [依赖项])

    • fn:要执行的计算逻辑

    • [deps]:依赖数组,只有依赖变化时,才会重新计算

    • 返回值:缓存的计算结果,在依赖不变的情况下,多次定义的时候,返回的值是相同的

  • 适用于计算开销大的逻辑(如排序、筛选、计算总和),也可以优化 React.memo 组件的 props 计算,避免组件每次渲染都重新计算

  • useMemo 什么时候不需要用?

    • 简单计算 (如 a + b 这种计算)

    • 没有性能问题的情况下(避免不必要的优化,代码更复杂)

  • useCallback对比如下:

  • 练习代码如下:

    js 复制代码
    // component/ComUseMemo.jsx
    import React, { memo, useMemo, useState } from 'react'
    
    const ComUseMemoSon = memo((props)=> {
      console.log('ComUseMemoSon渲染');
      return <div>
        ComUseMemoSon-userName: {props.user.name}-userPhone: {props.user.phone}
      </div>
    })
    const ComUseMemo = memo(() => {
      const [count, setCount] = useState(0)
      /* 
        useCallback(fn, [])相当于useMemo(() => fn, []) 
        useCallback: 返回的值是回调函数
        useMemo: 返回的值是回调函数中return的值
      */
    
      /* 
        和useCallback包裹一样,将changeCountMemo传给子组件,
        改变时子组件也不会渲染,若将changeCount传过去就会渲染 
      */
      const changeCountMemo = useMemo(()=> {return changeCount}, [])
      function changeCount() {
        setCount((pre)=> pre + 5)
      }
      const totalCount = useMemo(()=> {return getTotalCount(50)}, [])
      function getTotalCount(count) {
        let total = 0
        for(let i = 0; i <= count; i++) {
          total = total + i
        }
        return total
      }
      const user = useMemo(()=> ({name: 'han', phone: '1234567890'}),[])
      return (
        <div>
          <h3>useMemo的使用</h3>
          <p>当前计数:{count}</p>
          <button onClick={changeCountMemo}>+5</button>
          <p>当前count总数: {totalCount}</p>
          <ComUseMemoSon changeCountMemo={changeCountMemo} user={user} />
        </div>
      )
    })
    
    export default ComUseMemo

useImperativeHandle

useImperativeHandleReact 提供的一个 Hook,用于自定义 ref 的暴露方法,通常和 forwardRef 搭配使用

  • useImperativeHandle(ref, () => ({ 方法1, 方法2, ... }), [依赖项])

    • ref :传入的 ref,由 forwardRef 提供

    • 返回值是一个对象 ,定义了该 ref 可以暴露的方法

    • 只有 依赖项 [deps] 发生变化时,才会重新创建方法

  • refforwardRef结合使用 :通过forwardRef可以将ref转发到子组件,子组件拿到父组件中创建的ref,绑定到自己的某一个元素中

  • 以前的refforwardRef的做法本身没有什么问题,但是将子组件的DOM直接暴露给了父组件

  • useImperativeHandle主要用于限制父组件访问子组件的功能,而不是直接暴露整个 ref

  • 练习代码如下:

    js 复制代码
    // component/ComUseImperativeHandle.jsx
    import React, { forwardRef, memo, useImperativeHandle, useRef } from 'react'
    
    const ComUseImperativeHandleSon = memo(forwardRef((props,ref)=> {
      const inputSonRef = useRef()
      useImperativeHandle(ref, ()=> {
        // 要子组件再重新定义ref然后return给父组件的ref,只要return出去的方法父组件才可以使用
        return {
          focus() {
            inputSonRef.current.focus()
          },
          // 修改子组件输入值的方法
          setValue(value) {
            inputSonRef.current.value = value
          },
          // 获取子组件输入值的方法
          getValue() {
            return inputSonRef.current.value
          }
        }
      })
      return <input ref={inputSonRef} type="text" />
    }))
    const ComUseImperativeHandle = memo(() => {
      /* 
        useImperativeHandle通常结合在子组件被forwardRef包裹,
        父组件可以操作子组件ref时使用,限制父组件可以使用的范围
      */
      const inputRef = useRef()
      function inputFocus() {
        console.log(inputRef.current.getValue());
        inputRef.current.focus()
        inputRef.current.setValue(Math.random())
      }
      return (
        <div>
          <h3>useImperativeHandle的使用</h3>
          <ComUseImperativeHandleSon ref={inputRef} />
          <button onClick={e=>inputFocus()}>操作ComUseImperativeHandleSon</button>
        </div>
      )
    })
    
    export default ComUseImperativeHandle

自定义Hook

自定义Hook本质上只是一种函数代码逻辑的抽取,严格意义上来说,它本身并不算React的特性`

  • 导出练习的hooks :创建hooks/index.js

    js 复制代码
    import useLogLift from "./useLogLift";
    import usePublicContext from "./usePublicContext";
    import usePageScroll from "./usePageScroll";
    import useLocalStorage from "./useLocalStorage";
    
    export { useLogLift, usePublicContext, usePageScroll, useLocalStorage };
  • 练习一:所有的组件在创建和销毁时都进行打印

    js 复制代码
    // hooks/useLogLift.js
    import { useEffect, useLayoutEffect } from "react";
    
    function useLogLift(name) {
      useLayoutEffect(() => {
        console.log(name, "组件mount之前");
      }, [name]);
      useEffect(() => {
        console.log(name, "组件mount完成");
        return () => {
          console.log(name, "组件unMount之前");
        };
      }, [name]);
    }
    
    export default useLogLift;
    
    
    // component/MyUseLogLift.jsx
    import React, { memo, useState } from 'react'
    import { useLogLift } from '../hooks'
    
    const MyUseLogLiftSon1 = memo(() => {
      useLogLift('MyUseLogLiftSon1')
      return (
        <div>MyUseLogLiftSon1</div>
      )
    })
    const MyUseLogLiftSon2 = memo(() => {
      // useLayoutEffect(()=> {
      //   console.log('组件mount之前'); 
      //  },[])
      //  useEffect(()=> {
      //    console.log('组件mount完成');
      //    return ()=> {
      //      console.log('组件unMount之前');
      //    }
      //  },[])
    
      useLogLift('MyUseLogLiftSon2')
      return (
        <div>MyUseLogLiftSon2</div>
      )
    })
    const MyUseLogLift = memo(() => {
      const [isShow, setIsShow] = useState(true)
      function changeIsShow() {
        setIsShow(!isShow)
      }
      // useLayoutEffect(()=> {
      //  console.log('组件mount之前'); 
      // },[])
      // useEffect(()=> {
      //   console.log('组件mount完成');
      //   return ()=> {
      //     console.log('组件unMount之前');
      //   }
      // },[])
    
      useLogLift('MyUseLogLift')
      return (
        <>
          <h4>打印生命周期</h4>
          <MyUseLogLiftSon1 />
          {isShow && <MyUseLogLiftSon2 />}
          <button onClick={e=>changeIsShow()}>切换卸载组件</button>
        </>
      )
    })
    
    export default MyUseLogLift
  • 练习二:Context的共享

    js 复制代码
    // hooks/usePublicContext.js
    import { useContext } from "react";
    import { UserContext, ThemeContext } from "../context";
    function usePublicContext() {
      const user = useContext(UserContext);
      const theme = useContext(ThemeContext);
      return [user, theme];
    }
    
    export default usePublicContext;
    
    
    
    // component/MyPublicContext.jsx
    import React, { memo } from 'react'
    import { usePublicContext } from '../hooks'
    
    const MyPublicContext = memo(() => {
      const [user, theme] =  usePublicContext()
      return (
        <div>
          MyPublicContext
          <p>{user.name}</p>
          <p>{theme.color}</p>
          <p>{theme.fontSize}</p>
        </div>
      )
    })
    
    export default MyPublicContext
  • 练习三:获取滚动位置

    js 复制代码
    // hooks/usePageScroll.js
    import { useEffect, useState } from "react";
    
    function usePageScroll() {
      let [scrollX, setScrollX] = useState(0);
      let [scrollY, setScrollY] = useState(0);
      useEffect(() => {
        function handleScroll() {
          setScrollX(window.scrollX);
          setScrollY(window.scrollY);
        }
        window.addEventListener("scroll", handleScroll);
        return () => {
          window.removeEventListener("scroll", handleScroll);
        };
      }, []);
      return [scrollX, scrollY];
    }
    
    export default usePageScroll;
    
    
    
    // component/MyPageScroll.jsx
    import React, { memo } from 'react'
    import { usePageScroll } from '../hooks'
    
    const MyPageScroll = memo(() => {
      // useEffect(()=> {
      //   function handleScroll() {
      //     console.log('handleScroll', window.scrollX, window.scrollY);
      //   }
      //   window.addEventListener('scroll', handleScroll)
      //   return ()=>{
      //     window.removeEventListener('scroll', handleScroll)
      //   }
      // })
      const [scrollX,scrollY] = usePageScroll()
      return (
        <div>MyPageScroll--scrollX:{scrollX}--scrollY:{scrollY}</div>
      )
    })
    
    export default MyPageScroll
  • 练习四:localStorage数据存储

    js 复制代码
    // hooks/useLocalStorage.js
    /* eslint-disable react-hooks/exhaustive-deps */
    import { useEffect, useState } from "react";
    
    function useLocalStorage(key) {
      // 1.从localStorage中获取数据, useState可以传入一个回调函数
      const [storage, setStorage] = useState(() => {
        return !localStorage.getItem(key)
          ? ""
          : JSON.parse(localStorage.getItem(key));
      });
      // 2.监听data改变, 一旦发生改变就存储data最新值
      useEffect(() => {
        localStorage.setItem(key, JSON.stringify(storage));
      }, [storage]);
      // 3.将data/setData的操作返回给组件, 让组件可以使用和修改值
      return [storage, setStorage];
    }
    
    export default useLocalStorage;
    
    
    
    // component/MyUseLocalStorage.jsx
    import React, { memo } from "react";
    import useLocalStorage from "../hooks/useLocalStorage";
    
    const MyUseLocalStorage = memo(() => {
      const [storage, setStorage] = useLocalStorage("token");
      return (
        <div>
          MyUseLocalStorage-{storage}
          <button onClick={(e) => setStorage(Math.random())}>修改storage值</button>
        </div>
      );
    });
    
    export default MyUseLocalStorage;

redux hooks

Redux 提供了一些 React Hooks 让我们在函数组件中更方便地使用 Redux 状态管理,最常用的主要有以下几个:

  • useStore 用得比较少,通常 useSelector 就足够了,它可以用来 直接访问 Redux store (而不是 state),适用于 需要手动访问整个 store 的情况,一般调试时用

    js 复制代码
    import { useStore } from "react-redux";
    
    function Debugger() {
      const store = useStore();
    
      console.log("Redux Store:", store.getState()); // 获取完整 state
    
      return <div>打开控制台查看 Redux store</div>;
    }
    
    export default Debugger;
  • useSelector 用于 从 Redux store 中获取 state ,类似于 mapStateToProps(在 connect 中)

    • 参数一 :将state映射到需要的数据中,会订阅 store,当 state 发生变化时,组件会重新渲染

    • 参数二 :可以进行比较来决定是否组件重新渲染,const refEquality = (a, b) => a === b必须返回两个完全相等的对象才可以不引起重新渲染

  • useDispatch 用于分发 action 来更新 Redux state,是直接获取dispatch函数 ,之后在组件中直接使用即可,相当于 mapDispatchToProps

  • 练习代码如下:

    js 复制代码
    // store/modules/message.js
    import { createSlice } from "@reduxjs/toolkit";
    
    const messageSlice = createSlice({
      name: "message",
      initialState: {
        message: "",
      },
      reducers: {
        changeMessageAction(state, { payload }) {
          state.message = payload;
        },
      },
    });
    
    export const { changeMessageAction } = messageSlice.actions;
    export default messageSlice.reducer;
    
    
    
    // store/modules/token.js
    import { createSlice } from "@reduxjs/toolkit";
    
    const tokenSlice = createSlice({
      name: "token",
      initialState: {
        token: "937539r-395639",
      },
      reducers: {
        changeTokenAction(state, { payload }) {
          state.token = payload;
        },
      },
    });
    
    export const { changeTokenAction } = tokenSlice.actions;
    export default tokenSlice.reducer;
    
    
    
    // component/ComReduxHooks.jsx
    import React, { memo } from 'react'
    import { shallowEqual, useDispatch, useSelector, /* useStore */ } from 'react-redux'
    import { changeTokenAction } from '../store/modules/token'
    import { changeMessageAction } from '../store/modules/message';
    
    const ComReduxHooksSon = memo(()=>{
      console.log('ComReduxHooksSon渲染');
      /* 
        当不写shallowEqual时,只要是store里面的值改变,不管是不是message改变都会重新渲染,
        比如父组件的token改变时也会重新渲染
        当写shallowEqual时,则只会在message改变时子组件再渲染
      */
      const {message} = useSelector((state)=> ({
        message: state.message.message
      }),shallowEqual)
      const dispatch = useDispatch()
      function setMessage() {
        dispatch(changeMessageAction('你好呀'))
      }
      return (
        <div>
          <p>ComReduxHooksSon-message{message}</p> 
          <button onClick={e=>setMessage()}>修改message</button>
        </div>
      )
    })
    const ComReduxHooks = memo(() => {
      console.log('ComReduxHooks渲染');
      /* 
        参数一:将state映射到需要的数据中;
        参数二:可以进行比较来决定是否组件重新渲染
        默认会比较我们返回的两个对象是否相等,也就是我们必须返回两个完全相等的对象才可以不引起重新渲染;
      */
    
      /*
        当不写shallowEqual时,只要是store里面的值改变不管是不是token改变都会重新渲染,
        比如子组件的message改变时也会重新渲染
        当写shallowEqual时,则只会在token改变时组件再渲染
      */
      const { token } = useSelector((state)=> ({
        token: state.token.token
      }), shallowEqual)
      // const store = useStore() // 可以拿到整个store对象
      const dispath = useDispatch()
      // console.info(store, token, dispath);
      function setToken() {
        dispath(changeTokenAction(Math.random()+100))
      }
      return (
        <div>
          <h3>ComReduxHooks</h3>
          <p>{token}</p>
          <button onClick={e=>setToken()}>设置token</button>
          <ComReduxHooksSon />
        </div>
      )
    })
    
    export default ComReduxHooks

react18 hooks

React 18 引入了一些新的Hooks,同时优化了一些现有的 Hooks,使得并发渲染和性能优化更加容易

useTransition

官方解释:返回一个状态值,表示过渡任务的等待状态,以及一个启动该过渡任务的函数,简单解释如下:

  • 用于 状态更新的优先级管理 ,将部分更新标记为低优先级,避免 UI 卡顿

  • 适用于 搜索过滤、大数据列表渲染、动画 等场景

  • 练习代码如下:

    js 复制代码
    // component/ComUseTransition.jsx
    import React, { memo, useState, useTransition } from 'react'
    import array from '../array.js'
    const ComUseTransition = memo(() => {
      const [names, setNames] = useState(array)
      /* 
        这一段代码若把性能的cpu降一下就会发现,删除input的值时会删的很慢出现卡顿
        这时我们想要的结果是,input的值的增删快速的反应给用户,列表的过滤可以稍后完成 
      */
      /* function changeInputValue(e) {
        setNames(array.filter((f)=> {return f.includes(e.target.value)}))
      } */
    
      /* 
        useTransition告诉react对于某部分任务的更新优先级较低,可以稍后进行更新
        返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数 
      */
      const [pending, startTransition] = useTransition()
      function changeInputValue(e) {
        // 会在其他改变之后
        startTransition(()=> {
          setNames(array.filter((f)=> {return f.includes(e.target.value)}))
        })
      }
      return (
        <div>
          <h3>useTransition的使用</h3>
          <input type="text" placeholder='输入值过滤数组' onChange={changeInputValue} />
          {pending && <h4>数据加载中...</h4>}
          {!names.length && <h4>暂无符合数据</h4>}
          <ul>
            { 
              names.map((item,index)=> {
                return <li key={index}>{item}</li>
              })
            }
          </ul>
        </div>
      )
    })
    
    export default ComUseTransition

useDeferredValue

官方解释useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后

  • useDeferredValue 用于延迟某个值的更新,而不影响其他状态更新

  • 适用于 搜索框联动渲染、复杂计算等

  • 练习代码如下:

    js 复制代码
    // component/ComUseDeferredValue.jsx
    import React, {  memo, useDeferredValue, useState } from 'react'
    import array from '../array'
    
    const ComUseDeferredValue = memo(() => {
      const [names, setNames] = useState(array)
      /* function changeInputValue(e) {
        setNames(array.filter((f)=> f.includes(e.target.value)))
      } */
    
      /* 
        useDeferredValue的使用和useTransition同理都是更新延迟
        单useDeferredValue不会有pending状态
        参数: value:你想延迟的值,可以是任何类型。
      */
      const list = useDeferredValue(names)
      function changeInputValue(e) {
        setNames(array.filter((f)=> f.includes(e.target.value)))
      }
      return (
        <div>
          <h3>useDeferredValue的使用</h3>
          <input type="text" placeholder='输入值过滤数组' onChange={changeInputValue} />
          {!list.length && <h4>暂无符合数据</h4>}
          <ul>
            { 
              list.map((item,index)=> {
                return <li key={index}>{item}</li>
              })
            }
          </ul>
        </div>
      )
    })
    
    export default ComUseDeferredValue
    
    
    // src/array.js
    import { faker } from "@faker-js/faker";
    
    let list = [];
    for (let i = 0; i < 10000; i++) {
      list.push(faker.internet.userName());
    }
    
    export default list;

useId

官方的解释useId 是一个用可以保证应用程序在客户端和服务器端生成唯一的ID ,同时避免 hydration 不匹配的 hook,这里需要先理解一些服务器端渲染(SSR)的概念和hydration

  • useId是用于react的同构应用开发的,前端的SPA页面并不需要使用它

  • 练习代码如下:

    js 复制代码
    // component/ComUseId.jsx
    import React, { memo, useId } from 'react'
    
    const ComUseId = memo(() => {
      // 服务端渲染时使用userId,要了解ssr和同构应用和Hydration
      // useId会生成唯一的ID,让其在服务端渲染时和客户端渲染时的id一致
      const id = useId()
      return (
        <div>
          <h3 id={id}>ComUseId的使用-id-{id}</h3>
        </div>
      )
    })
    
    export default ComUseId

SSR同构应用

  • SSRServer-Side Rendering,服务端渲染)React 应用的一种渲染模式,指的是在服务器端预渲染页面的 HTML,再发送给客户端,以提高首屏加载速度和 SEO 友好性。

  • 同构(Isomorphic/Universal 应用指的是同一份 React 代码可以同时运行在服务器端(Node.js)和客户端(浏览器),提供流畅的用户体验

  • 同构是一种SSR的形态,是现代SSR的一种表现形式,核心特点如下:

    • 服务器端 :使用 ReactDOMServer.renderToString() 在 Node.js 中预渲染 HTML

    • 客户端 :使用 ReactDOM.hydrate() 在浏览器端接管 React

  • 基本流程如下:

    • 浏览器请求 Node.js 服务器

    • 服务器端渲染 HTML 并返回(首屏可见)

    • 客户端加载 React 组件 ,使用 hydrate() 让组件变得可交互

    • 之后的路由更新由 客户端接管

Hydration

什么是Hydration?这里引入vite-plugin-ssr插件的官方解释:

  • Hydration(水合)React SSR(服务端渲染)中的一个关键过程 ,它指的是:

    • 服务器端 生成 HTML 并发送到客户端

    • 客户端 加载 React 组件,并在已有的 HTML 结构上重新绑定事件和状态,让页面变得可交互

  • 简单理解如下:

    • SSR 负责 首次渲染 HTML (提高首屏速度、SEO 友好)。

    • Hydration静态 HTML 变为动态可交互React 组件接管)

相关推荐
我自纵横202323 分钟前
第一章:欢迎来到 HTML 星球!
前端·html
发财哥fdy24 分钟前
3.12-2 html
前端·html
ziyu_jia31 分钟前
React 组件测试【React Testing Library】
前端·react.js·前端框架
祈澈菇凉31 分钟前
如何在 React 中实现错误边界?
前端·react.js·前端框架
撸码到无法自拔32 分钟前
❤React-组件的新旧生命周期
前端·javascript·react.js·前端框架·ecmascript
凉生阿新35 分钟前
【React】React + Tailwind CSS 快速入门指南
css·react.js·arcgis
betterangela35 分钟前
react基础语法视图层&类组件
前端·javascript·vue.js
小段hy42 分钟前
在小程序中/uni-app中,当没有登录时,点击结算按钮,3s后自动跳转到登录页面
前端·小程序·uni-app
CSDN专家-微编程43 分钟前
UNIAPP圈子社区纯前端万能源码模板 H5小程序APP多端兼容 酷炫UI
前端·小程序·uni-app
冴羽1 小时前
SvelteKit 最新中文文档教程(2)—— 路由
前端·javascript·svelte