reactjsreact-hookswebpack-dev-server

Basic React app is triggering useEffect twice


I've searched around and all the similar posts either dont apply or have no effect. I have a very simplified React app (v18.2.0) and im wanting to do some API calls and other logic in my app on render using useEffect. My understanding is this should only trigger once when providing an empty array as a param.

index.js:

import React from "react";
import App from "./src/App";

import { createRoot } from "react-dom/client";
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);

App.js:

import React, { useEffect } from "react";

const App = () => {
  useEffect(() => {
    console.log("trigger app");
  }, []);

  return <div />;
};

export default App;

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
    <script defer src="main.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

webpack config:

module.exports = {
  mode: "development",
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "main.js",
  },
  target: ["web", "es5"],
  stats: { children: true },
  devServer: {
    port: "9500",
    static: ["./public"],
    open: true,
    hot: true,
    liveReload: true,
  },
  devtool: "inline-source-map",
  resolve: {
    extensions: [".js", ".jsx", ".json"],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: "babel-loader",
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
    new CleanWebpackPlugin(),
  ],
};

Looked into this post, Ive verified im not using Strict Mode, there doesnt seem to be anything that is triggering a re-render, and the component isnt being used more than once.


Solution

  • UseEffect hooks are always executed at least two times. Even when you use an empty dependency array

    useEffect(()=>{}, [])
    

    The first time it executes before the component is mounted on the DOM, and the second time is after it gets mounted into the DOM.

    If you want overcome this, and only run your useEffect one time, apply a custom hook to detect if the component is mounted or not.

    Usually, people apply this as a reusable custom hook using a ref. Or they apply the logic right into their code by using useState()

    It's pretty simple to create:

    Using state:

    const [mounted, setMounted] = React.useState(false);
      React.useEffect(() => {
        setMounted(true);
      }, []);
      if (!mounted) return <Button disabled label="Please wait..."/>;
      else return <Button enabled />;
    

    Or if you want, Using refs:

    import React, { useEffect, useRef } from 'react'
    
    export default function useIsMounted() {
      const didMountRef = useRef(false)
      useEffect(() => {
        didMountRef.current = true
      })
    
      return Boolean(didMountRef.current)
    }
    
    

    You can then use it in any component you want to detect if the component is mounted or not:

    import useIsMounted from '@lib/useIsMounted'
    const App = ()=> {
      const isMounted = useIsMounted()
      useEffect(()=> {
        if(!isMounted) return
        // else, here, execute the following code only once, and after mount:
        console.log("you'll see me once if you disable strict mode")
      }, [])
    }
    

    If you want, there's already an npm package for this hook, you can download it from here:

    $ npm i use-is-mounted-ref
    

    Then in the same way:

    import useIsMounted from 'use-is-mounted-ref';
    
    // ... etc
    

    Using the reusable custom hook approach with ref is the ultimate solution for any scenario in my experience.