-
React Hooks 是 React 16.8 引入的一项重要特性,它使函数组件能够拥有类组件的一些特性,例如状态管理和生命周期方法的使用;
-
通过 Hooks 可以更加简洁和灵活地编写 React 组件;
什么是 React Hooks
React Hooks 是一种函数式组件的增强机制,它允许在不编写类组件的情况下使用 React 的特性;
主要的 Hooks 包括 useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef 和 useImperativeHandle 等,这些 Hooks 提供了访问 React 特性的方式,可以更好地组织和重用代码;
class 组件的问题
复杂组件变得难以理解
- 在最初编写一个 class 组件时,往往逻辑比较简单,并不会非常复杂,但是随着业务的增多,class 组件会变得越来越复杂;
- 比如 componentDidMount 中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听 (还需要在 componentWillUnmount 中移除),而对于这样的 class 实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度;
难以理解的 class,处理 this 比较麻烦;
组件复用状态很难;
使用 Hooks 的好处
-
更简洁的组件逻辑:无需编写类组件,可以使用函数组件和 Hooks 来管理状态和生命周期;
-
提高代码复用性:Hooks 可以将逻辑提取到可重用的函数中,减少重复代码;
-
更好的性能优化:使用 useEffect、useCallback、useMemo 等 Hooks 可以更精确地控制副作用和性能消耗;
注意事项
-
仅在顶层使用 Hooks:不要在循环、条件或嵌套函数中调用 Hook,确保 Hooks 在每次渲染时都以相同的顺序被调用;
-
使用 ESLint 插件:React 官方提供了 eslint-plugin-react-hooks 插件来帮助检查 Hook 的使用是否正确;
Effect Hook
-
Effect Hook 用于在函数组件中处理副作用:
- ajax 请求
- 计时器
- 其他异步操作
- 更改真实 DOM 对象
- 本地存储
- 其他会对外部产生影响的操作
- …
-
现在有一个需求:页面的 title 总是显示 counter 的数字 (生命周期 VS Effect Hook);
JSXJSX// 使用生命周期函数实现 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> ); }
语法
-
useEffect(setup, dependencies?)
useEffect( () => { // 组件挂载、更新的时候执行的副作用操作 return () => {/* 在组件卸载时,执行的清理操作(可选) */ }; }, [/* 依赖数组 */] );
-
细节
- 副作用函数的运行时间点,是在页面完成真实的 UI 渲染之后。因此它的执行是异步的,并且不会阻塞浏览器:
- componentDidMount 和 componentDidUpdate 更改了真实 DOM,但是用户还没有看到 UI 更新,同步的;
- useEffect 中的副作用函数,更改了真实 DOM,并且用户已经看到了 UI 更新,异步的;
- 每个函数组件中,可以多次使用 useEffect,但不要放入判断或循环等代码块中;
- useEffect 中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数,组件被销毁时一定会运行;
- useEffect 函数,可以传递第二个参数:
- 第二个参数是一个数组,记录该副作用的依赖数据,当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用;
- 所以,当传递了依赖数据之后,如果数据没有发生变化,副作用函数仅在第一次渲染后运行,清理函数仅在卸载组件后运行;
- 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化;
- 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂;
- 副作用函数的运行时间点,是在页面完成真实的 UI 渲染之后。因此它的执行是异步的,并且不会阻塞浏览器:
依赖值为对象
经常会将一个对象作为依赖,一般都是希望对象的内容发生变化时,去执行某些操作,在实际的业务开发中,会遇到一些莫名其妙的坑,列举几个常见的现象:
- 明明对象的内容已经发生了变化,但是为什么没有触发 useEffect;
- 明明对象的内容没有发生变化,但是为什么一直触发 useEffect;
-
改变对象中的属性值,未触发 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} />; }
-
接受父组件的对象属性作为依赖,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> ); };
-
对象内容未变化时,不希望触发 useEffect
- 业务层经常会对一些状态进行重置,setState([]) 或者 setState({}),有可能本身 state 的值就是 [] 或者 {},重置后,内容未发生变化,但是引用已经改变,从而导致触发 useEffect;
- 解决方案:将对象转为字符串后再作为 useEffect 的依赖;
-
第一次渲染时,不希望触发 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> ); };
-
两个 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]); };
-
不要将普通变量作为依赖;
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> ); };
-
依赖监听 useRef 的值,有时可以触发更新,有时无法触发更新;
- 现象:
- 点击 button1 时,会触发 useEffect1
- 点击 button2 时,不会触发 useEffect2
- 再次点击 button1 时,会触发 useEffect1 和 useEffect2
- 问题原因:只有状态变更的时候,才会触发更新,而状态变更,只有 useState 和 useReducer 可以触发更新;
- 使用指南:建议不要使用 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
-
在之前的开发中,要在组件中使用共享的 Context 共享数据,但是多个 Context 共享时的方式会存在大量的嵌套,有两种方式:
- 类组件可以通过 类名.contextType = MyContext 方式,在类中获取 context;
- 多个 Context 或者在 函数式 组件中通过 MyContext.Consumer 方式共享 context;
-
Context Hook 允许通过 Hook 来直接获取某个 Context 的值;
JSXJSX// 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
-
useReducer* 仅仅是 useState 的一种替代方案:
- 在某些场景下,如果 state 的处理逻辑比较复杂,可以通过 useReducer 来对其进行拆分;
- 或者这次修改的 state 需要依赖之前的 state 时,也可以使用;
JSXJSX// 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
React✍️ 属性默认值和类型检查
上一篇