React Hook 其他

2024/08/07

useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

useDebugValue(value, format?)

参数

返回值

基础用法

function testHook(initialCount) {
  const [count, setCount] = useState(initialCount);
  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "testHook: 好好学习"
  useDebugValue('好好学习');
  return [count, setCount];
}

function Counter(props) {
  const [count, setCount] = testHook(props.initialCount);
  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      count is:
      {' '}
      {count}
    </button>
  );
}

延迟格式化 debug 值

在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。

因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

例如,一个返回 Date 值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString 函数调用:

// 只有在 Hook 被检查时才会被调用
useDebugValue(date, date => date.toDateString());

useId

生成传递给无障碍属性的唯一 ID

const id = useId();

基础用法

为无障碍属性生成唯一 ID

import { useId } from 'react';

function PasswordField() {
  const passwordHintId = useId();
  return (
    <>
      <label>
        密码:
        <input
          type="password"
          aria-describedby={passwordHintId}
        />
      </label>
      <p id={passwordHintId}>
        密码应该包含至少 18 个字符
      </p>
    </>
  );
}

为多个相关元素生成 ID

import { useId } from 'react';

export default function Form() {
  const id = useId();
  return (
    <form>
      <label htmlFor={`${id}-firstName`}>名字:</label>
      <input id={`${id}-firstName`} type="text" />
      <hr />
      <label htmlFor={`${id}-lastName`}>姓氏:</label>
      <input id={`${id}-lastName`} type="text" />
    </form>
  );
}

为所有生成的 ID 指定共享前缀

如果你在单个页面上渲染多个独立的 React 应用程序,请在 createRoothydrateRoot 调用中将 identifierPrefix 作为选项传递。这确保了由两个不同应用程序生成的 ID 永远不会冲突

import { createRoot } from 'react-dom/client';

const root1 = createRoot(document.getElementById('root1'), {
  identifierPrefix: 'my-first-app-'
});
root1.render(<App />);

const root2 = createRoot(document.getElementById('root2'), {
  identifierPrefix: 'my-second-app-'
});
root2.render(<App />);

为什么 useId 比递增计数器更好

useId 的主要好处是 React 确保它能够与 服务端渲染一起工作。 在服务器渲染期间,你的组件生成输出 HTML。随后,在客户端,hydration 会将你的事件处理程序附加到生成的 HTML 上。由于 hydration,客户端必须匹配服务器输出的 HTML。

使用递增计数器很难保证这一点,因为客户端组件被 hydrate 处理后的顺序可能与服务器 HTML 的顺序不匹配。调用 useId 可以确保 hydration 正常工作,以及服务器和客户端之间的输出相匹配。

在 React 内部,调用组件的“父路径”生成 useId。这就是为什么如果客户端和服务器的树相同,不管渲染顺序如何,“父路径”始终都匹配。

useSyncExternalStore

在组件顶层调用 useSyncExternalStore 以从外部 store 读取值。

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

参数

返回值

把逻辑抽取到自定义 Hook

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return isOnline;
}

function getSnapshot() {
  return navigator.onLine;
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

服务端渲染支持

如果你连接到一个浏览器特有的 API,因为它在服务端不存在,所以是不可行的, 你需要传一个 getServerSnapshot 函数作为第三个参数给 useSyncExternalStore

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
  return isOnline;
}

function getServerSnapshot() {
  return true; // 服务端生成的 HTML 总是显示“在线”
}
// 确保客户端初始渲染与服务端渲染时 getServerSnapshot 返回完全相同的数据。例如,如果在服务端 getServerSnapshot 返回一些预先载入的 store 内容,你就需要把这些内容也传给客户端。一种方法是在服务端渲染时,生成 <script> 标签来设置像 window.MY_STORE_DATA 这样的全局变量,并在客户端 getServerSnapshot 内读取此全局变量。

注意事项

function getSnapshot() {
  // 🔴 getSnapshot 不要总是返回不同的对象
  return {
    todos: myStore.todos
  };
}
function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);

  // 🔴 总是不同的函数,所以 React 每次重新渲染都会重新订阅
  // ✅ 你可以把subscribe 提到组件外部 或者包在 useCallback 里面
  function subscribe() {
    // ...
  }
}

useActionState(todo)

useOptimistic(todo)

use(todo)