web前端02:关于React中的hooks
一. react用法
1. react渲染逻辑
根据数据变化,不断重新计算UI,只更新变化的部分。
1.render(计算):执行组件函数,生成虚拟DOM。
2.Diff(对比):对比虚拟DOM和真实DOM,找出变化的部分。
3.Commit(更新 DOM):根据对比结果,更新真实DOM。
2. 常用的hooks
2.1 useState
它是什么?
useState 是 React 中用来保存组件内部状态的工具。你可以把它理解为:
📦 “一个可以存数据的盒子”,
💡 当你修改这个数据时,页面会自动重新渲染。
能做什么?
常见用途包括:计数器,表单输入值,控制元素的显示/隐藏,切换按钮状态等,当你想让页面随数据变化而更新时,就用它
如何使用?
import { useState } from 'react';
const [count, setCount] = useState(0);
- count:当前的值(状态)
- setCount:修改状态的方法
- 参数 0:初始状态
举个例子:
点击按钮时,show 状态会在 true/false 之间切换,页面也会跟着更新。
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(!show)}>切换显示</button>
{show && <p>你好,React!</p>}
</div>
);
在实际开发中,你可能会遇到这些情况
(一).初始值通过函数获取
当初始值是从缓存,本地存储等读取到的,或者是经过复杂计算得到时,可以使用函数式初始值,这个初始值通过一个函数来“延迟”执行,只在组件第一次渲染时运行一次。
const [count, setCount] = useState(() => {
return localStorage.getItem('count') || 0;
});
(二).函数式更新状态值
有时候,新的状态值依赖于“上一次的值”,这时推荐用函数式更新,避免异步更新带来的问题。比直接写 setCount(count + 1) 更安全,特别是在连续调用或并发场景中。
setCount((prevCount) => prevCount + 1);
(三).更新对象,数组
状态是对象时,注意浅合并问题 ,useState 不像 class 组件中的 setState 会自动合并对象,所以需要你自己手动合并。
/* 更新对象 */
const [form, setForm] = useState({ name: '', age: 0 });
// 错误!会丢失 age
setForm({ name: '小明' });
// 正确方式(合并之前的状态)
setForm(prev => ({ ...prev, name: '小明' }));
/* 更新数组 */
const [list, setList] = useState<string[]>([]);
// 添加新项
setList(prev => [...prev, '新项']);
// 删除某项
setList(prev => prev.filter(item => item !== '要删除的项'));
2.2 useEffect
它是什么?
useEffect 是 React 中的一个 Hook,意思是“在函数组件中处理副作用(Effect)”。
🔧 “当组件加载、更新或卸载时,执行一些额外的操作。”
能做什么?
- 当页面加载时需要初始化数据(发请求)
- 监听页面大小变化、滚动事件
- 设置定时器、轮询
- 操作 DOM(比如焦点)
- 清除一些资源(定时器、监听器等)
如何使用?
useEffect(() => {
// ✅ 这里写你想做的操作(副作用)
return () => {
// ❌ 清理操作(组件卸载或依赖变化前)
};
}, [依赖项]);
关于依赖项:
| 写法 | 意义 |
|---|---|
[] | 只在第一次加载时执行一次(如:初始化请求) |
[value] | 每次 value 发生变化时执行 |
| 没写依赖 | 每次组件渲染(更新)时都会执行(不常用) |
在实际开发中,你可能会遇到这些情况
useEffect 就是“在 React 组件里做副作用操作的地方”,你可以理解为是函数组件里的“生命周期钩子”。
页面加载时发送请求
useEffect(() => {
fetchData();
}, []); // 空数组表示只执行一次
根据某个值的变化做响应操作
useEffect(() => {
console.log('count变了');
}, [count]); // 每次 count 改变都会执行
组件卸载时清除定时器、取消事件监听、终止网络请求等
useEffect(() => {
const timer = setInterval(() => {
console.log('运行中...');
}, 1000);
return () => {
clearInterval(timer); // 组件卸载或依赖变化时清除
};
}, []);
用户输入时不要每次都发请求,而是“停下来一段时间再发” (防抖/节流)
const [keyword, setKeyword] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
if (keyword) {
fetch(`/api/search?q=${keyword}`);
}
}, 500); // 500ms 防抖
return () => clearTimeout(timer); // 输入时先清除前一个
}, [keyword]);
依赖多条件组合(避免重复触发)
useEffect(() => {
if (ready && userId) {
fetchUserData(userId);
}
}, [ready, userId]);
嵌套异步函数(useEffect 不能直接 async)
useEffect(() => {
(async () => {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
})();
}, []);
2.3 useRef
介绍 useRef,forwardRef,useImperativeHandle 的用法,以及useRef和useEffect搭配使用
它是什么?
useRef 是 React 中一个能存储值的 Hook,是你在 React 函数组件中存“稳定的值或 DOM 引用”的秘密武器,不会因渲染而丢失,非常适合存 DOM、定时器、最新状态等“静态但重要”的信息。
🔍 一个不会变的盒子(引用容器),里面装着你想保存的数据或 DOM 节点。
能做什么?
- 访问DOM元素(获取真实DOM)
- 存储不会触发重新渲染的数据(比如定时器ID,上一次的值等)
- 在父子组件中,父组件获取子组件的DOM或调用子组件向父组件暴露的方法
如何使用?
const myRef = useRef(初始值);
myRef.current就是你要用的“那个值”- 它不会引起页面重新渲染(不像 useState)
在实际开发中,你可能会遇到这些情况
页面加载后自动聚焦到输入框
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
每次点击打印最新值,但页面不会重新渲染
const countRef = useRef(0);
function handleClick() {
countRef.current += 1;
console.log(countRef.current);
}
需要在异步函数、定时器、事件监听器里使用最新的状态值时,搭配 useEffect 使用
const [count, setCount] = useState(0);
const latestCount = useRef(count);
// 每次渲染都更新 ref 的值
useEffect(() => {
latestCount.current = count;
});
useEffect(() => {
const timer = setInterval(() => {
console.log('当前 count:', latestCount.current); // 总是最新的值
}, 1000);
return () => clearInterval(timer);
}, []);
需要保存一个“全局变量”,它不需要触发组件更新,类似组件内的“静态变量”。
const cacheRef = useRef({
fetched: false,
data: null,
});
结合 forwardRef,useImperativeHandle 实现父组件访问子组件内部 DOM 或方法
const MyInput = forwardRef((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
}));
return <input ref={inputRef} />;
});
const Parent = () => {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<MyInput ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦输入框</button>
</>
);
};
2.4 React.memo,useMemo,useCallback
它是什么?
React.memo 是 React 提供的一个高阶组件(HOC****),用于缓存函数组件****的渲染结果。如果组件的 props 没有变化,它就不会重新渲染。
useMemo 是 React 的一个 Hook,用于缓存某段“计算逻辑”的返回结果,只有依赖项变化时才重新执行。
useCallback 是 React 提供的一个 Hook,用来缓存一个函数的引用地址,让它在组件重新渲染时不会变,除非它的依赖项变了。
| 名称 | 类型 | 优化目标 | 常见用途 |
|---|---|---|---|
React.memo | 高阶组件 | 避免组件不必要渲染 | 包裹函数组件 |
useMemo | Hook | 避免重复计算 | 缓存计算结果、稳定对象/数组引用 |
useCallback | Hook | 避免函数重复定义 | 缓存函数引用,防止组件子组件重渲染 |
能做什么?
- 避免组件不必要的重复渲染 (React.memo)
- 提高性能,减少 DOM 操作和 diff 计算 (React.memo)
- 有复杂运算逻辑(如筛选、排序、大数组处理等) (useMemo)
- 传递的对象/数组 props 导致子组件每次都渲染(引用变)(useMemo)
- 需要缓存计算结果以提升性能 (useMemo)
- 保证函数在多次渲染中引用稳定 (useCallback)
- 避免把“变了”的函数传给
React.memo子组件,导致子组件重新渲染 (useCallback)
如何使用?
React.memo
const ChildComponent = React.memo((props) => {
return <div>{props.title}</div>;
});
- 如果
props没变,组件就不会重新渲染 - 用于纯展示组件,性能优化非常有效
useMemo
const cachedValue = useMemo(() => {
return heavyFunction(value);
}, [value]);
- 只有
value变化时才会重新计算 - 适合:复杂计算、生成大数组、创建对象、依赖值传递等
useCallback
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
- 当你需要“记住”一个函数,或者需要保证函数地址不变时,就应该使用
useCallback。
在实际开发中,你可能会遇到这些情况
React.memo
每次点击按钮,只更新父组件Parent 中 count,但子组件 MemoChild 不会重新渲染
const Parent = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<MemoChild title="我是静态的标题" />
</>
);
};
const MemoChild = React.memo(({ title }: { title: string }) => {
console.log('MemoChild 渲染了');
return <p>{title}</p>;
});
useMemo
缓存数组计算,虽然 count 改变,expensiveList 不会重新计算。
const [count, setCount] = useState(0);
const expensiveList = useMemo(() => {
console.log('正在进行大计算...');
return Array.from({ length: 10000 }, (_, i) => i * 2);
}, []); // 空依赖:只计算一次
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
需要避免子组件不必要的渲染时
每次 Parent 渲染都会创建一个新对象,Child 会被强制重渲染。
'use client'; //next.js
import React, { useState, useMemo } from 'react';
type users = {
name: string;
};
const useMemoDemo: React.FC = () => {
const [count, setCount] = useState(0);
// const user: users = { name: '小明' }; // 优化后,user不会随着count变化而变化
const user: users = useMemo(
() => ({
name: '小明',
}),
[],// [count]感受一下当count变化时,子组件才会渲染
);
return (
<div>
<button onClick={() => setCount(count + 1)}>+1</button>
<div>count:{count}</div>
<UseMemoChildren user={user} />
</div>
);
};
const UseMemoChildren = React.memo(({ user }: { user: users }) => {
console.log('我是子组件,我渲染了');
return <p>{user.name}</p>;
});
export default useMemoDemo;
useCallback
防止子组件重新渲染
在父子组件中,父组件传给子组件的是函数,就要想到useCallback,避免重复调用函数
每次点击“父组件加一”,父组件重新渲染
但 Child不会重新渲染,因为 onClick 函数引用没变
'use client';
import React, { useState, useCallback } from 'react';
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('👶 子组件渲染');
return <button onClick={onClick}>点击我</button>;
});
const useMemoDemo = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('点击了子组件按钮');
}, []);
// ⚠️ 如果不用 useCallback,点击“父组件加一”按钮重新渲染页面时,onClick 都是新函数,Child 会重新渲染
// const handleClick = () => {
// console.log('点击了子组件按钮');
// };
return (
<>
<p>父组件 count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>父组件加一</button>
<Child onClick={handleClick} />
</>
);
};
export default useMemoDemo;
作为 useEffect 的依赖保持函数引用稳定
fetchUser 每次渲染都会变化,useEffect 就会一直触发!
const MyComponent = ({ userId }: { userId: string }) => {
const fetchUser = useCallback(() => {
console.log('请求用户数据:', userId);
// fetch(`/api/user/${userId}`);
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]); // ✅ 不会死循环
return <div>用户数据加载中...</div>;
};
与 debounce 结合实现防抖/节流
如果不使用 useCallback,每次输入时都会重新创建一个新的 debounced 函数,前一次设置的定时器就“白建了”。
'use client';
import React, { useState, useCallback } from 'react';
import { debounce } from 'lodash';
const useMemoDemo = () => {
const [keyword, setKeyword] = useState('');
const fetchData = (value: string) => {
console.log('请求接口:', value);
};
// 使用 useCallback 缓存 debounce 包装后的函数,防止重新创建导致失效
const handleSearch = useCallback(
debounce((value: string) => fetchData(value), 300),
[],
);
return (
<input
placeholder="搜索"
onChange={(e) => {
setKeyword(e.target.value);
handleSearch(e.target.value);
}}
/>
);
};
export default useMemoDemo;
2.5 useContext
它是什么?
useContext 是 React 提供的一as【】】个 Hook,用来跨组件共享数据。它是 Context API 的配套使用方式,让你可以不通过 props 一层层传递数据,而是直接在组件中访问共享状态。
能做什么?
- 共享主题(暗色 / 亮色)
- 共享登录信息(用户名、Token)
- 共享语言设置 / 多语言
- 配置项共享
- 轻量状态管理
如何使用?
创建 Context 文件(ThemeContext.tsx)
import { createContext } from 'react';
export const ThemeContext = createContext<'light' | 'dark'>('light');
在顶层组件提供 Provider(一般是 App.tsx)
import { ThemeContext } from './ThemeContext';
const App = () => {
return (
<ThemeContext.Provider value='dark'>
<Page />
</ThemeContext.Provider>
);
};
在任意子组件中消费 Context
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
const Page = () => {
const theme = useContext(ThemeContext);
return <div>当前主题是:{theme}</div>;
};
在实际开发中,你可能会遇到这些情况
页面需要根据用户选择在“明亮模式”和“暗黑模式”之间切换。
子组件根据
theme改变样式、className、颜色变量
const ThemeContext = createContext<'light' | 'dark'>('light');
const theme = useContext(ThemeContext);
登录信息 / 用户信息共享
用户登录后,用户头像、用户名、token 等信息在多个组件中都要使用。
登录后在顶层组件通过 Provider 传入 user 数据
其他组件(如 Header、Sidebar、Profile)中调用
useContext(AuthContext)获取用户信息
const AuthContext = createContext<User | null>(null);
const user = useContext(AuthContext);
权限控制(访问权限)
根据用户权限判断某些页面或功能是否可见。
判断
permissions.includes('user:add')决定按钮是否显示或结合
react-router判断是否能进入页面
const PermissionContext = createContext<string[]>([]);
const permissions = useContext(PermissionContext);
语言切换 / 国际化(i18n)
项目中需要多语言支持,比如中英文切换。
const LangContext = createContext<'en' | 'zh-CN'>('zh-CN');
const lang = useContext(LangContext);
页面级配置共享
页面某些配置(如表格列配置、分页大小、搜索关键字等)要在多个组件中共享。
const PageConfigContext = createContext({ pageSize: 10 });
const config = useContext(PageConfigContext);
2.6 useReducer
它是什么?
useReducer 是 React 提供的一个 Hook,用于以更清晰的方式管理复杂状态逻辑。
它的工作方式类似于 Redux:通过 action 改变状态,适合多状态组合、状态变更逻辑清晰表达的场景。
能做什么?
- 替代
useState管理多个相关状态 - 封装复杂的状态变更逻辑(例如:购物车、表单校验、异步状态)
- 做出 Redux 的轻量版(无需引入库)
如何使用?
const [state, dispatch] = useReducer(reducer, initialState);
state: 当前状态dispatch(action): 触发状态更新reducer(state, action): 状态更新逻辑initialState: 初始状态值
// reducer 函数
function reducer(state: number, action: { type: 'inc' | 'dec' }) {
switch (action.type) {
case 'inc':
return state + 1;
case 'dec':
return state - 1;
default:
return state;
}
}
export default function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => dispatch({ type: 'inc' })}>加</button>
<button onClick={() => dispatch({ type: 'dec' })}>减</button>
</div>
);
}
在实际开发中,你可能会遇到这些情况
多状态联动
例子:表单有
name,isValid,errorMsg多个状态,useState写起来麻烦,useReducer可以统一管理。
type FormState = { name: string; email: string; error: string };
type Action =
| { type: 'change_name'; payload: string }
| { type: 'change_email'; payload: string }
| { type: 'set_error'; payload: string };
function formReducer(state: FormState, action: Action): FormState {
switch (action.type) {
case 'change_name':
return { ...state, name: action.payload };
case 'change_email':
return { ...state, email: action.payload };
case 'set_error':
return { ...state, error: action.payload };
default:
return state;
}
}
结合useContext+useReducer实现“全局状态管理”
仅适合一些简单的小项目,主流的状态管理工具会在后续章节介绍
const GlobalStore = createContext();
const [state, dispatch] = useReducer(reducer, initState);
2.7 自定义Hooks
它是什么?
自定义 Hook 是指你自己封装的、可复用的函数,它以 use 开头,内部可以使用 React 的其他 Hook(如 useState、useEffect 等)。
本质上就是:把组件中可复用的状态逻辑提取出来,变成一个函数。
能做什么?
- 抽离重复逻辑(如请求数据、监听窗口大小、节流、防抖等)
- 提高代码复用性和可读性
- 封装复杂逻辑,保持组件简洁
- 统一管理 side effect(副作用)
如何使用?
举个简单例子:监听窗口宽度变化
自定义hook:useWindowWidth.tsx
import { useEffect, useState } from 'react';
export function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return width;
}
在组件中使用
import { useWindowWidth } from './useWindowWidth';
export const MyComponent = () => {
const width = useWindowWidth();
return <p>当前窗口宽度:{width}px</p>;
};
在实际开发中,你可能会遇到这些情况
自定义 Hook 就是你自己造的“乐高积木”,可以随时复用和组合,帮你写出更干净、逻辑更清晰的组件。
自定义hooks的通用模板
- ✅ 命名:以
use开头 - ✅ 可使用其他 Hooks(如
useState、useEffect) - ✅ 通常 return:状态值 / 方法 / 组合对象
import { useEffect, useState } from 'react';
// 函数名必须以 use 开头
function useSomething(param) {
const [state, setState] = useState(null);
useEffect(() => {
// 执行副作用逻辑
// 如监听事件、请求数据等
return () => {
// 清理逻辑(可选)
};
}, [param]);
// 返回需要暴露的数据或函数
return state;
}
以下是在实际开发中可以封装的自定义hooks,也可以用第三方自定义hooks库
| 场景 | 描述 | 自定义 Hook 示例 |
|---|---|---|
| 页面加载数据 | 组件挂载时请求接口 | useFetch(url)、useRequest(apiFn) |
| 监听滚动或窗口 | 页面滚动、窗口变化 | useScrollPosition()、useWindowSize() |
| 表单处理 | 表单字段状态与校验 | useForm()、useInput(name) |
| 防抖/节流处理 | 处理高频操作(如搜索) | useDebounce(value, delay)、useThrottle(fn) |
| 倒计时功能 | 常用于验证码发送 | useCountdown(initialTime) |
| 保存上一次状态 | 类似于“上一个 props” | usePrevious(value) |
| 页面标题管理 | 设置浏览器 tab 的标题 | useDocumentTitle(title) |
| 深层状态管理 | 封装 useReducer 或状态容器 | useGlobalStore() |
推荐几个高质量的自定义hooks库
| 库名 | 特点 |
|---|---|
| ahooks | 阿里开源,功能全、文档好、适合企业级项目 |
| react-use | 社区活跃、涵盖面广,轻量实用 |
| usehooks-ts | 基于 TypeScript 的实用 hook 集合,适合 TS 项目 |
| zustand | 虽然是状态库,但也支持用 hook 的形式管理全局状态 |