'React custom mount hook

I always forget to add empty dependencies array in my useEffect hook in React to run the effect only once. That's why I decided to use a custom hook with a clear intention in it's name to run only once on mount. But I don't understand why is it running twice?

import React from "react";
import "./styles.css";

function useOnMount(f) {
  const isMountedRef = React.useRef(false);
  console.log("ref1", isMountedRef.current);
  if (!isMountedRef.current) {
    f();
    isMountedRef.current = true;
    console.log("ref2", isMountedRef.current);
  }
}

export default function App() {
  useOnMount(() => {
    console.log("useOnMount");
  });

  return <div>Hello useOnMount</div>;
}

Here's the output:

ref1 false
useOnMount 
ref2 true
ref1 false
useOnMount 
ref2 true

I use ref hook to keep mutable flag between renders. But I can't understand why isMountedRef.current is true on the first render and reverts to false on the second render 🤔



Solution 1:[1]

You keep forgetting the dependencies array? Why not just remember to write it once in your custom hook?

function useOnce (once) {
  return useEffect(once, [])
}

Using it in your App -

function App () {
  useOnce(_ => console.log("useOnce"))

  return <div>hello</div>
}

Here's a demo -

const { useEffect, useState } = React

function useOnce (once) {
  return useEffect(once, [])
}

function App () {

  useOnce(_ => alert("Wake up. It's time to make candy!"))

  const [candy, setCandy] =
    useState(0)

  const earn =
    <button onClick={_ => setCandy(candy + 1)}>
      Make candy
    </button>

  const spend =
    <button onClick={_ => setCandy(candy - 10)}>
      Buy Chocolate (10)
    </button>

  return <div>
    <b>Candy Box</b>
    <p>Alert will only appear one time after component mounts</p>
    <p>
      {earn}
      {(candy >= 10) && spend}
    </p>
    <p>You have {candy} candies</p>
  </div>
}

ReactDOM.render(<App/>, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

Solution 2:[2]

This one works for me and doesn't generate react-hooks/exhaustive-deps warnings.

function useOnce (once) {
  const [ res ] = useState(once)
  return res
}

Usage:

function App () {
  useOnce(() => {
    console.log("useOnce")
    // use any App props or result of other hooks
    ...
  })

  return <div>hello</div>
}

It was recommended here for another task but it works well as a componentDidMount replacement. Any parameters may be used inside the function. Just make sure you really don't need to monitor changes of props.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Dron007