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一起使用,便于状态共享和管理。

相关推荐
zwjapple1 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20203 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
aiprtem4 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊4 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术4 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing4 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止5 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall5 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴5 小时前
简单入门Python装饰器
前端·python
袁煦丞5 小时前
数据库设计神器DrawDB:cpolar内网穿透实验室第595个成功挑战
前端·程序员·远程工作