跳到主要内容
技术
前端
react

web前端02:关于React中的hooks

阅读需 18 分钟

一. 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高阶组件避免组件不必要渲染包裹函数组件
useMemoHook避免重复计算缓存计算结果、稳定对象/数组引用
useCallbackHook避免函数重复定义缓存函数引用,防止组件子组件重渲染
能做什么?
  • 避免组件不必要的重复渲染 (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

每次点击按钮,只更新父组件Parentcount,但子组件 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

防止子组件重新渲染
success

在父子组件中,父组件传给子组件的是函数,就要想到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, email, 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实现“全局状态管理”
success

仅适合一些简单的小项目,主流的状态管理工具会在后续章节介绍

const GlobalStore = createContext();
const [state, dispatch] = useReducer(reducer, initState);

2.7 自定义Hooks

它是什么?

自定义 Hook 是指你自己封装的、可复用的函数,它以 use 开头,内部可以使用 React 的其他 Hook(如 useStateuseEffect 等)。

本质上就是:把组件中可复用的状态逻辑提取出来,变成一个函数。

能做什么?
  • 抽离重复逻辑(如请求数据、监听窗口大小、节流、防抖等)
  • 提高代码复用性和可读性
  • 封装复杂逻辑,保持组件简洁
  • 统一管理 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(如 useStateuseEffect
  • ✅ 通常 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 的形式管理全局状态
Loading Comments...