状态派生
一、useMemo
useMemo
是 React 提供的一个性能优化 Hook。它的主要功能是避免在每次渲染时执行复杂的计算和对象重建。通过记忆上一次的计算结果,仅当依赖项变化时才会重新计算,提高了性能,有点类似于 Vue 的computed
。
1.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
的效果会大打折扣
2.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 用于优化性能,返回一个记忆化的回调函数,可以减少不必要的重新渲染,也就是说它是用于缓存组件内的函数,避免函数的重复创建。
1.为什么需要 useCallback
在 React 中,函数组件的重新渲染会导致组件内的函数被重新创建,这可能会导致性能问题。useCallback 通过缓存函数,可以减少不必要的重新渲染,提高性能。
2.用法
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
3.参数
入参
- callback:回调函数
- deps:依赖项数组,当依赖项发生变化时,回调函数会被重新创建,跟 useEffect 一样。
返回值
- 返回一个记忆化的回调函数,可以减少函数的创建次数,提高性能。
和 useMemo 的区别
useMemo
用于 缓存计算结果,避免在每次渲染时重复计算。useCallback
用于 缓存函数,避免在组件重新渲染时创建新的函数实例(函数引用不变)。
4.案例 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);
}, []);
5.案例 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 执行了');
}, []);
6.总结
useCallback 的使用需要有所节制,不要盲目地对每个方法应用 useCallback,这样做可能会导致不必要的性能损失。useCallback 本身也需要一定的性能开销。
useCallback 并不是为了阻止函数的重新创建,而是通过依赖项来决定是否返回新的函数或旧的函数,从而在依赖项不变的情况下确保函数的地址不变。
三、useDebugValue
useDebugValue
是一个专为开发者调试自定义 Hook 而设计的 React Hook。它允许你在 React 开发者工具中为自定义 Hook 添加自定义的调试值。
1.用法
const debugValue = useDebugValue(value);
2.参数说明
入参
value
: 要在 React DevTools 中显示的值- ts
formatter?
: (可选) 格式化函数
- 作用:自定义值的显示格式
- 调用时机:仅在 React DevTools 打开时才会调用,可以进行复杂的格式化操作
- 参数:接收 value 作为参数
- 返回:返回格式化后的显示值
3.返回值
- 无返回值(void)
4.获取 React DevTools
1.Chrome 商店安装
- 访问 React Developer Tools
- 点击"添加至 Chrome"即可安装
2.在微信公众号(小满zs
) 回复 React/React工具
获取安装包
离线安装步骤
- 打开 Chrome 浏览器,点击右上角三个点 → 更多工具 → 扩展程序
- 开启右上角的"开发者模式"
- 将下载的 .crx 文件直接拖拽到扩展程序页面
- 在弹出的确认框中点击"添加扩展程序"
5.实战案例:自定义 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 中的显示效果:
6.使用建议
- 仅在自定义 Hook 中使用
useDebugValue
- 对于简单的值,可以省略 formatter 函数
- 当格式化值的计算比较昂贵时,建议使用 formatter 函数,因为它只在开发者工具打开时才会执行
四、useId
useId 是 React 18 新增的一个 Hook,用于生成稳定的唯一标识符,主要用于解决 SSR 场景下的 ID 不一致问题,或者需要为组件生成唯一 ID 的场景。
1.使用场景
- 为组件生成唯一 ID
- 解决 SSR 场景下的 ID 不一致问题
- 无障碍交互唯一 ID
2.用法
const id = useId();
// 返回值: :r0: 多次调用值递增
3.参数说明
入参
- 无入参
4.返回值
- 唯一标识符 例如
:r0:
5.案例
为组件生成唯一 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" />
</>
);
};
解决 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>
);
};
无障碍交互唯一 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>
);
};
6.总结
基本介绍
useId 是 React 18 引入的新 Hook,用于生成稳定且唯一的标识符
使用特点
- 无需传入参数
- 返回确定性的唯一字符串(如
:r0:
) - 同一组件多次调用会生成递增的 ID
- 适合在需要稳定 ID 的场景下使用,而不是用于视觉或样式目的
最佳实践
- 当需要多个相关 ID 时,应该使用同一个 useId 调用,并添加后缀
- 不要用于列表渲染的 key 属性
- 优先用于可访问性和 SSR 场景
[!CAUTION]
本文内容参考小满大佬