引言
官方已经封装好了很多函数,这些钩子函数是react的核心之处
很多功能、效果的实现需要用到这些钩子函数
接下来要讲讲一些最常用的、最关键的钩子函数
准备
- 依旧创建一个新的react项目
- 删除不必要的文件,只留下App.jsx和index.jsx
- 在APP.jsx删除不必要的代码
- 凡事先看官方文档
- 快速入门 – React 中文文档
- 接下来的代码基本是写在src目录下,每个案例可以单独创建一个后缀为.jsx的文件,注意需要抛出和引入
1. useState - 状态管理的基石
useState 是最基础也是最常用的 Hook,用于在函数组件中添加状态。
基本用法
- useState是官方封装好的函数,需要通过对象解构获取
- 定义一个响应式变量,提供专门的方法修饰变量值
const [count, setCount] = useState(0);- 这行代码是声明了一个变量,变量名为count,初始值为0;声明了一个函数,函数名为setCount
- 函数setCount接收一个参数,为变量count赋值
onClick={() => setCount(count + 1)}- 这是绑定了一个点击事件,里面放一个回调函数
- 当点击相应的按钮,数字就是增加或减小
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>
增加
</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>
减少
</button>
</div>
);
}
关键点一:数据自动重新渲染的秘密
- 通过重新加载整个组件实现页面刷新,但是变量会做缓存,确保原先数据不会丢失(这话的意思是确保变量重复声明后还能获取到页面刷新前的值)
- 观察下面这份代码,页面初次加载时打印了“页面刷新了”
- 在button按钮上绑定了一个点击事件
- 每次点击这个按钮,都会让num的值+1
- num + 1后的值要在页面上重新渲染
- 这时useState函数就会重新加载整个组件,但是会利用缓存保留之前的num值,使得数据重新渲染到页面上,而不是让num重新声明后回到初始值
- 所以,每次点击button,都会打印“页面刷新了”,并且num值也在增加
import { useState } from "react";
function App() {
let [num, setNum] = useState(1); //[1,function]
function handle() {
setNum(++num);
}
console.log("页面刷新了");
return (
<button onClick={handle}>{num}</button>
)
}
export default App;
关键点二:useState赋初始值时不能放异步代码
- 声明了函数query模拟一个接口
- 将query的返回值传给useState作为num的初始值
- 但是浏览器并未显示任何东西
- 这就说明useState赋初始值时不能放异步代码
import { useState } from "react";
function App() {
async function query() {
const data = await new Promise((reslove) => {
setTimeout(() => {
reslove(2);
}, 2000)
})
return data;
}
let [num, setNum] = useState(query());
return (
<button>{num}</button>
)
}
export default App;
2. useEffect - 异步副作用函数
- 组件每次加载(挂载)会触发
- useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发
- useEffect 第二个参数为一个数组,数组中传入一个变量时,该变量每次修改值都会带来useEffect的重新执行
- useEffect 第一个参数是函数,该函数内部返回出来一个新的函数,新函数会在组件不展示(卸载)时才触发
案例一:组件每次加载(挂载)会触发
- 初次加载页面,会打印“页面刷新了”
- 每次手动刷新页面,也都会打印“页面刷新了”
import { useEffect } from "react"
export default function App() {
useEffect(() => {
console.log("组件刷新了");
})
return (
<div></div>
)
}
案例二:useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发
- 这份案例里,useEffect 第二个参数没有放任何值
- 每次点击按钮,都会让num的值+1,利用useState的渲染机制让页面重新刷新
- useEffect在页面刷新时都会加载一遍
- 所以出现了页面多次刷新
import { useState, useEffect } from "react";
function App() {
let [num, setNum] = useState(1);
useEffect(() => {
console.log(`页面第${num}次刷新`);
})
return (
<button onClick={handle}>{num}</button>
)
}
export default App;
现在第二个参数放数组
- 可以看到,useEffect里的代码只在页面初次加载时执行了一遍
- num的值变更,虽然导致了组件被重新加载
- 但是useEffect里的代码并没有被重新执行
- 这就说明了useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发
import { useState, useEffect } from "react";
function App() {
let [num, setNum] = useState(1);
useEffect(() => {
console.log(`页面第${num}次刷新`);
},[])
return (
<button onClick={handle}>{num}</button>
)
}
export default App;
案例三:useEffect 第二个参数为一个数组,数组中传入一个变量时,该变量每次修改值都会带来useEffect的重新执行
- 利用钩子函数useState声明了一个变量num
- 在按钮上绑定了一个点击事件
- 每次点击都会使得num的值+1
- useState的渲染机制会让组件重新刷新
- 这时useEffect的第二个参数为[num]
- num的值发生改变,就会重新执行useEffect里的代码
- 所以,浏览器的控制台上就会打印页面第xx次刷新
import { useLayoutEffect, useState, useEffect } from "react";
function App() {
const [num, setNum] = useState(1)
useEffect(() => {
console.log(`页面第${num}次刷新`);
}, [num])
return (
<div>
<button onClick={() => { setNum(num + 1) }}>{num}</button>
</div>
)
}
export default App;
3. useLayoutEffect - 同步副作用处理
useLayoutEffect与useEffect类似useLayoutEffect是同步代码
案例:为什么页面始终显示100
- num值变更引起了组件重新的重新渲染
- num虽然已经+1,但是在并未渲染到浏览器上时
- 这段时间,num值变更,会引起组件重新加载,代码从上往下执行,到return时页面才会重新渲染
- 先执行useLayoutEffect,因为useLayoutEffect第二个参数是[num],机制与useEffect是一样的
- 在useLayoutEffect里有setNum(100),所以,这时,num值从2变成了100
- 所以,不管点击多少次,都只能在页面上看到100
function App() {
const [num, setNum] = useState(1)
useLayoutEffect(() => {
setNum(100);
}, [num])
return (
<div>
<button onClick={() => { setNum(num + 1) }}>{num}</button>
</div>
)
}
export default App;
4. useEffect vs useLayoutEffect - 关键区别
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | DOM 更新后异步执行 | DOM 更新后同步执行 |
| 阻塞渲染 | 不阻塞 | 阻塞浏览器绘制 |
| 使用场景 | 数据获取、订阅、一般副作用 | DOM 测量、防止闪烁 |
| 性能影响 | 较小 | 可能影响性能 |
对比示例发现
- useLayoutEffect防止布局闪烁
5. useReducer + Immer - 复杂状态管理
useReducer适合处理复杂的状态逻辑,配合 Immer 可以更方便地处理不可变数据。
安装 Immer
npm install immer
案例
- 这是一份简单的案例,模拟了加法和减法运算,并将最终的结果渲染到页面上
- const [res, dispatch] = useReducer(reducer, { result: 0 })
- 这时useReducer声明新变量的方式,[]里第一个是声明了一个变量,接收一个对象{ result: 0 },第二个是一个函数
- 例如这里的dispatch是一个函数,可以理解为一个中转站,会将接收到的值传递给函数reducer,reducer专门处理复杂的运算,这里用加减法简单模拟了一下
- reducer拿到值后,将值传递给了自己的第二个参数action
- 初始值{ result: 0 }传递给state
- useReducer里的参数不可调换位置,第一个必须是一个函数,第二个必须是一个对象
- reducer里接收两个参数,不可调换位置
import { useReducer } from "react"
import { produce } from 'immer'
function reducer(state, action) {
// { result: 0 } { type: 'add', num: 2 }
console.log(state, action);
switch (action.type) {
case 'add':
return produce(state, (state) => {
state.result += action.num;
})
case 'minus':
return produce(state, (state) => {
state.result -= action.num;
})
}
}
function App() {
const [res, dispatch] = useReducer(reducer, { result: 0 })
return (
<div>
<h3>{res.result}</h3>
<button onClick={() => dispatch({ type: 'add', num: 2 })}>+</button>
<button onClick={() => dispatch({ type: 'minus', num: 2 })}>-</button>
</div>
)
}
export default App
6. useRef - DOM 引用和值持久化
useRef:用于获取Dom结构- 与原生JS里的那些方法有所类似
- 都要先“标记”一下元素
- const input = useRef(null);
- 先用useRef声明一个变量
- 再在Dom结构上加上ref={变量名}
- 这样就完成了Dom结构的抓取
DOM 操作
import { useEffect, useRef } from "react";
function App() {
const input = useRef(null);
useEffect(() => {
// 页面初次加载将光标聚焦在输入框上
input.current.focus()
console.log(input);
}, [])
return (
<div>
<input type="text" ref={input} />
</div>
)
}
export default App;
7. useContext - 跨组件状态共享
useContext 用于在组件树中共享数据,避免 props 逐层传递。
- import { createContext, useContext } from "react";
- 从react中结构出两个钩子函数
- createContext用于创建上下文对象
- 这行代码就是声明了一个上下文对象
const numContext = createContext() - useContext用于获取上下文对象
- 看代码,需要用声明的对象作为标签
<numContext.Provider value={num}> </numContext.Provider>value={num}传递值
案例
- Child1被包含在父组件里
- Child2被包含在Child1组件里
- Child2组件拿到了父组件传递的值
- 虽然整个组件只抛出了App
- 但是,页面上三个组件都能正常加载出来
import { createContext, useContext } from "react";
const numContext = createContext()
function App() {
const num = 100;
return (
<div>
<numContext.Provider value={num}>
<h1>父组件</h1>
<Child1 />
</numContext.Provider>
</div>
)
}
function Child1() {
return (
<div>
<h2>子组件</h2>
<Child2 />
</div>
)
}
function Child2() {
const count = useContext(numContext)
return (
<div>
<h3>孙子组件---{count}</h3>
</div>
)
}
export default App;
9. Hooks 最佳实践
1. 规则遵循
- 只在最顶层调用 Hook:不要在循环、条件或嵌套函数中调用
- 只在 React 函数中调用 Hook:函数组件和自定义 Hook
- 使用 ESLint 插件:
eslint-plugin-react-hooks帮助检查规则
2. 性能考虑
// 好的做法:合理使用依赖数组
useEffect(() => {
fetchData(id);
}, [id]); // 只有 id 变化时才重新获取
// 避免:依赖数组过于频繁变化
useEffect(() => {
fetchData(user.id);
}, [user]); // user 对象引用变化会导致频繁执行
// 更好的做法:解构需要的值
useEffect(() => {
fetchData(user.id);
}, [user.id]); // 只关心 id 的变化
总结
React Hooks 为函数组件提供了强大的状态管理和副作用处理能力。通过合理使用这些 Hooks,可以编写出更简洁、更可维护的 React 应用:
hooks(钩子函数)
- 由 react 官方封装好的的一系列函数,它们的用法和作用
useState ---定义一个响应式变量,提供专门的方法修饰变量值
- 通过重新加载整个组件实现页面刷新,但是变量会做缓存,确保原先数据不会丢失(这话的意思是确保变量重复声明后还能获取到页面刷新前的值)
useEffect --- 副作用函数
- 组件每次加载(挂载)会触发
- useEffect 第二个参数为一个空数组时,只会在组件初次渲染(挂载)时触发
- useEffect 第二个参数为一个数组,数组中传入一个变量时,该变量每次修改值都会带来useEffect的重新执行
- useEffect 第一个参数是函数,该函数内部返回出来一个新的函数,新函数会在组件不展示(卸载)时才触发
useLayoutEffect
- JS的加载会影响HTML的渲染
- 中的 effect 函数作为同步函数来执行
useReducer
- 当修改state的逻辑比较复杂时,用useReducer
- 传入的 reducer 函数中不能直接修改原 state,必须要返回一个新对象
- 配合 immer 一起使用,直接修改原对象,如果原对象太复杂,返回新对象太麻烦了
- npm i immer
useRef
- 获取 DOM 结构
useContext
- 跨多层组件进行数据传递
掌握这些 Hooks 的使用方法和最佳实践,将让你在 React 开发中更加得心应手。记住,选择合适的工具来解决特定的问题,避免过度工程化,始终保持代码的简洁和可读性。