状态派生
useMemo
useMemo 是 React 提供的一个性能优化 Hook。它的主要功能是避免在每次渲染时执行复杂的计算和对象重建。通过记忆上一次的计算结果,仅当依赖项变化时才会重新计算,提高了性能,有点类似于Vue的computed。
React.memo
React.memo 是一个 React API,用于优化性能。它通过记忆上一次的渲染结果,仅当 props 发生变化时才会重新渲染, 避免重新渲染。
用法
使用 React.memo 包裹组件[一般用于子组件],可以避免组件重新渲染。
import React, { memo } from 'react';
const MyComponent = React.memo(({ prop1, prop2 }) => {
// 组件逻辑
});
const App = () => {
return <MyComponent prop1="value1" prop2="value2" />;
};
React.memo 案例
首先明确 React 组件的渲染条件:
- 组件的 props 发生变化
- 组件的 state 发生变化
- useContext 发生变化
我们来看下面这个例子,这个例子没有使用 memo 进行缓存,所以每次父组件的 state 发生变化,子组件都会重新渲染。
而我们的子组件只用到了 user 的信息,但是父组件每次 search 发生变化,子组件也会重新渲染, 这样就就造成了没必要的渲染所以我们使用
memo缓存。
import React, { useMemo, useState } from 'react';
interface User {
name: string;
age: number;
email: string;
}
interface CardProps {
user: User;
}
const Card = function ({ user }: CardProps) {
const Card = React.memo(function ({ user }: CardProps) {
console.log('Card render'); // 每次父组件的 state 发生变化,子组件都会重新渲染
const styles = {
backgroundColor: 'lightblue',
padding: '20px',
borderRadius: '10px',
margin: '10px'
}
return <div style={styles}>
<h1>{user.name}</h1>
<p>{user.age}</p>
<p>{user.email}</p>
</div>
}
})
function App() {
const [users, setUsers] = useState<User>({
name: '张三',
age: 18,
email: 'zhangsan@example.com'
});
const [search, setSearch] = useState('');
return (
<div>
<h1>父组件</h1>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<Card user={users} />
</div>
);
}
export default App;
当我们使用 memo 缓存后,只有 user 发生变化时,子组件才会重新渲染, 而 search 发生变化时,子组件不会重新渲染。
import React, { useMemo, useState } from 'react';
interface User {
name: string;
age: number;
email: string;
}
interface CardProps {
user: User;
}
const Card = React.memo(function ({ user }: CardProps) {
// 只有 user 发生变化时,子组件才会重新渲染
console.log('Card render');
const styles = {
backgroundColor: 'lightblue',
padding: '20px',
borderRadius: '10px',
margin: '10px'
}
return <div style={styles}>
<h1>{user.name}</h1>
<p>{user.age}</p>
<p>{user.email}</p>
</div>
})
function App() {
const [users, setUsers] = useState<User>({
name: '张三',
age: 18,
email: 'zhangsan@example.com'
});
const [search, setSearch] = useState('');
return (
<div>
<h1>父组件</h1>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
<div>
<button onClick={() => setUsers({
name: '李四',
age: Math.random() * 100,
email: 'lisi@example.com'
})}>更新user</button>
</div>
<Card user={users} />
</div>
);
}
export default App;
React.memo 总结
-
使用场景:
-
当子组件接收的 props 不经常变化时
-
当组件重新渲染的开销较大时
-
当需要避免不必要的渲染时
-
-
优点:
- 通过记忆化避免不必要的重新渲染
- 提高应用性能
- 减少资源消耗
-
注意事项:
- 不要过度使用,只在确实需要优化的组件上使用
- 对于简单的组件,使用
memo的开销可能比重新渲染还大 - 如果 props 经常变化,
memo的效果会大打折扣
useMemo 用法
参数
入参
- 回调函数:Function:返回需要缓存的值
- 依赖项:Array:依赖项发生变化时,回调函数会重新执行
(执行时机跟useEffect类似)
返回值
- 返回值:返回需要缓存的值
(返回之后就不是函数了)
useMemo 案例
我们来看下面这个例子,这个例子没有使用
useMemo进行缓存,所以每次 search 发生变化,total都会重新计算,这样就造成了没必要的计算所以我们可以使用useMemo缓存,因为我们的total跟search没有关系,那么如果计算的逻辑比较复杂,就造成了性能问题。
import React, { useMemo, useState } from 'react';
function App() {
const [search, setSearch] = useState('');
const [goods, setGoods] = useState([
{ id: 1, name: '苹果', price: 10, count: 1 },
{ id: 2, name: '香蕉', price: 20, count: 1 },
{ id: 3, name: '橘子', price: 30, count: 1 },
]);
const handleAdd = (id: number) => {
setGoods(goods.map(item => item.id === id ? { ...item, count: item.count + 1 } : item));
}
const handleSub = (id: number) => {
setGoods(goods.map(item => item.id === id ? { ...item, count: item.count - 1 } : item));
}
const total = () => {
console.log('total'); // 此时只要input发生了改变都会进入到这个函数,影响性能
//例如很复杂的计算逻辑
return goods.reduce((total, item) => total + item.price * item.count, 0)
}
return (
<div>
<h1>父组件</h1>
<input type="text" value={search} onChange={(e) => setSearch(e.target.value)} />
<table border={1} cellPadding={5} cellSpacing={0}>
<thead>
<tr>
<th>商品名称</th>
<th>商品价格</th>
<th>商品数量</th>
</tr>
</thead>
<tbody>
{goods.map(item => <tr key={item.id}>
<td>{item.name}</td>
<td>{item.price * item.count}</td>
<td>
<button onClick={() => handleAdd(item.id)}>+</button>
<span>{item.count}</span>
<button onClick={() => handleSub(item.id)}>-</button>
</td>
</tr>)}
</tbody>
</table>
<h2>总价:{total()}</h2>
</div>
);
}
export default App;
当我们使用
useMemo缓存后,只有 goods 发生变化时,total才会重新计算, 而 search 发生变化时,total不会重新计算
import React, { useMemo, useState } from 'react';
function App() {
const [search, setSearch] = useState('');
const [goods, setGoods] = useState([
{ id: 1, name: '苹果', price: 10, count: 1 },
{ id: 2, name: '香蕉', price: 20, count: 1 },
{ id: 3, name: '橘子', price: 30, count: 1 },
]);
const handleAdd = (id: number) => {
setGoods(goods.map(item => item.id === id ? { ...item, count: item.count + 1 } : item));
}
const handleSub = (id: number) => {
setGoods(goods.map(item => item.id === id ? { ...item, count: item.count - 1 } : item));
}
const total = useMemo(() => {
// 只有当goods改变才会进入到这个函数
console.log('total');
return goods.reduce((total, item) => total + item.price * item.count, 0)
}, [goods]);
return (
<div>
<h1>父组件</h1>
<input type="text" value={search} onChange={(e) => setSearch(e.target.value)} />
<table border={1} cellPadding={5} cellSpacing={0}>
<thead>
<tr>
<th>商品名称</th>
<th>商品价格</th>
<th>商品数量</th>
</tr>
</thead>
<tbody>
{goods.map(item => <tr key={item.id}>
<td>{item.name}</td>
<td>{item.price * item.count}</td>
<td>
<button onClick={() => handleAdd(item.id)}>+</button>
<span>{item.count}</span>
<button onClick={() => handleSub(item.id)}>-</button>
</td>
</tr>)}
</tbody>
</table>
<h2>总价:{total}</h2>
</div>
);
}
export default App;
useMemo 执行时机(依赖项)
- 如果依赖项是个空数组,那么
useMemo的回调函数会执行一次 - 指定依赖项,当依赖项发生变化时,
useMemo的回调函数会执行 - 不指定依赖项,不推荐这么用,因为每次渲染和更新都会执行
useMemo 总结
- 使用场景:
- 当需要缓存复杂计算结果时
- 当需要避免不必要的重新计算时
- 当计算逻辑复杂且耗时时
- 优点:
- 通过记忆化避免不必要的重新计算
- 提高应用性能
- 减少资源消耗
- 注意事项:
- 不要过度使用,只在确实需要优化的组件上使用
- 如果依赖项经常变化,useMemo 的效果会大打折扣
- 如果计算逻辑简单,使用 useMemo 的开销可能比重新计算还大
useCallback
useCallback 用于优化性能,返回一个记忆化的回调函数,可以减少不必要的重新渲染,也就是说它是用于缓存组件内的函数,避免函数的重复创建。
为什么需要useCallback
在React中,函数组件的重新渲染会导致组件内的函数被重新创建,这可能会导致性能问题。useCallback 通过缓存函数,可以减少不必要的重新渲染,提高性能。
用法
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
参数
入参
- callback:回调函数
- deps:依赖项数组,当依赖项发生变化时,回调函数会被重新创建,跟useEffect一样。
返回值
- 返回一个记忆化的回调函数,可以减少函数的创建次数,提高性能。
和useMemo的区别
useMemo用于 缓存计算结果,避免在每次渲染时重复计算。useCallback用于 缓存函数,避免在组件重新渲染时创建新的函数实例(函数引用不变)。
案例1
来看这个实例:
- 我们创建了一个WeakMap(用Map也行),用于存储回调函数,并记录回调函数的创建次数。
- 在组件重新渲染时,changeSearch 函数会被重新创建,我们这边会进行验证,如果函数被重新创建了数量会+1,如果没有重新创建,数量默认是1。
import { useCallback, useState } from 'react'
const functionMap = new WeakMap()
let counter = 1
const App: React.FC = () => {
console.log('Render App')
const [search, setSearch] = useState('')
const changeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
if(!functionMap.has(changeSearch)) {
functionMap.set(changeSearch, counter++)
}
console.log('函数Id', functionMap.get(changeSearch))
return <>
<input type="text" value={search} onChange={changeSearch} />
</>;
};
export default App;
我们更改输入框的值,可以看到函数Id在增加,说明函数被重新创建了。
为什么是4呢,因为默认是1,然后输入框更改了3次,所以是4,那么这样好吗?我们使用useCallback来优化一下。
只需要在changeSearch函数上使用useCallback,就可以优化性能。
const changeSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}, [])
案例2
应用于子组件:
- 我们创建了一个Child子组件,并使用React.memo进行优化,memo在上一章讲过了,他会检测props是否发生变化,如果发生变化,就会重新渲染子组件。
- 我们创建了一个childCallback函数,传递给子组件,然后我们输入框更改值,发现子组件居然重新渲染了,但是我们并没有更改props,这是为什么呢?
- 这是因为输入框的值发生变化,App就会重新渲染,然后childCallback函数就会被重新创建,然后传递给子组件,子组件会判断这个函数是否发生变化,但是每次创建的函数内存地址都不一样,所以子组件会重新渲染。
import React, { useCallback, useState } from 'react'
const Child = React.memo(({ user, callback }: { user: { name: string; age: number }, callback: () => void }) => {
console.log('Render Child')
const styles = {
color: 'red',
fontSize: '20px',
}
return <div style={styles}>
<div>{user.name}</div>
<div>{user.age}</div>
<button onClick={callback}>callback</button>
</div>
})
const App: React.FC = () => {
const [search, setSearch] = useState('')
const [user, setUser] = useState({
name: 'John',
age: 20
})
const childCallback = () => {
console.log('callback 执行了')
}
return <>
<input type="text" value={search} onChange={e => setSearch(e.target.value)} />
<Child callback={childCallback} user={user} />
</>;
};
export default App;
因为App重新渲染了,所以childCallback函数会被重新创建,然后传递给子组件,子组件会判断这个函数是否发生变化,但是每次创建的函数内存地址都不一样,所以子组件会重新渲染。
只需要在childCallback函数上使用useCallback,就可以优化性能。
const childCallback = useCallback(() => {
console.log('callback 执行了')
}, [])
总结
useCallback的使用需要有所节制,不要盲目地对每个方法应用useCallback,这样做可能会导致不必要的性能损失。useCallback本身也需要一定的性能开销。
useCallback并不是为了阻止函数的重新创建,而是通过依赖项来决定是否返回新的函数或旧的函数,从而在依赖项不变的情况下确保函数的地址不变。
工具Hooks
useDebugValue
useDebugValue 是一个专为开发者调试自定义 Hook 而设计的 React Hook。它允许你在 React 开发者工具中为自定义 Hook 添加自定义的调试值。
用法
const debugValue = useDebugValue(value)
参数说明
入参
-
value: 要在 React DevTools 中显示的值 -
formatter?: (可选) 格式化函数
- 作用:自定义值的显示格式
- 调用时机:仅在 React DevTools 打开时才会调用,可以进行复杂的格式化操作
- 参数:接收 value 作为参数
- 返回:返回格式化后的显示值
返回值
- 无返回值(void)
获取 React DevTools
1.Chrome 商店安装
- 访问 React Developer Tools
- 点击"添加至 Chrome"即可安装
离线安装步骤
- 打开 Chrome 浏览器,点击右上角三个点 → 更多工具 → 扩展程序
- 开启右上角的"开发者模式"
- 将下载的 .crx 文件直接拖拽到扩展程序页面
- 在弹出的确认框中点击"添加扩展程序"
实战案例:自定义 useCookie Hook
下面通过实现一个 useCookie Hook 来展示 useDebugValue 的实际应用。这个 Hook 提供了完整的 cookie 操作功能,并通过 useDebugValue 来增强调试体验。
import React, { useState, useDebugValue } from 'react';
/**
* 自定义 Hook,用于管理浏览器的 cookie。
* @param {string} name - cookie 的名称。
* @param {string} [initialValue=''] - cookie 的初始值,默认为空字符串。
* @returns {[string, (value: string, options?: any) => void, () => void]} - 返回一个数组,包含当前 cookie 的值、更新 cookie 的函数和删除 cookie 的函数。
*/
const useCookie = (name: string, initialValue: string = '') => {
const getCookie = () => {
// 使用正则表达式匹配 cookie 字符串中指定名称的值
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]*)(;|$)`))
return match ? match[2] : initialValue
}
const [cookie, setCookie] = useState(getCookie())
/**
* 更新指定名称的 cookie 值。
* @param {string} value - 要设置的新的 cookie 值。
* @param {any} [options] - 可选的 cookie 选项,如过期时间、路径等。
*/
const updateCookie = (value: string, options?: any) => {
// 设置新的 cookie 值
document.cookie = `${name}=${value};${options}`
// 更新状态中的 cookie 值
setCookie(value)
}
/**
* 删除指定名称的 cookie。
*/
const deleteCookie = () => {
// 通过设置过期时间为过去的时间来删除 cookie
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`
// 将状态中的 cookie 值重置为初始值
setCookie(initialValue)
}
/**
* 使用 useDebugValue Hook 在 React DevTools 中显示调试信息。
* 这里将 cookie 的值格式化为 "cookie: {value}" 的形式。
*/
useDebugValue(cookie, (value) => {
return `cookie: ${value}`
})
return [cookie, updateCookie, deleteCookie] as const
}
/**
* 主应用组件,演示如何使用 useCookie Hook 管理 cookie。
* @returns {JSX.Element} - 返回一个包含显示 cookie 值和操作按钮的 JSX 元素。
*/
const App: React.FC = () => {
const [cookie, updateCookie, deleteCookie] = useCookie('key', 'value')
return (
<div>
<div>{cookie}</div>
<button onClick={() => { updateCookie('update-value') }}>设置cookie</button>
<button onClick={() => { deleteCookie() }}>删除cookie</button>
</div>
);
}
export default App;
Hook 功能说明
- getCookie: 获取指定名称的 cookie 值
- updateCookie: 更新或创建新的 cookie
- deleteCookie: 删除指定的 cookie
useDebugValue 的应用
在这个例子中,我们使用 useDebugValue 来显示当前 cookie 的值:
useDebugValue(cookie, (value) => `cookie: ${value}`)
调试效果展示
在 React DevTools 中的显示效果:
使用建议
- 仅在自定义 Hook 中使用
useDebugValue - 对于简单的值,可以省略 formatter 函数
- 当格式化值的计算比较昂贵时,建议使用 formatter 函数,因为它只在开发者工具打开时才会执行
useId
useId 是 React 18 新增的一个 Hook,用于生成稳定的唯一标识符,主要用于解决 SSR 场景下的 ID 不一致问题,或者需要为组件生成唯一 ID 的场景。
使用场景
- 为组件生成唯一 ID
- 解决 SSR 场景下的 ID 不一致问题
- 无障碍交互唯一ID
用法
const id = useId()
// 返回值: :r0: 多次调用值递增
参数说明
入参
- 无入参
返回值
- 唯一标识符 例如
:r0:
案例
1.为组件生成唯一 ID
比如表单元素,label 需要和 input 绑定,如果使用 id 属性,需要手动生成唯一 ID,使用 useId 可以自动生成唯一 ID,这就非常方便。
/**
* App 组件,创建一个带标签的输入框,使用 useId 生成唯一的 ID 以关联标签和输入框。
* @returns {JSX.Element} 返回一个包含标签和输入框的 JSX 元素。
*/
export const App = () => {
// 使用 useId 钩子生成一个唯一的 ID,用于关联标签和输入框
const id = useId()
return (
<>
{/* 使用生成的唯一 ID 关联标签和输入框,提升可访问性 */}
<label htmlFor={id}>Name</label>
{/* 为输入框设置唯一的 ID,与标签关联 */}
<input id={id} type="text" />
</>
)
}
2. 解决 SSR 场景下的 ID 不一致问题
在服务端渲染(SSR)场景下,组件会在服务端和客户端分别渲染一次。如果使用随机生成的 ID,可能会导致两端渲染结果不一致,引发 hydration 错误。useId 可以确保生成确定性的 ID。
// 一个常见的 SSR 场景:带有工具提示的导航栏组件
const NavItem = ({ text, tooltip }) => {
// ❌ 错误做法:使用随机值或递增值
const randomId = `tooltip-${Math.random()}`
// 在 SSR 时服务端可能生成 tooltip-0.123
// 在客户端可能生成 tooltip-0.456
// 导致 hydration 不匹配
return (
<li>
<a
aria-describedby={randomId}
href="#"
>
{text}
</a>
<div id={randomId} role="tooltip">
{tooltip}
</div>
</li>
)
}
// ✅ 正确做法:使用 useId
const NavItemWithId = ({ text, tooltip }) => {
const id = useId()
const tooltipId = `${id}-tooltip`
return (
<li>
<a
href="#"
aria-describedby={tooltipId}
className="nav-link"
>
{text}
</a>
<div
id={tooltipId}
role="tooltip"
className="tooltip"
>
{tooltip}
</div>
</li>
)
}
// 使用示例
const Navigation = () => {
return (
<nav>
<ul>
<NavItemWithId
text="首页"
tooltip="返回首页"
/>
<NavItemWithId
text="设置"
tooltip="系统设置"
/>
<NavItemWithId
text="个人中心"
tooltip="查看个人信息"
/>
</ul>
</nav>
)
}
3. 无障碍交互唯一ID
aria-describedby 是一个 ARIA 属性,用于为元素提供额外的描述性文本。它通过引用其他元素的 ID 来关联描述内容,帮助屏幕阅读器为用户提供更详细的信息。
当视障用户使用屏幕阅读器浏览网页时:
- 读到输入框时会先读出输入框的标签
- 然后会读出通过
aria-describedby关联的描述文本 - 用户就能知道这个输入框需要输入什么内容,有什么要求
export const App = () => {
const id = useId()
return (
<div>
<input
type="text"
aria-describedby={id}
/>
<p id={id}>
请输入有效的电子邮件地址,例如:xiaoman@example.com
</p>
</div>
)
}
总结
基本介绍
useId 是 React 18 引入的新 Hook,用于生成稳定且唯一的标识符
使用特点
- 无需传入参数
- 返回确定性的唯一字符串(如
:r0:) - 同一组件多次调用会生成递增的 ID
- 适合在需要稳定 ID 的场景下使用,而不是用于视觉或样式目的
最佳实践
- 当需要多个相关 ID 时,应该使用同一个 useId 调用,并添加后缀
- 不要用于列表渲染的 key 属性
- 优先用于可访问性和 SSR 场景
[!CAUTION]
本文内容参考小满大佬