React常见Hooks使用(三)

210 阅读13分钟

状态派生

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 组件的渲染条件:

  1. 组件的 props 发生变化
  2. 组件的 state 发生变化
  3. 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 总结
  1. 使用场景

    • 当子组件接收的 props 不经常变化时

    • 当组件重新渲染的开销较大时

    • 当需要避免不必要的渲染时

  2. 优点

    • 通过记忆化避免不必要的重新渲染
    • 提高应用性能
    • 减少资源消耗
  3. 注意事项

    • 不要过度使用,只在确实需要优化的组件上使用
    • 对于简单的组件,使用 memo 的开销可能比重新渲染还大
    • 如果 props 经常变化, memo 的效果会大打折扣

useMemo 用法

参数

入参

  • 回调函数:Function:返回需要缓存的值
  • 依赖项:Array:依赖项发生变化时,回调函数会重新执行(执行时机跟useEffect类似)

返回值

  • 返回值:返回需要缓存的值(返回之后就不是函数了)
useMemo 案例

我们来看下面这个例子,这个例子没有使用 useMemo 进行缓存,所以每次 search 发生变化, total 都会重新计算,这样就造成了没必要的计算所以我们可以使用 useMemo 缓存,因为我们的 totalsearch 没有关系,那么如果计算的逻辑比较复杂,就造成了性能问题。

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 执行时机(依赖项)
  1. 如果依赖项是个空数组,那么 useMemo 的回调函数会执行一次
  2. 指定依赖项,当依赖项发生变化时, useMemo 的回调函数会执行
  3. 不指定依赖项,不推荐这么用,因为每次渲染和更新都会执行
useMemo 总结
  1. 使用场景
    • 当需要缓存复杂计算结果时
    • 当需要避免不必要的重新计算时
    • 当计算逻辑复杂且耗时时
  2. 优点
    • 通过记忆化避免不必要的重新计算
    • 提高应用性能
    • 减少资源消耗
  3. 注意事项
    • 不要过度使用,只在确实需要优化的组件上使用
    • 如果依赖项经常变化,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在增加,说明函数被重新创建了。

usecallback1.png

为什么是4呢,因为默认是1,然后输入框更改了3次,所以是4,那么这样好吗?我们使用useCallback来优化一下。

只需要在changeSearch函数上使用useCallback,就可以优化性能。

const changeSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value)
}, [])

usecallback2.png

案例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函数会被重新创建,然后传递给子组件,子组件会判断这个函数是否发生变化,但是每次创建的函数内存地址都不一样,所以子组件会重新渲染。

useCallback-3.Bd3ynv-p.png

只需要在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 商店安装

  1. 访问 React Developer Tools
  2. 点击"添加至 Chrome"即可安装

离线安装步骤

  1. 打开 Chrome 浏览器,点击右上角三个点 → 更多工具 → 扩展程序
  2. 开启右上角的"开发者模式"
  3. 将下载的 .crx 文件直接拖拽到扩展程序页面
  4. 在弹出的确认框中点击"添加扩展程序"

实战案例:自定义 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 功能说明
  1. getCookie: 获取指定名称的 cookie 值
  2. updateCookie: 更新或创建新的 cookie
  3. deleteCookie: 删除指定的 cookie
useDebugValue 的应用

在这个例子中,我们使用 useDebugValue 来显示当前 cookie 的值:

useDebugValue(cookie, (value) => `cookie: ${value}`)
调试效果展示

在 React DevTools 中的显示效果:

useDebugValue.png

使用建议

  1. 仅在自定义 Hook 中使用 useDebugValue
  2. 对于简单的值,可以省略 formatter 函数
  3. 当格式化值的计算比较昂贵时,建议使用 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 来关联描述内容,帮助屏幕阅读器为用户提供更详细的信息。

当视障用户使用屏幕阅读器浏览网页时:

  1. 读到输入框时会先读出输入框的标签
  2. 然后会读出通过 aria-describedby 关联的描述文本
  3. 用户就能知道这个输入框需要输入什么内容,有什么要求
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]

本文内容参考小满大佬