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;
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;
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");
}
});
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"
});
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.
- Write a greeting message in web console when a user first visits your page.
- Initialize a third-party library by calling one of their init-functions.
- Send analytics data when a user visits your site (and resend it when user reloads the page).
- 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.
- Fetch data when a user visits your site (once per session).
- Send analytics data when a component mount.
- Send analytics data when a user visits your site (once per session).
- Run code that should run once on client side and not at all on server-side.
- 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.
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
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
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
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
Summary
useRunOnce is a hook you can use for two use cases.
- When you want to run some code every time a component mounts or remounts.
- 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.