React Hook:useRunOnce

2025-06-07

React Hook:useRunOnce

有时我们只需要运行一次代码。useRunOnce 是一个钩子,它会在组件挂载时或每个浏览器会话中运行一次函数。本文介绍了此钩子的常见用例以及何时不使用它。

编辑:更新本文,强调此钩子仅应在特殊情况下使用,并非日常使用。多次运行 useEffect 并以简洁的方式处理后果通常是更好的解决方案。在使用它之前,请阅读何时不使用此钩子,并尝试找出是否有方法可以避免使用它。

感谢 Luke Shiru 发表的评论,让我明白用例不是很清楚。

在本文中

useRunOnce 钩子

下面你可以看到 useRunOnce 钩子在 JavaScript 和 TypeScript 中的实现方式。该钩子可用于在挂载时或每次浏览器会话时运行一次函数。

该钩子接受一个对象作为参数,该对象具有两个可用属性。首先,一个必需的fn属性,它是将要运行的回调函数。如果没有传递其他属性,则回调函数将在每次组件挂载时运行一次。

如果传递了第二个属性sessionKey,则钩子将利用会话存储,在每个浏览器会话中仅运行一次回调函数。本文稍后将对此进行进一步解释。

该代码也可在CodeSandboxGitHub上获取。您可以在 CodeSandbox 上试用,但我将在本文中更详细地解释其工作原理。

JavaScript

import { useEffect, useRef } from "react";

const useRunOnce = ({ fn, sessionKey }) => {
  const triggered = useRef(false);

  useEffect(() => {
    const hasBeenTriggered = sessionKey
      ? sessionStorage.getItem(sessionKey)
      : triggered.current;

    if (!hasBeenTriggered) {
      fn();
      triggered.current = true;

      if (sessionKey) {
        sessionStorage.setItem(sessionKey, "true");
      }
    }
  }, [fn, sessionKey]);

  return null;
};

export default useRunOnce;
Enter fullscreen mode Exit fullscreen mode

TypeScript

import React, { useEffect, useRef } from "react";

export type useRunOnceProps = {
  fn: () => any;
  sessionKey?: string;
};

const useRunOnce: React.FC<useRunOnceProps> = ({ fn, sessionKey }) => {
  const triggered = useRef<boolean>(false);

  useEffect(() => {
    const hasBeenTriggered = sessionKey
      ? sessionStorage.getItem(sessionKey)
      : triggered.current;

    if (!hasBeenTriggered) {
      fn();
      triggered.current = true;

      if (sessionKey) {
        sessionStorage.setItem(sessionKey, "true");
      }
    }
  }, [fn, sessionKey]);

  return null;
};

export default useRunOnce;
Enter fullscreen mode Exit fullscreen mode

React hook useRunOnce 阿甘正传 meme
阿甘正传从未听说过分段错误

在安装上运行一次

如果要在组件挂载后运行某个函数,只需将回调函数传递给参数对象的fn属性即可。该回调只会触发一次。除非组件被卸载并再次挂载,否则它会再次触发。

useRunOnce({
    fn: () => {
        console.log("Runs once on mount");
    }
});
Enter fullscreen mode Exit fullscreen mode

每个会话运行一次

如果您希望每个会话仅运行一次函数,可以将sessionKey传递给钩子。然后,钩子将使用会话存储来确保回调函数每个会话仅运行一次。

换句话说,当传递 sessionKey 时,传入的函数只会在用户访问你的网站时运行一次。回调函数不会再次触发,即使用户使用浏览器的刷新按钮重新加载网站也不会再次触发。

为了再次运行回调函数,用户需要关闭浏览器标签页或浏览器,然后在其他标签页或浏览器会话中重新访问该网站。这些操作均根据会话存储文档进行。

useRunOnce({
    fn: () => {
        // This will not rerun when reloading the page.
        console.log("Runs once per session");
    },
    // Session storage key ensures that the callback only runs once per session.
    sessionKey: "changeMeAndFnWillRerun"
});
Enter fullscreen mode Exit fullscreen mode

注意:会话和本地存储的一个常见问题是无法强制用户关闭浏览器。不过,在某些情况下,可能需要清除存储。最简单的方法是使用另一个存储密钥。因此,如果您将此钩子与 sessionKey 一起使用,并希望所有客户端即使没有关闭浏览器也能重新运行该钩子,只需使用另一个 sessionKey 重新部署您的应用程序即可。

何时不使用

有时候,当我觉得自己需要这个钩子时,我会再三考虑,然后意识到我其实并不需要。以下是一些我不会使用这个钩子的情况。

  1. 当用户首次访问您的页面时,在 Web 控制台中写一条问候消息。
  2. 通过调用其中一个 init 函数来初始化第三方库。
  3. 当用户访问您的网站时发送分析数据(并在用户重新加载页面时重新发送)。
  4. 当组件安装时获取数据。

1. 当用户首次访问您的页面时,在 Web 控制台中编写问候消息

你可能不需要使用 hook 的一个原因是,如果你不需要读取或设置组件的内部状态,就没必要使用 hook/useEffect。向 Web 控制台写入问候消息与 React 组件及其生命周期无关,你可以在纯 JavaScript 中执行此操作,因此没有必要在 React 组件中执行此操作。

2. 通过调用其中一个 init 函数来初始化第三方库

初始化第三方库时不使用此钩子的原因与向 Web 控制台写入消息时相同。初始化第三方库可能包括向日期库注册插件、在 i18n 库中配置语言等等。

此类逻辑很少依赖于 React 组件中的数据,因此应在组件外部初始化。只需将代码放在 React 组件正上方的文件中,它就会运行一次,这就是ES6 模块的设计方式。请参阅React 文档中关于何时不使用 useEffect的示例

3. 当用户访问您的网站时发送分析数据(并在用户重新加载页面时重新发送)

您也会在用例中找到这一点。这实际上取决于您想要测量的内容。您是否希望在用户使用 Web 浏览器的“重新加载”按钮重新加载页面时重新发送分析数据?

在这种情况下,如果您不需要读取或设置组件的内部状态,则可以如上所述在 React 组件外部获取数据。另一方面,如果您不想在页面重新加载时重新获取数据,则可以使用 useRunOnce 钩子并为其提供sessionKey 。

4. 组件挂载时获取数据

如果你不想在代码中引入大量 bug,这一点非常重要。在 React 18严格模式下,在开发模式下挂载组件时 useEffects 会运行两次。在未来的版本中,这种情况有时也会在生产环境中发生。

因此,在 useEffects 中发送网络请求时应格外小心。此钩子包含一个 useEffect,但并未以最佳实践的方式处理它,因为它并未将所有实际依赖项都包含在 useEffects 的依赖列表中。

你应该尽量避免在 useEffects 中发送网络请求。POST、PUT、PATCH 或 DELETE 类型的网络请求几乎不应该放在 useEffects 中,它们通常是由用户操作直接触发的,因此应该由 onClick 处理程序触发,而不是在 useEffect 中。

在 useEffects 中获取数据可能没问题,但这样做时,必须确保处理数据接收两次或三次的情况。换句话说,你的回调函数必须是幂等的。最好使用像useSWR这样的钩子,它可以同时处理缓存和请求去重。React在其文档中记录了如何处理此类情况,请务必阅读,你最终需要学习它。

用例

什么时候应该使用这个钩子?以下是一些示例用例。

  1. 当用户访问您的网站时获取数据(每个会话一次)。
  2. 当组件安装时发送分析数据。
  3. 当用户访问您的网站时发送分析数据(每个会话一次)。
  4. 运行应该在客户端运行一次而根本不在服务器端运行的代码。
  5. 计算用户访问您网站的次数。

1. 当用户访问您的网站时获取数据(每个会话一次)

首先,如果您还没有阅读过关于在组件挂载时不使用此钩子获取数据的内容,请先阅读。如果您确实需要每个会话只获取一次数据,那么可以使用此钩子。然后将其与传入的 sessionKey 属性一起使用。

2. 组件挂载时发送分析数据

这也许是最常见的用例。React 18 的文档介绍了如何在严格模式下处理分析数据。他们提到,在开发模式下允许数据发送两次是个好主意。

无论如何,他们展示的是一个比较简单的案例。你可能不太幸运,你的分析请求只依赖于一个URL变量。它可能依赖于很多变量,而且你可能不想发送 30 次分析请求。

您可以使用与此钩子包含的代码类似的代码轻松地在代码中解决这个问题,或者您可以使用此钩子。

3. 当用户访问您的网站时发送分析数据(每个会话一次)

由于此钩子包含一个包含 sessionKey 的选项,因此您还可以在每个浏览器会话中发送一次分析数据。这样,即使用户连续几天打开浏览器标签页,只是偶尔重新加载,您也可以只发送一次分析请求。

4. 运行应该在客户端运行一次、根本不在服务器端运行的代码

React 支持服务器端渲染(SSR),并且存在多个基于 React 构建的支持 SSR 甚至静态站点生成(SSG)的框架,其中之一就是 Next.js。

在服务器端渲染 React 时,全局的windowdocument对象不可用。尝试访问服务器上的其中一个对象会抛出错误。因此,按照React 的建议来检测应用程序何时初始化是行不通的。因此,这个钩子在处理运行在服务器端的框架时非常有用,因为它只会触发客户端的回调函数。

5. 统计用户访问网站的次数

为什么不统计用户访问量?有时候这很有用。如果是这样,你可以依赖这个钩子。

React hook useRunOnce meme
修复错误的最简单方法是删除代码

示例

下面的代码演示了如何使用 useRunOnce 钩子在组件挂载时发送分析数据。为了演示,它还在组件中设置了一个内部状态并渲染了一段文本。

import React from 'react'
import useRunOnce from 'hooks/useRunOnce'
import fetchData from 'services/fetchData'

const MyComponent = () => {
  const [analyticsHasBeenSent, setAnalyticsHasBeenSent] = useState(falsse)

  useRunOnce({
    fn: () => {
      sendAnalytics()
      setAnalyticsHasBeenSent(true)
    }
  });

  return <>{analyticsHasBeenSent ? 'Analytics has been sent' : 'Analytics has not been sent'}</>
}

export default MyComponent
Enter fullscreen mode Exit fullscreen mode

在下面的示例中,我们将已发送的分析数据记录到本地存储中。这样,您可能不需要使用此钩子。原因是回调函数中的任何内容都不依赖于组件的内部状态。回调函数中的代码是纯 JavaScript,可以从 React 组件中提取出来。

import React from 'react'
import useRunOnce from 'hooks/useRunOnce'
import fetchData from 'services/fetchData'

const MyComponent = () => {

  useRunOnce({
    fn: () => {
      sendAnalytics()
      localStorage.setItem('analytics-has-been-sent', 'true')
    }
  });

  return <>MyComponent</>
}

export default MyComponent
Enter fullscreen mode Exit fullscreen mode

如果我们删除钩子并提取获取数据并将其存储在本地存储中的代码,则上述代码将如下所示。

import React from 'react'
import fetchData from 'services/fetchData'

sendAnalytics()
localStorage.setItem('analytics-has-been-sent', 'true')

const MyComponent = () => {
  return <>MyComponent</>
}

export default MyComponent
Enter fullscreen mode Exit fullscreen mode

如果我们不想在网站重新加载时重新发送分析,我们可以使用钩子来确保它在每个浏览器会话中只发送一次数据,然后它看起来就像这样。

import React from 'react'
import useRunOnce from 'hooks/useRunOnce'
import fetchData from 'services/fetchData'

const MyComponent = () => {

  useRunOnce({
    fn: () => {
      sendAnalytics()
      localStorage.setItem('analytics-has-been-sent', 'true')
    },
    sessionKey: "anyStringHere"
  });

  return <>MyComponent</>
}

export default MyComponent
Enter fullscreen mode Exit fullscreen mode

概括

useRunOnce 是一个可用于两种用例的钩子。

  1. 当您想要在每次安装或重新安装组件时运行一些代码时。
  2. 当您想在每个浏览器会话中运行一次某些代码时。

由于 hooks 包装了 useEffect,因此在 React 18 严格模式下,函数挂载时运行代码可能会引发副作用。请参阅React 文档,了解如何处理这种情况

该钩子使用会话存储在每个浏览器会话中运行一次代码。因此,一旦新的会话启动,钩子就会立即运行其代码。有关详细信息,请参阅会话存储文档或阅读本文。

文章来源:https://dev.to/perssondennis/react-hook-userunonce-48a8
PREV
JavaScript 数组的 20 个最常见用例
NEXT
从程序员到软件开发人员——成就非凡的技能