译:101个React技巧#9React hooks🎣

298 阅读7分钟

59. 确保在 useEffect 钩子中执行必要的清理

如果你在 useEffect 钩子中设置了任何需要后续清理的内容,请始终返回一个清理函数。

这可以是任何内容,从结束聊天会话到关闭数据库连接。

忽略这一步可能导致资源使用不当和潜在的内存泄漏。

❌ 不好: 这个例子设置了一个间隔计时器,但我们从未清除它,这意味着即使在组件卸载后它也会继续运行。

function Timer() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    setInterval(() => {
      setTime(new Date());
    }, 1_000);
  }, []);

  return <>当前时间 {time.toLocaleTimeString()}</>;
}

✅ 好: 当组件卸载时,间隔计时器被正确清除。

function Timer() {
  const [time, setTime] = useState(new Date());
  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    // 我们清除间隔计时器
    return () => clearInterval(intervalId);
  }, []);

  return <>当前时间 {time.toLocaleTimeString()}</>;
}

60. 使用 refs 访问 DOM 元素

你永远不应该直接用 React 操作 DOM。

document.getElementByIddocument.getElementsByClassName 这样的方法是被禁止的,因为 React 应该访问/操作 DOM。

那么当你需要访问 DOM 元素时应该怎么做呢?

你可以使用 useRef 钩子,就像下面的例子中我们需要访问 canvas 元素一样。

🏖 沙盒

注意: 我们可以给 canvas 添加一个 ID 并使用 document.getElementById,但这不被推荐。

61. 使用 refs 在重新渲染间保留值

如果你的 React 组件中有不在状态中的可变值,你会发现这些值的更改不会在重新渲染间保留。

除非你将它们保存在全局中,否则就会发生这种情况。

你可能会考虑将这些值放入状态中。然而,如果它们与渲染无关,这样做会导致不必要的重新渲染,浪费性能。

这就是 useRef 的用武之地。

在下面的例子中,我想在用户点击某个按钮时停止计时器。为此,我需要将 intervalId 存储在某个地方。

❌ 不好: 下面的例子不会按预期工作,因为 intervalId 在每次组件重新渲染时都会被重置。

function Timer() {
  const [time, setTime] = useState(new Date());
  let intervalId;

  useEffect(() => {
    intervalId = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    return () => clearInterval(intervalId);
  }, []);

  const stopTimer = () => {
    intervalId && clearInterval(intervalId);
  };

  return (
    <>
      <>当前时间: {time.toLocaleTimeString()} </>
      <button onClick={stopTimer}>停止计时器</button>
    </>
  );
}

✅ 好: 通过使用 useRef,我们确保 interval ID 在重新渲染间被保留。

function Timer() {
  const [time, setTime] = useState(new Date());
  const intervalIdRef = useRef();
  const intervalId = intervalIdRef.current;

  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date());
    }, 1_000);
    intervalIdRef.current = interval;
    return () => clearInterval(interval);
  }, []);

  const stopTimer = () => {
    intervalId && clearInterval(intervalId);
  };

  return (
    <>
      <>当前时间: {time.toLocaleTimeString()} </>
      <button onClick={stopTimer}>停止计时器</button>
    </>
  );
}

62. 在钩子如 useEffect 中优先使用命名函数而非箭头函数,以便在 React Dev Tools 中轻松找到它们

如果你有很多钩子,在 React DevTools 中找到特定的钩子可能会有挑战性。

一个技巧是使用命名函数,这样你可以快速定位它们。

❌ 不好: 在很多钩子中找到特定的 effect 很困难。

function HelloWorld() {
  useEffect(() => {
    console.log("🚀 ~ Hello, I just got mounted");
  }, []);

  return <>Hello World</>;
}

Effect 没有关联的名称

✅ 好: 你可以快速定位 effect。

function HelloWorld() {
  useEffect(function logOnMount() {
    console.log("🚀 ~ Hello, I just got mounted");
  }, []);

  return <>Hello World</>;
}

Effect 有关联的名称

63. 使用自定义钩子封装逻辑

假设我有一个组件,它从用户的深色模式偏好中获取主题并在应用中使用它。

最好将返回主题的逻辑提取到一个自定义钩子中(以便重用并保持组件简洁)。

❌ 不好: App 过于拥挤

function App() {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    setTheme(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, []);

  return (
    <div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
  );
}

✅ 好: App 简洁多了,我们可以重用逻辑

function App() {
  const theme = useTheme();

  return (
    <div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
  );
}

// 可以重用的自定义钩子
function useTheme() {
  const [theme, setTheme] = useState("light");

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    setTheme(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      setTheme(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, []);

  return theme;
}

64. 优先使用函数而非自定义钩子

永远不要把逻辑放在钩子里,当可以使用函数时 🛑。

实际上:

  • 钩子只能在其他钩子或组件中使用,而函数可以在任何地方使用。
  • 函数比钩子更简单。
  • 函数更容易测试。
  • 等等。

❌ 不好: useLocale 钩子是不必要的,因为它不需要是一个钩子。它没有使用其他钩子如 useEffectuseState 等。

function App() {
  const locale = useLocale();
  return (
    <div className="App">
      <IntlProvider locale={locale}>
        <BlogPost post={EXAMPLE_POST} />
      </IntlProvider>
    </div>
  );
}

function useLocale() {
  return window.navigator.languages?.[0] ?? window.navigator.language;
}

✅ 好: 创建一个函数 getLocale 代替

function App() {
  const locale = getLocale();
  return (
    <div className="App">
      <IntlProvider locale={locale}>
        <BlogPost post={EXAMPLE_POST} />
      </IntlProvider>
    </div>
  );
}

function getLocale() {
  return window.navigator.languages?.[0] ?? window.navigator.language;
}

65. 使用 useLayoutEffect 钩子防止视觉 UI 闪烁

当一个 effect 不是由用户交互引起时,用户会在 effect 运行之前看到 UI(通常很短暂)。

因此,如果 effect 修改了 UI,用户会先看到初始 UI 版本,然后很快看到更新后的版本,造成视觉闪烁。

使用 useLayoutEffect 确保 effect 在所有 DOM 变更后同步运行,防止初始渲染时的闪烁。

在下面的沙盒中,我们希望宽度在列之间均匀分布(我知道这可以用 CSS 完成,但我需要一个例子 😅)。

使用 useEffect,你可以在开始时短暂地看到表格在变化。列首先以默认大小渲染,然后才调整到正确的大小。

🏖 沙盒

如果你在寻找另一个很好的用法,看看这篇文章

66. 使用 useId 钩子为可访问性属性生成唯一 ID

厌倦了想 ID 或者它们冲突?

你可以使用 useId 钩子在 React 组件内部生成一个唯一 ID,并确保你的应用是可访问的。

例子

function Form() {
  const id = useId();
  return (
    <div className="App">
      <div>
        <label>
          姓名 <input type="text" aria-describedby={id} />
        </label>
      </div>
      <span id={id}>确保包含全名</span>
    </div>
  );
}

67. 使用 useSyncExternalStore 订阅外部存储

这是一个很少需要但超级强大的钩子 💪。

在以下情况下使用这个钩子:

  • 你有一些状态不在 React 树中可访问(即不在状态或上下文中)
  • 状态可以改变,你需要你的组件被通知这些变化

在下面的例子中,我想要一个 Logger 单例来记录我的整个应用中的错误、警告、信息等。

这些是需求

  • 我需要能够在 React 应用中的任何地方调用它(甚至在非 React 组件中),所以我不会把它放在状态/上下文中。
  • 我想在一个 Logs 组件中向用户显示所有日志

👉 我可以在 Logs 组件中使用 useSyncExternalStore 来访问日志并监听变化。

function createLogger() {
  let logs = [];
  let listeners = [];

  const pushLog = (log) => {
    logs = [...logs, log];
    listeners.forEach((listener) => listener());
  };

  return {
    getLogs: () => Object.freeze(logs),
    subscribe: (listener) => {
      listeners.push(listener);
      return () => {
        listeners = listeners.filter((l) => l !== listener);
      };
    },
    info: (message) => {
      pushLog({ level: "info", message });
      console.info(message);
    },
    error: (message) => {
      pushLog({ level: "error", message });
      console.error(message);
    },
    warn: (message) => {
      pushLog({ level: "warn", message });
      console.warn(message);
    },
  };
}

export const Logger = createLogger();

🏖 沙盒

68. 使用 useDeferredValue 钩子在新的结果可用前显示之前的查询结果

想象你正在构建一个在地图上显示国家的应用。

用户可以筛选以查看特定最大人口规模的国家。

每次 maxPopulationSize 更新时,地图都会重新渲染(见下面的沙盒)。

🏖 沙盒

因此,注意当你移动滑块太快时它是多么卡顿。这是因为每次滑块移动时地图都会重新渲染。

为了解决这个问题,我们可以使用 useDeferredValue 钩子,使滑块更新更流畅。

<Map
  maxPopulationSize={deferredMaxPopulationSize}
  // …
/>

如果你在寻找另一个很好的用法,看看这篇文章