DEV Community

Cover image for React Hook: useRunOnce
Dennis Persson
Dennis Persson Subscriber

Posted on • Edited on • Originally published at perssondennis.com

React Hook: useRunOnce

Sometimes we need to run code only once. useRunOnce is a hook that runs a function one time when a component mounts, or one time per browser session. The article explains common use cases of this hook and when not to use it.

EDITED: Updated this article to emphasize that this hook should only be used in special cases, it's not for everyday usage. It's often a better solution to run a useEffect multiple times and handle the consequences in a clean way. Before using it, please read about when not to use this hook and try to figure out if there is a way not to use it.

Thanks to Luke Shiru who posted a comment that enlightened me that the use cases wasn't very clear.

In This Article

useRunOnce Hook

Below you can see how useRunOnce hook is implemented in JavaScript and typescript. The hook can be used to run a function once, either on mount or per browser session.

The hook takes an object as an argument, with two available properties. Firstly, a required fn property that is the callback function that will run. If no other property is passed, the callback function will run once every time the component mounts.

If the second property sessionKey is passed, the hook will instead utilize session storage to run the callback function just once per browser session. That is further explained later in this article.

The code is also available at CodeSandbox and GitHub. You can try it out on CodeSandbox, but I will explain more about how it works here in the article.

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 Forest Gump meme
Forest Gump has never heard about segmentation fault

Run Once on Mount

If you want to run a function once a component mounts, simply pass a callback function to the argument object's fn attribute. The callback will only fire one time. Unless the component is being unmounted and mounted again, in that case, it will fire again.

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

Run Once per Session

If you would like to run a function only one time per session, you can pass a sessionKey to the hook. The hook will then use session storage to ensure that the callback function only runs once per session.

In other words, when passing a sessionKey, the passed in function will only run one single time when a user visits your website. The callback function won't be triggered again, not even when the user reloads the website using the browser's reload button.

For the callback function to run one more time, the user will need to close the browser tab or the browser and then revisit the website in another tab or browser session. This is all according to the session storage documentation

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

Note. A common problem with session and local storage is that you cannot force users to close their browsers. Although, in some cases it may be necessary to clear the storage. Easiest way to do that is to use another storage key. So, if you are using this hook with a sessionKey and want all clients to rerun the hook, even if they aren't closing their browser, just make another deployment of your application with another sessionKey.

When Not To Use

Occationally, when I think I need this hook, I think twice about it and realize that I really don't. Here follows some cases when I wouldn't use the hook.

  1. Write a greeting message in web console when a user first visits your page.
  2. Initialize a third-party library by calling one of their init-functions.
  3. Send analytics data when a user visits your site (and resend it when user reloads the page).
  4. Fetch data when a component mount.

1. Write a Greeting Message in Web Console When a User First Visits Your Page

One reason you may not need the hook is because it's unnecessary to use a hook/useEffect if you don't need to read or set an internal state in a component. Writing a greeting message to the web console has nothing to do with React components or its life cycle, you can do that in pure JavaScript and there is no reason to do that within a React component.

2. Initialize a Third-Party Library by Calling One of Their Init-Functions

The reason to not using this hook when initializing third-party libraries is the same as when writing a message to the web console. Initializing third-party libraries may include registering plugins to a date library, configuring languages in a i18n library or whatsoever.

Such logic is rarely dependent on data in a React component and should therefore be initialized outside your components. Simply place the code in a file right above a React component and it will run once and only once, that's how ES6 modules are designed. See examples of when not to use an useEffect in Reacts documentation.

3. Send Analytics Data When a User Visits Your Site (and Resend It When User Reloads the Page)

You will find this point among the use cases as well. It really depends on what you want to measure. Do you want to resend analytics data when the user reloads a page with the web browser's reload button?

In that case, you may be able to fetch the data outside your React components as described above, if you don't need to read or set a component's internal state. On the other hand, if you don't want to refetch the data when a page is being reloaded, you can use the useRunOnce hook and provide a sessionKey to it.

4. Fetch Data When a Component Mount

This point is quite important if you don't want to introduce a lot of bugs in your code. In React 18 Strict Mode, useEffects will run twice when mounting a component in development mode. In future releases that will also sometimes happen in production.

For that reason, you should be careful with sending network requests in useEffects. This hook includes a useEffect and does not handle it in a best-practice way, since it doesn't include all real dependencies in the useEffects dependency list.

You should most often avoid sending network requests in useEffects. Network requests of POST, PUT, PATCH or DELETE types should nearly never be placed in useEffects, they are usually triggered as a direct consequence of a user action and should therefore be triggered by a onClick handler, not in a useEffect.

It may be fine to fetch data in useEffects, but when doing that, you must ensure to handle the case when data is received twice or thrice. In other words, your callback function must be idempotent. You are better off using a hook like useSWR which handles both caching and request deduplications for you. React have documented how to handle cases like this in their docs, make sure to read it, you will need to learn it eventually.

Use Cases

When would one want to use this hook? Here are some example use cases.

  1. Fetch data when a user visits your site (once per session).
  2. Send analytics data when a component mount.
  3. Send analytics data when a user visits your site (once per session).
  4. Run code that should run once on client side and not at all on server-side.
  5. Count how many times a user visits your site.

1. Fetch Data When a User Visits Your Site (Once per Session)

First of all, if you have not read about not using this hook to fetch data when a component mount, do that first. If you do have a reason to fetch data only once per session though, this hook could be used for that. Then use it with a passed-in sessionKey attribute.

2. Send Analytics Data When a Component Mount

This is maybe the most common use case. The docs for React 18 brings up how to handle analytics data in Strict Mode. What they mention is that it's a good idea letting it send it twice in development mode.

Anyhow, what they show is a simple case to handle. You may not be lucky enough that your analytics request only is dependent on a single url variable. It may be dependent on a lot of more variables, and you probably don't want to send the analytics request 30 times.

You can easily solve that in your code with code similar to what this hook contains, or you can use this hook.

3. Send Analytics Data When a User Visits Your Site (Once per Session)

Since this hook includes an option to include a sessionKey, you can also send analytics data once per browser session. This allows you to send analytic requests only once even when users are keeping their browser tab open for multiple days and just reloading it once in a while.

4. Run Code That Should Run Once on Client Side and Not at All on Server-Side

React supports server-side rendering (SSR), and there exist multiple frameworks that is built on React which supports SSR and even static site generation (SSG), one of those is Next.js.

When rendering React on server-side, the global window and document objects aren't available. Trying to access one of those objects on the server would throw an error. For that reason, following Reacts suggestion for how to detect when an application initializes isn't possible. This hook can therefore be very useful when dealing with frameworks that run server-side, since this hook only will trigger the callback function on client side.

5. Count How Many Times a User Visits Your Site

Why not count user visits? It may be useful sometimes. In that case, you can count on this hook.

React hook useRunOnce meme
Easiest way to fix a bug is to remove code

Examples

The code below illustrates how to use the useRunOnce hook to send analytics data when a component mounts. For demonstration, it also sets an internal state in the component and renders a text.

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

In the example below, we instead log to local storage that analytics has been sent. This way, you probably don't need to use this hook. The reason is that nothing in the callback function is dependent on an internal state in the component. The code within the callback is pure JavaScript and can be lifted out of the React component.

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

This is how the above code would look if we removed the hook and lifted out the code that fetches data and stores it in local storage.

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

If we don't want to resend analytics when the website is reloaded, we could use the hook to ensure that it only send data once per browser session, it would then look as this.

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

Summary

useRunOnce is a hook you can use for two use cases.

  1. When you want to run some code every time a component mounts or remounts.
  2. When you want to run some code once per browser session.

Since the hooks wraps a useEffect, running code when a function mount can infer side effects in React 18 Strict Mode. Read React's documentation to see how to handle that.

The hook uses session storage to run code once per browser session. The hook will therefore run its code as soon as a new session is initiated, see session storage documentation for details or read through this article.

Top comments (8)

Collapse
 
mrcaidev profile image
Yuwang Cai

Love this post! I also implemented my own useRunOnce today, and it made me feel like, we've migrated from classes to hooks, but still we have to write componentDidMount ourselves. Kinda interesting.

Collapse
 
perssondennis profile image
Dennis Persson

Thanks :) I'm all in for hooks.

I will see if people seem to like this article, then I will post more nice hooks.

Collapse
 
trondeh80 profile image
DevTron

Awesome article. Just a small question; Is it really needed to place a boolean variable in a useRef hook? Why can you not just define a "let triggered = false" outside the scope of the react function and read and change it when needed?
Its a bit more straightforward than the triggered.current syntax IMHO

Collapse
 
perssondennis profile image
Dennis Persson

Great question. I explain why it would work differently. But what you are saying is a good idea for extending this hook, because it adds a third use case it.

As I mentioned in the article, ES6 modules are ensured to only be imported once. What that means is that a variable declared outside the React component would only trigger once. The difference to when a useRef is used is that the useRef will reset the boolean each time the hook unmounts and mount again, a variable outside the React function scope wouldn't run again.

So to sum up:

  • useRef: Will run every time the component which uses the hook mounts. This means that the function can run multiple times if the component are unmounted and then mounted again. Can for example happen when a user navigates to another page on your website and then goes back to the previous page again.
  • Variable as you suggest: Will only run the callback function once during a user visit. The user will have to reload the web page for the hook to run the function one more time. It will not retrigger the function if you just remount a component.
  • Session storage: Will only run the callback function once during a browser session, which means it won't even retrigger if you reload the web page, you will have to close the browsser tab.
Collapse
 
andrewbaisden profile image
Andrew Baisden

Nice article thats a super useful hook!

Collapse
 
paras231 profile image
paras231

great article ,would love to see more amazing hooks like this

Collapse
 
ivan_jrmc profile image
Ivan Jeremic

Correct answer, don't use hooks like this better prepare correctly for react 18+

Collapse
 
avuenja profile image
Marcelo Pecin

It's awesome, generally I need to use something similar. Like show a cookie alert for example.
But, I never think to create a specific hook like this hehe

Generally, Iā€™m use useEffect and localStorage in your raw version.

Great post man!