文章目录
React Hooks 是 React 16.8 引入的特性,允许你在函数组件中使用状态和其他 React 特性。以下是 React 中主要的 Hooks 分类和具体用法:
一、基础Hooks
1、useState
- 在函数中添加状态管理
- 返回一个状态值和一个更新状态的函数
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2、useEffect
- 处理副作用(数据获取、订阅、手动修改DOM等)
- 可以模拟 componentDidMount、componentDidUpdate 和 componentWillUnmount
import { useState, useEffect } from 'react';
function Example() {
const [data, setData] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => setData(data));
return () => {
// 清理函数(如取消订阅)
};
}, []); // 空数组表示只在组件挂载时运行一次
}
3、useContext
访问React中的上下文(Context),避免多层 props 传递
import { useContext } from 'react';
const ThemeContext = React.createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button style={{ background: theme === 'dark' ? '#333' : '#eee' }}>按钮</button>;
}
二、额外Hooks
4、useReducer
复杂状态逻辑管理(类似Redux)
useReducer 基本语法:
const [state, dispatch] = useReducer(reducer, initialState, initFunction);
参数说明:
- reducer:一个函数,形式为(state,antion)=> newState
- initialStata:状态初始值
- initFunction(可选):用于惰性初始化状态的函数
工作原理:
- 组件通过dispatch(action)发出动作
- React调用reducer(currentState,action)计算新状态
- 组件使用新状态重新渲染
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
5、useRef
访问DOM节点或保存可变值(不会触发重新渲染)
访问DOM节点:
const inputRef = useRef(null);
<input ref={inputRef} />
保存可变值:创建可变引用,修改其值不会触发更新
import { useRef } from 'react';
function Counter() {
const countRef = useRef(0); // 初始化值为0
const handleClick = () => {
countRef.current += 1; // 修改ref值不会触发重新渲染
console.log(countRef.current);
};
return <button onClick={handleClick}>点击</button>;
}
特点:
- 不会触发重新渲染:修改 .current属性,不会导致组件重新渲染
- 跨渲染周期保持值:在组件的整个生命周期中保持同一个引用
- 与实例变量类似:类似于类组件中的this.xxx属性
常见使用场景:
1、访问DOM元素
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus(); // 访问DOM元素
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</>
);
}
2、保存可变值(不触发重新渲染)
function Timer() {
const countRef = useRef(0);
const [dummy, setDummy] = useState(0); // 用于强制渲染
useEffect(() => {
const id = setInterval(() => {
countRef.current += 1; // 修改ref值
console.log(countRef.current);
// 每5次更新一次UI
if (countRef.current % 5 === 0) {
setDummy(d => d + 1);
}
}, 1000);
return () => clearInterval(id);
}, []);
return <div>计数: {countRef.current}</div>;
}
3、保存上一次值
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; // 在渲染后更新ref
}); // 没有依赖数组,每次渲染后都执行
return (
<div>
<p>当前: {count}, 之前: {prevCountRef.current}</p>
<button onClick={() => setCount(c => c + 1)}>增加</button>
</div>
);
}
与useState区别:
特性 | useRef | useState |
---|---|---|
触发重新渲染 | 否 | 是 |
值存储位置 | 返回对象的.current属性 | 直接返回状态值 |
更新方式 | 直接修改.current | 必须通过setState函数 |
使用场景 | 需要保持可变但不触发渲染的值 | 需要触发UI更新的状态 |
4、高级用法-在useCallback中使用ref
function Form() {
const [value, setValue] = useState('');
const valueRef = useRef(value);
useEffect(() => {
valueRef.current = value; // 同步value到ref
}, [value]);
const handleSubmit = useCallback(() => {
console.log('提交的值:', valueRef.current); // 总是获取最新值
}, []); // 不需要依赖value
return (
<form onSubmit={handleSubmit}>
<input value={value} onChange={e => setValue(e.target.value)} />
</form>
);
}
5、实现组件实例方法
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
scrollIntoView: () => {
inputRef.current.scrollIntoView();
}
}));
return <input ref={inputRef} />;
}
const ForwardedFancyInput = forwardRef(FancyInput);
// 父组件使用
function Parent() {
const inputRef = useRef();
const handleClick = () => {
inputRef.current.focus(); // 调用子组件方法
};
return (
<>
<ForwardedFancyInput ref={inputRef} />
<button onClick={handleClick}>聚焦输入框</button>
</>
);
}
注意事项:
1、不要在渲染期间写入ref
// ❌ 错误写法
function MyComponent() {
const myRef = useRef();
myRef.current = 123; // 渲染期间修改ref
return ...;
}
// ✅ 正确写法
function MyComponent() {
const myRef = useRef(123); // 初始化时设置
useEffect(() => {
myRef.current = 456; // 在effect中修改
}, []);
return ...;
}
2、与 useEffect 配合使用时注意时序:
useEffect(() => {
// 这里的ref.current可能不是最新的
}, []);
useEffect(() => {
// 更好的做法是在依赖数组中包含ref.current
}, [ref.current]);
6、useMemo
性能优化、缓存计算结果
useMemo 是 React 提供的一个性能优化 Hook,用于缓存计算结果,避免在每次渲染时都进行高开销的计算
基本语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
参数说明:
- 计算函数:返回需要缓存的值
- 依赖数组:只有依赖变化的时候,才会重新计算值
常见场景:
1、复杂计算优化
const sortedList = useMemo(() => {
return largeList.sort((a, b) => a.value - b.value);
}, [largeList]);
2、避免子组件不必要的渲染
const childProps = useMemo(() => ({ value: props.value }), [props.value]);
return <Child {...childProps} />;
3、引用类型稳定性
const config = useMemo(() => ({
color: theme === 'dark' ? 'white' : 'black',
size: 'large'
}), [theme]);
错误写法:
1、缺少依赖项
const badExample = useMemo(() => a + b, [a]); // 缺少b依赖
2、副作用操作
// 不应该在useMemo中执行副作用
useMemo(() => {
fetchData(); // ❌ 副作用应放在useEffect中
}, []);
3、总是重新计算
// 空依赖数组表示只计算一次
const once = useMemo(() => compute(a), []);
// 如果a可能变化,这会导致使用过期值
注意:
- 所有回调函数内部用到的变量都必须包含在依赖数组中
- 故意省略依赖是危险的,会导致缓存失效或使用过期值
- 如果发现需要省略某些依赖,通常意味着代码结构需要重构
7、useCallback
缓存函数,避免不必要的重新渲染 从而优化性能
useCallback 会返回一个记忆化(memoized)的回调函数,只有当依赖项发生变化时才会重新创建函数。它的主要用途是:
- 避免子组件不必要的重新渲染(配合 React.memo 使用)
- 保持函数引用稳定,避免作为依赖项触发不必要的 effect 执行
- 优化性能敏感的场景(如高频触发的事件处理)
基本语法:
const memoizedCallback = useCallback(
() => {
// 函数逻辑
},
[dep1, dep2], // 依赖项数组
);
典型使用场景:
1、避免子组件不必要的重新渲染
// 子组件
const Child = React.memo(function Child({ onClick }) {
console.log('Child 渲染');
return <button onClick={onClick}>点击</button>;
});
// 父组件
function Parent() {
const [count, setCount] = useState(0);
// 使用useCallback缓存函数
const handleClick = useCallback(() => {
console.log('点击处理');
}, []); // 空依赖表示函数永远不会改变
return (
<div>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>计数: {count}</button>
</div>
);
}
效果:当父组件状态更新时,Child 组件不会重新渲染,因为 onClick 的引用保持不变。
2、作为其他Hooks的依赖项
function Example({ id }) {
const [data, setData] = useState(null);
// 使用useCallback确保fetchData引用稳定
const fetchData = useCallback(async () => {
const result = await fetch(`/api/data/${id}`);
setData(await result.json());
}, [id]); // id变化时才重新创建函数
useEffect(() => {
fetchData();
}, [fetchData]); // 安全地将函数作为依赖
return <div>{data ? data.name : '加载中...'}</div>;
}
3、高频事件
function ScrollHandler() {
const handleScroll = useCallback(() => {
console.log('滚动位置:', window.scrollY);
}, []);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
return <div style={{ height: '200vh' }}>滚动测试</div>;
}
与普通函数区别:
特性 | 普通函数 | useCallback 记忆化函数 |
---|---|---|
创建时机 | 每次渲染都重新创建 | 依赖变化时才重新创建 |
引用稳定性 | 每次渲染引用不同 | 依赖不变时引用相同 |
性能影响 | 可能导致子组件不必要渲染 | 减少不必要的子组件渲染 |
适用场景 | 简单场景 | 作为prop传递或hook依赖时使用 |
注意事项:
- 不要过度使用:简单组件不需要使用useCallback,过早优化可能适得其反
- 正确设置依赖:遗漏依赖可能导致闭包问题(获取过时的值)
- 与useMemo区别:useCallback是缓存函数本身,useMemo是缓存计算结果
- 配合 React.memo 使用:单独使用 useCallback 而没有 React.memo 通常不会带来性能提升
替代写法比较:
直接在effect中定义函数(不推荐)
useEffect(() => {
const fetchData = async () => {
const result = await fetch(`/api/data/${id}`);
setData(await result.json());
};
fetchData();
}, [id]); // 需要禁用eslint警告
问题:
每次渲染都会创建新函数(尽管可能不会执行)
如果函数体复杂,可能造成不必要的性能开销
写法2:使用 useCallback + useEffect(推荐)
const fetchData = useCallback(async () => {
// ...
}, [id]);
useEffect(() => {
fetchData();
}, [fetchData]);
优点:
函数只在必要时重新创建
依赖关系明确
适合复杂函数逻辑
关键点:
useEffect 执行规则总结表:
依赖项情况 | 注册时机 | 注册时机 | 典型用途 |
---|---|---|---|
无依赖数组 | 每次渲染都重新注册 | 每次渲染后都执行 | 极少使用(通常需要lint禁用) |
[] 空数组 | 每次渲染都重新注 | 仅首次渲染后执行(挂载时) | 组件挂载时的初始化操作 |
[dep] 有依赖 | 每次渲染都重新注册 | 首次渲染 + 依赖项变化时执行 | 数据获取、订阅管理等 |
这里的重点: 注册 ≠ 执行,每次渲染都会注册(可以理解为新回调函数放入React的待处理列表),但是是否“执行”取决于依赖项是否变化
三、自定义Hooks
8、自定义 Hook 示例
作用:复用状态逻辑
import { useState, useEffect } from 'react';
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
function MyComponent() {
const width = useWindowWidth();
return <div>Window width: {width}</div>;
}
四、其他Hooks
9、useLayoutEffect
作用:与 useEffect 类似,但在DOM更新后同步触发、而不是异步执行
import { useLayoutEffect } from 'react';
function MeasureExample() {
const [size, setSize] = useState({});
const divRef = useRef();
useLayoutEffect(() => {
setSize(divRef.current.getBoundingClientRect());
}, []);
return <div ref={divRef}>{JSON.stringify(size)}</div>;
}
与 useEffect 的区别
1、执行时机
- useLayoutEffect:在 DOM 更新后,浏览器绘制前同步执行
- useEffect:在 DOM 更新后,浏览器绘制后异步执行
2、使用场景
- useLayoutEffect:当你需要读取或修改 DOM 布局,并且不希望用户看到闪烁时使用
- useEffect:适用于大多数副作用场景,如数据获取、订阅等
场景案例
1、测量DOM元素
function MeasureExample() {
const [size, setSize] = useState({});
const ref = useRef();
useLayoutEffect(() => {
setSize(ref.current.getBoundingClientRect());
}, []);
return <div ref={ref}>{JSON.stringify(size)}</div>;
}
2、同步更新 DOM
function Tooltip({ children, targetRef }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
const targetRect = targetRef.current.getBoundingClientRect();
setPosition({
top: targetRect.bottom,
left: targetRect.left
});
}, [targetRef]);
return (
<div style={{ position: 'fixed', ...position }}>
{children}
</div>
);
}
3、防止闪烁:当你需要修改 DOM 样式或布局时,使用 useLayoutEffect 可以确保在浏览器绘制前完成,避免用户看到中间状态。
注意事项:
1、性能影响:由于 useLayoutEffect 是同步执行的,复杂的逻辑可能会阻塞浏览器渲染。所以,只在必要时使用,大多数情况下应优先使用 useEffect
2、服务器渲染 (SSR):在服务器端渲染时,useLayoutEffect 会导致警告,因为它不能在服务器端执行,如果需要在 SSR 中使用,可以考虑使用 useEffect 或动态加载组件
3、执行顺序:组件内多个 useLayoutEffect 按声明顺序执行,清理函数按相反顺序执行
4、将 DOM 读取和写入操作放在同一个 useLayoutEffect 中,避免不必要的重排
5、避免在 useLayoutEffect 中执行耗时操作
备注:useLayoutEffect 的清理和effect清理一样
清理时机:
组件卸载时,react会清理函数
依赖项变化时候,React 会先执行上一次的清理函数
11、useId
useId 是一个用于生成唯一 ID 的 Hook,主要用于解决服务端渲染(SSR)和客户端渲染(CSR)之间的 ID 不一致问题
import { useId } from 'react';
function Form() {
const id = useId(); // 生成唯一的 ID(如 ":r1:")
return (
<>
<label htmlFor={id}>Username</label>
<input id={id} type="text" />
</>
);
}
12、useDeferredValue
useDeferredValue 是一个用于延迟更新低优先级状态的 Hook,类似于防抖(debounce)或节流(throttle),但更智能。
使用场景
- 优化输入框联想搜索(用户输入时立即显示输入内容,联想结果稍后更新)
- 非紧急的渲染(如大型列表、图表等)
示例
import { useState, useDeferredValue } from 'react';
function Search() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query); // 延迟的 query
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{/* 延迟渲染结果,避免阻塞输入 */}
<Results query={deferredQuery} />
</>
);
}
特点:
- 延迟的值会“滞后”于原始值,但 React 会优先处理高优先级更新(如用户输入)
- 与 useTransition 类似,但更适用于直接延迟某个状态值,而非标记整个更新过程
与Transition详细区别:
1、useDeferredValue
作用:
- 延迟一个状态的更新,使其不阻塞高优先级的渲染(如用户输入
- 本质是对值的延迟,React 会在后台先处理高优先级更新,再处理这个延迟的值
适用场景:
- 当需要延迟派生状态(如根据输入过滤列表)时
- 希望保持 UI 响应性(如输入框即时响应,结果稍后显示)
示例
function Search() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query); // 延迟的 query
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{/* 结果延迟更新 */}
<Results query={deferredQuery} />
</>
);
}
特点:
- 被动触发:依赖其他状态的变化(如 query 变化后延迟更新 deferredQuery)
- 针对单个值:直接延迟某个状态值,不控制更新过程
2、useTransition
作用:
- 标记一个更新为低优先级,允许 React 中断其渲染以处理更紧急的任务(如用户交互)
- 本质是对更新过程的控制,可以手动决定哪些更新应该被延迟。
适用场景:
- 当需要手动控制非紧急更新(如页面导航、批量操作)时
- 需要显示加载状态(如切换页面时的加载动画)
示例:
function App() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState("home");
function switchTab(nextTab) {
startTransition(() => {
setTab(nextTab); // 低优先级更新
});
}
return (
<>
<button onClick={() => switchTab("home")}>Home</button>
<button onClick={() => switchTab("settings")}>Settings</button>
{isPending && <Spinner />} {/* 过渡期间的加载状态 */}
<Content tab={tab} />
</>
);
}
特点:
- 主动触发:通过 startTransition 显式包裹低优先级更新。
- 控制更新过程:可以管理多个状态的更新,并获取过渡状态(isPending)
- 更灵活:适合复杂场景(如路由切换、批量状态更新)
核心区别对比
特性 | useDeferredValue | useTransition |
---|---|---|
用途 | 延迟某个值的更新 | 延迟一组状态的更新 |
触发方式 | 被动(自动延迟派生值) | 主动(手动包裹低优先级更新) |
返回值 | 延迟后的值 | [isPending, startTransition] |
适用场景 | 输入联想、派生状态优化 | 页面导航、批量操作、需要加载状态的场景 |
性能优化角度 | 对值的优化 | 对更新过程的优化 |
结合使用的例子
function HybridExample() {
const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();
const deferredQuery = useDeferredValue(query);
function handleInput(e) {
setQuery(e.target.value); // 高优先级更新(输入框即时响应)
startTransition(() => {
// 低优先级更新(如联想搜索)
// 可以在这里处理其他非紧急状态
});
}
return (
<>
<input value={query} onChange={handleInput} />
{isPending && <Spinner />}
<Results query={deferredQuery} />
</>
);
}