React Hooks:重塑你的前端开发体验,提升生产力的终极指南

1、useState

通过传入 useState 参数后返回一个带有默认状态和改变状态函数的数组。通过传入新状态给函数来改变原本的状态值。值得注意的是 useState 不帮助你处理状态,相较于 setState 非覆盖式更新状态,useState 覆盖式更新状态,需要开发者自己处理逻辑。

useState主要用于给组件添加状态变量。注意,我们只能在组件的顶层或自定义的 Hooks 中调用。

基础使用
js 复制代码
const [age, setAge] = useState(42);
对象与数组的更新
js 复制代码
//对象 setForm({ ...form, name: e.target.value // 更新这个属性 }); 
//数组 setTodos([ ...todos, { id: nextId++, title: title, done: false } ]);

函数的更新
js 复制代码
//这是一个错误的做法 
//这样的用法是把函数的返回值存储或更新到状态中,并不是把函数存储到状态中。
const [fn, setFn] = useState(someFunction); 
function handleClick() { setFn(someOtherFunction); }
//想在状态中存储一个函数,你需要使用一个箭头函数来"包裹"它。这是正确的做法: 
const [fn, setFn] = useState(() => someFunction); 
function handleClick() { setFn(() => someOtherFunction)};

2、useEffect

有两个主要参数:第一个参数是一个函数,这个函数就是我们执行副作用操作的地方;第二个参数是一个依赖数组(dependency array),它告诉 React 什么时候应该执行这个副作用操作。

在类组件中,我们通常会使用 componentDidMount,componentDidUpdate 和 componentWillUnmount 生命周期方法来执行副作用操作,但是在函数组件中,我们使用 useEffect 来达到类似的效果。

在哪些场景中可以使用 useEffect 呢?

  1. 数据获取:如果你的组件需要从服务器获取数据,那么你可以使用 useEffect 来发起数据获取请求,并在请求完成后更新组件的状态。
  2. 订阅事件:如果你的组件需要监听一些事件(比如窗口的滚动事件、键盘事件等),那么你可以使用 useEffect 来订阅这些事件,并在事件触发时执行相应的逻辑。
  3. 副作用操作:如果你的组件需要在渲染时执行一些副作用操作(比如修改全局状态、设置定时器等),那么你可以使用 useEffect 来执行这些操作。
  4. 性能优化:在一些复杂的组件中,你可能需要根据组件的状态来调整渲染的逻辑。在这种情况下,你可以使用 useEffect 来根据状态的变化来执行不同的逻辑。
  5. 懒加载:如果你需要实现懒加载的功能(比如图片的延迟加载、文章的逐步展示等),那么你可以使用 useEffect 来监听相关的变化,并在变化发生时执行相应的逻辑。

基础使用
js 复制代码
import React, { useState, useEffect } from 'react';
function Example() { 
    const [count, setCount] = useState(0); 
    useEffect(() =>{
        document.title = `You clicked ${count} times`;
     },[count]);
    // 只有当 count 发生变化时,才会执行 useEffect 中的函数
    return ( 
        <div>
            <p>You clicked {count} times.</p> 
            <button onClick={() => setCount(count + 1)}>
                Click me </button>
        </div> 
    );
 }

❤️在这个例子中,我们使用 useEffect 来更新文档的标题。每当点击按钮时,count 状态就会更新,从而触发 useEffect 中的函数,更新文档的标题。这就是 useEffect 的基本用法。


清除函数的作用
js 复制代码
useEffect(() => {
   const timer = setTimeout(() => {
     console.log('这个消息将在 5 秒后显示'); }, 5000);
      // 返回一个清理函数,用于清除定时器 
      return () => { clearTimeout(timer); }; 
}, []);
// 注意这里传入空的依赖数组,表示这个 useEffect 只会在组件首次渲染和卸载时运行

❤️在这个例子中通过返回一个函数,可以在useEffect钩子中注册一个清理函数,当组件卸载时,清理函数会自动被调用。这样,就可以确保在组件卸载时,任何需要被清理的资源或状态都会被正确地清除。


不同的依赖数组的区别
js 复制代码
//1、如果第二个参数不写,那么它将在每次渲染后执行。
useEffect(() => { 
    console.log('这个函数将在每次渲染后执行'); 
});
//2、第二个参数是空数组,那么它只会在组件首次渲染时执行一次。 
useEffect(() => {
    console.log('这个函数只会在组件首次渲染时执行一次'); 
}, []); 
//3、依赖数组中的值发生变化时执行。 
const [count, setCount] = useState(0);
useEffect(() => {
    console.log('这个函数将在 count 发生变化时执行');
}, [count]); // 这里的依赖数组包含 count 变量

3、useLayoutEffect(同步执行副作用)

赋值给 useEffect 的函数会在组件渲染到屏幕之后执行 useLayoutEffect ,它会在所有的 DOM 变更之后同步调用。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新

基础使用
js 复制代码
import React, { useState, useEffect, useLayoutEffect } from "react"; 
function App() {
    const [width, setWidth] = useState(0); 
    useEffect(() => {
        console.log("useEffect"); 
        console.log(width)
    }); 
    useLayoutEffect(() => {
        const title = document.querySelector("#title");
        const titleWidth = title.getBoundingClientRect().width; 
        console.log("useLayoutEffect");
        if (width !== titleWidth) { 
            setWidth(titleWidth); 
        }
    }); 
    return (
        <div> 
            <h1 id="title">hello</h1>
                <h2>{width}</h2>
            </div> 
    );
 }
 export default App

❤️在以上例子中的总结如下:

  1. 首次渲染,Dom构建完成后执行useLayoutEffect,判断宽度不相同,去派发;
  2. 更改状态,在组件首次渲染完成后执行useEffect,因为width更改,又会render;
  3. 再次在dom完成后执行useLayoutEffect,发现相等,不在派发,随后渲染完成后执行useEffect。

useEffect和useLayoutEffect的区别
  1. 执行时机:useEffect是异步执行的,其执行时机是在浏览器完成渲染之后(DOM更新后执行的)。而useLayoutEffect是同步执行的,其执行时机是在浏览器把内容真正渲染到界面之前(浏览器绘制之前执行),和componentDidMount等价。
  2. 同步/异步:useEffect是异步的,这意味着它会在当前的浏览器帧结束之后执行。而useLayoutEffect是同步的,它会在浏览器开始渲染之前执行,因此可以用来同步获取布局(ref)信息。

useEffect和useLayoutEffect在react中是怎么执行的?
  1. react 在 diff 后,会进入到 commit 阶段,准备把虚拟 DOM 发生的变化映射到真实 DOM 上,在 commit 阶段的前期,会把使用了 useEffect 组件产生的生命周期函数入列到 React 自己维护的调度队列中,给予一个普通的优先级,让这些生命周期函数异步执行;
  2. 随后内存中的真实 DOM 已经变化,浏览器也没有立刻渲染到屏幕上。此时会进行收尾工作,同步执行对应的生命周期方法,我们说的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函数都是在这个阶段被同步执行;
  3. 浏览器把发生变化的 DOM 渲染到屏幕上,到此为止 react 仅用一次回流、重绘的代价,就把所有需要更新的 DOM 节点全部更新完成;
  4. 浏览器渲染完成后,浏览器通知 react 自己处于空闲阶段,react 开始执行自己调度队列中的任务,此时才开始执行 useEffect(create, deps) 的产生的函数。
为什么建议将修改DOM的操作放在 useLayoutEffect 中,而不是 useEffect ?

❤️当DOM被修改时,浏览器的线程处于被阻塞阶段,此时还没有发生回流、重绘。由于内存中的DOM已经被修改,通过useLayoutEffect可以拿到最新的DOM节点,并且在此时对DOM进行样式上的修改。假设修改了元素的 height,这些修改会一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。相反,如果将修改DOM操作放在useEffect中,会导致浏览器的重绘和回流。因此,如果涉及修改DOM的操作,建议将其放入useLayoutEffect中,以避免不必要的重绘和回流。

4、useCallback

useCallback 用于缓存需要在组件中多次调用的回调函数,接受两个参数:回调函数和一个依赖项数组。只有当依赖项发生变化时,才会重新创建这个回调函数。这有助于减少不必要的重新渲染,尤其是在子组件中使用回调函数时。

useCallback执行过程
  1. 组件渲染时:React 将会执行函数组件的整个函数体, useCallback 也会调用。
  2. 缓存函数引用: 当 useCallback 被调用时,React 将会缓存传入的函数,并且返回缓存函数。不会在每次渲染时重新创建。
  3. 依赖项检测:接受一个依赖项数组。React 会监视这个依赖项数组,并且在数组中的任何一个值发生变化时,重新创建函数。如果依赖项数组中的值保持不变,React 将会跳过函数的重新创建。
  4. 传递给子组件:缓存的函数可以作为 props 传递给子组件 ,父组件重新渲染,子组件也不会重新渲染。

基础使用
js 复制代码
const memoizedCallback = useCallback( 
    () => { 
        // 函数体 
     }, 
    [dependency1, dependency2, ...] // 依赖数组
 );
useCallback配合React.memo使用
js 复制代码
import react, {memo, useCallback, useState} from "react"; 
function ParentComponent(){ 
    const [value,setValue] = useState(0); 
    const changeChildren = useCallback(()=>{ 
        console.log("传入子组件") },[])
        return ( 
            <div>
                <span> Value:{value}
                    <button onClick={()=>{ setValue(value+1) }}>
                        click Btn
                     </button>
               </span>
               <ChildrenComponent fun={changeChildren}/>
            </div> 
         ) 
} 

const ChildrenComponent = memo(()=>{ 
    console.log("children"); 
    return <div>children</div> 
}) 
export default ParentComponent;

❤️在以上例子中的总结如下:

  1. React.memo检测的是props中数据的栈地址是否改变。而父组件重新构建的时候,会重新构建父组件中的所有函数,新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。
  2. 使用useCallBack后父组件重新渲染,子组件中的函数就会因为被useCallBack保护而返回旧的函数地址,子组件就不会检测成地址变化,也就不会重选渲染。

注意事项
  1. 避免滥用: 尽量避免过度使用 useCallback。如果所有函数都是用 useCallback,React内部将所有函数放在一个缓存数组中,如果依赖项中任一参数发生变化都会重新创建此函数;
  2. 性能问题:缓存大量函数,这些函数占用了大量内存或者使用了昂贵的计算,可能会对性能造成负面影响,还可能会降低代码的可读性和可维护性;
  3. 简单组件:对于没有经过 React.memo 优化的子组件或者那些不会因为 prop 变化而重新渲染的组件,使用useCallback 是不必要的。
  4. 不涉及其它 Hooks 的函数:如果一个函数并不被用作其他 Hooks 的依赖,并且也不被传递给任何子组件,那么没有理由使用useCallback。
  5. 不要将非必要参数传入到依赖项中:非必要的参数更新将会重新创建函数。

5、useMemo

核心目的是通过缓存计算结果,避免在组件渲染时进行不必要的重复计算,从而优化性能。这意味着只有当其依赖项发生变化时 useMemo 才会重新计算这个值,否则它将重用之前的结果。

基础使用
js 复制代码
const memoizedValue = useMemo(() => expensiveCalculation(), [/* 依赖项 */]);

❤️在以上例子中的总结如下:

  1. expensiveCalculation 是一个昂贵的计算函数;
  2. 使用 useMemo 缓存了它的计算结果,并且只有当依赖项数组中的值发生变化时才会重新计算。

useMemo的执行过程
js 复制代码
import react, {useEffect, useMemo, useState} from "react"; 
import axios from "axios";

export default function Cinema() { 
    const [list, setList] = useState([]);
    const [loading, setLoading] = useState(false);
    const [inputVal, setInputVal] = useState('');
    const handleGetList = () => {
    setLoading(true); axios({
        url: "https://m.maizuo.com/gatewaycityId=440300&pageNum=1&pageSize=15&type=1&k=8857390",
        method: 'get', 
        headers: { "X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"17110951061639191448387585"}', "X-Host": "mall.film-ticket.film.list" } 
    })
    .then(res => { 
        if (res && res.data.status === 0) { 
            setList(res.data.data.films) 
        } else { 
            setList([]) 
        } setLoading(false) }) 
    } 
    const getList = useMemo(() => {
        return list.filter(item => item.name.includes(inputVal)); 
    }, [list, inputVal]); 
    useEffect(() => { 
        handleGetList();
    }, [])
    return ( 
        <div>
            <input value={inputVal} onChange={(e) => setInputVal(e.target.value)}/> 
            <ul> {!loading && getList.map(item => { 
                return <li key={item.filmId}>{item.name}</li> })
            } </ul> {loading && <span>Loading.........</span>} </div> 
    )
}

❤️在以上例子中的总结如下:

  1. 初始化阶段:useMemo 的回调函数在组件渲染时被执行,计算初始的 memoized 值。
  2. 依赖项数组变化时:当 list 或 inputVal 发生变化时,useMemo 的回调函数会被重新执行,计算新的 memoized 值。
  3. 依赖项数组不变时:如果 list 和 inputVal 的值保持不变,useMemo 会返回之前存储的 memoized 值,避免重复计算。

useMemo和useCallback的区别及使用场景

* * *

注意事项
  1. 不要滥用:只在有昂贵的计算或大型引用类型传递给子组件时考虑使用,过度使用可能导致复杂性和性能问题;
  2. 正确选择依赖项:确保依赖数组包含所有在计算函数中使用的变量,否则可能导致缓存的值不会在依赖变化时重新计算;
  3. 深层比较:注意引用类型的依赖项是否需要深层比较,以确保正确触发重新计算;
  4. 避免内存占用: 小心缓存大型对象或数组,以避免不必要的内存占用。在某些情况下,直接传递引用类型的变化可能更合适;
  5. 返回值:useMemo 返回计算的值,而不是函数。如果需要缓存函数,应该使用 useCallback;
  6. 性能监控工具:使用性能监控工具(如 React DevTools)检查 useMemo 缓存的值是否按预期工作,有助于排除性能问题和调试;
  7. 组件层次结构:useMemo 只在当前组件内有效,如果需要在多个组件之间共享缓存值,可能需要提升状态或使用上下文(Context)。

6、useRef

主要用于在函数组件中存储持久性的值,并且不会触发组件的重新渲染。

基础使用
js 复制代码
const ref = useRef(initialValue);

❤️注意:

  1. 返回一个可变的 ref 对象,该对象只有个 .current 属性,初始值为传入的参数( initialValue );
  2. 返回的 ref 对象在组件的整个生命周期内保持不变;
  3. 不要在渲染期间写入或者读取 ref.current;
  4. 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方。

使用场景
  1. 获取 DOM 元素:
js 复制代码
function TextInput() { 
    const inputRef = useRef(null);
    
    function focusInput() {
        inputRef.current.focus(); 
    } 
    return ( 
        <div> 
            <input ref={inputRef} type="text" /> 
                <button onClick={focusInput}>Focus the input</button>
         </div> 
    );
 }
  1. 存储任意可变值(不能使用在页面上)
js 复制代码
import React, { useRef, useState } from 'react';
function MyComponent() { 
    const counterRef = useRef(0); 
    // 存储计数器值 
    const increment = () => {
        counterRef.current += 1;// 修改计数器值,不会触发重新渲染 
        console.log('Counter:', counterRef.current); 
     };
     return ( 
         <div>
             <button onClick={increment}>Increment</button> 
         </div> 
     );
 }
  1. 在 effect 中保存变量的前一个值(保存props 或 state 的上一次值,可以使用useRef结合useEffect)
js 复制代码
import React, { useRef, useEffect } from 'react'; 
function MyComponent({ value }) { 
    const prevValueRef = useRef(); // 保存前一个值的引用
    useEffect(() => { 
        prevValueRef.current = value; // 在每次渲染结束后更新 prevValueRef 的值 
    }); 
    
    const prevValue = prevValueRef.current; // 获取前一个值
    
    return (
        <div> 
            <p>Current value: {value}</p> 
            <p>Previous value: {prevValue}</p> 
        </div>
    );
}
注意事项

❤️避免重复创建Ref(用null作为初始值,渲染的过程判断仅在null时去计算或调用有副作用的方法)

js 复制代码
function ClickCounter() { 
    // good
    const countRef = useRef(null); 
    // good 
    if (countRef.current === null) { 
        countRef.current = getInitialCount(); 
    } 
    function handleClick() {
        countRef.current += 1; 
        console.log(`Button clicked ${countRef.current} times.`);
    } 
    return <button onClick={handleClick}>Click me!</button>; 
}

7、useContext

​用于在函数式组件中访问上下文(Context​)的值。跨层级组件之间共享,传递数据的机制。

基础使用
  1. 创建 Context
js 复制代码
const MyContext = React.createContext(defaultValue);

❤️这里的 defaultValue 是当组件不在任何 Context Provider 内部时的默认值,defaultValue可以用 null,但 React 官方建议提供一个有意义的默认值,这样可以让调用usecontext组件更安全。

  1. 使用 Context Provider
js 复制代码
<MyContext.Provider value={someValue}>
    {/* 子组件 */} 
</MyContext.Provider>

❤️为了在组件树中使用这个 context ,需要使用 组件,接受一个valueprop,这就是在其他子组件中共享的值。

  1. 在组件中访问 Context
js 复制代码
function MyComponent() { 
    const contextValue = useContext(MyContext); 
    return <div>{contextValue}</div>; 
}

❤️在函数组件中,可以使用 useContext 来访问这个 context 的值。这里的 contextValue 就是第二步传入的 someValue,而且

contextValue 获取到的永远是最新的值。


使用案例
js 复制代码
import React, { useContext } from 'react'; 
// 1. 创建 Context
const ThemeContext = React.createContext('light');
function App() {
    return (
        // 2. 使用 Context Provider 
        <ThemeContext.Provider value="dark"> 
            <Toolbar /> 
        </ThemeContext.Provider> 
     );
} 
function Toolbar() { 
     return (
         <div>
             <ThemeButton />
         </div>
     ); 
}
function ThemeButton() {
     // 3. 在组件中访问 Context 
     const theme = useContext(ThemeContext); 
     return <button>{theme} theme</button>; 
} 

export default App;

❤️在以上例子中的总结如下:

  1. App 中引用了 ThemeContext 并传了值;
  2. ThemeButton 是 App 的孙组件,这二者之间没有通过 Toolbar 进行嵌套传值;
  3. ThemeButton 依然通过useContext拿到了 App 里的值。
  4. React.createContext和useContext共同组成了一个管道,通过这个管道,可以进行跨组件共享状态。

覆盖Provider value
js 复制代码
<ThemeContext.Provider value="dark">
    ... 
    <ThemeContext.Provider value="light">
        <Footer />
    </ThemeContext.Provider>
    ...
 </ThemeContext.Provider> 
 //当我们调用多个相同 Context,会实现value的覆盖

注意事项

当 Provider 的 value 属性值发生变化时,所有使用了 useContext 的组件都将重新渲染。如果value经常变化,或者消费者组件很多,那么这会引起大量的不必要的渲染。

  1. 细化 Context:如果 context 包含许多不同的状态值,可以将它们分解成多个 context。
  2. 使用 useMemo 和 useCallback 优化 value:为了避免 value 变化造成子孙组件频繁的重新渲染,可以使用 useMemo 和useCallback 对参数和方法进行缓存,减少value的无意义更新。

8、useReducer

可以将其看作是一种"全局变量"的替代方案。使用 useReducer 可以更好地组织和管理组件的状态,并且适用于一些具有复杂状态逻辑的组件。与useState相比,useReducer更适用于状态逻辑比较复杂,或者需要多个状态之间存在关联的情况。使用useReducer时,可以将所有的状态更新逻辑集中在reducer函数中,使代码更加清晰和易于维护。

使用案例
  1. 创建reducer函数,接受两个参数:当前的状态(state)和要执行的action;reducer 函数根据 action的类型来更新状态,并返回新的状态
js 复制代码
const reducer = (state, action) => {
    switch (action.type) { 
        case 'INCREMENT': 
            return { count: state.count + 1 }; 
        case 'DECREMENT': 
            return { count: state.count - 1 }; 
        default: return state; 
    } 
};
  1. 调用useReducer 将返回当前状态和 dispatch 函数,dispatch函数用于发送action以触发状态更新。
js 复制代码
import React, { useReducer } from 'react'; 
const initialState = { count: 0 }; 
function Counter() { 
    const [state, dispatch] = useReducer(reducer, initialState); 
    
    return (
        <div> 
            Count: {state.count} 
            <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
            <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button> 
        </div> 
    ); 
 }

注意事项

  1. 纯函数reducer:确保reducer函数是纯函数,接收状态和action,返回新状态,避免副作用。
  2. 合理划分状态:将状态合理地划分为多个reducer,避免将所有状态放在一个reducer中。
  3. 适度使用:只在需要复杂状态逻辑时使用useReducer,简单状态管理优先考虑useState。
  4. 初始状态选择:提供合适的初始状态,确保与组件预期行为相符,并具有合理的默认值。
  5. 避免直接修改状态:始终通过dispatch函数发送action来更新状态,避免直接修改状态对象。
  6. 性能优化:使用useCallback和useMemo优化性能,减少不必要的渲染和计算。
  7. 充分利用Context:useReducer通常与Context一起使用,便于状态共享和管理。

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax