Stop useEffect from running on mount
Asked Answered
V

4

25

I only want useEffect to run when my dependency list changes, it is also running every time the component is mounted, is there any way to not fire on mount?

You can tell React to skip applying an effect if certain values haven’t changed between re-renders.

I initially thought that meant it shouldn't re-render on subsequent mounts but this question cleared that up.

I am displaying a list of records in a master "page" (react-router), the user can choose a single record and go to the detail page, and then return to the master page - so the master list component is completely unmounted/mounted in that scenario. And every time I load the "master page", I see the data being fetched, I only want this to happen when one of the dependencies changes; these dependencies and the data itself are stored in Redux so they're global.

Can useEffect or another hook be made to only fire when the dependencies change?

const {page, pageSize, search, sorts} = useSelector(getFilters);
const data = useSelector(getData);

useEffect(() => {

  console.log("fetching");
  dispatch(fetchData(page, pageSize, search, sorts));

}, [page, pageSize, search, sorts]);
Vickeyvicki answered 26/12, 2019 at 19:26 Comment(0)
C
36

You can't configure it out of the box.

But, a common pattern is to use some isMounted flag like so:

// Is Mounted
const useFetchNotOnMount = () => {
  ...
  const isMounted = useRef(false);

  useEffect(() => {
    if (isMounted.current) {
      console.log('fetching');
      dispatch(fetchData(filters));
    } else {
      isMounted.current = true;
    }
  }, [dispatch, filters]);
};

// Same (Is First Render)
const useFetchNotOnMount = () => {
  ...
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
    } else {
      console.log("fetching");
      dispatch(fetchData(filters));
    }
  }, [dispatch, filters]);
};
Cantina answered 26/12, 2019 at 19:39 Comment(3)
I believe there is a bug with the second example, the "Is First Render" one. Instead of isFirstRender = false it should be isFirstRender.current = false.Marconigraph
@Marconigraph thanks you are rightCantina
The problem with this approach is that useEffect is called twice on mount when in dev mode. (I hate react!!!)Potto
L
2

If you have several useEffect to prevent from running at initially, you can do the following:


export default function App() {
  const mountedRef = useMountedRef();
  const [isLoggedIn, setLoggedIn] = React.useState(false);
  const [anotherOne, setAnotherOne] = React.useState(false);

  React.useEffect(() => {
    if (mountedRef.current) {
      console.log("triggered", isLoggedIn);
    }
  }, [isLoggedIn]);

  React.useEffect(() => {
    if (mountedRef.current) {
      console.log("triggered", anotherOne);
    }
  }, [anotherOne]);

  React.useEffect(() => {
    if (mountedRef.current) {
      console.log("triggered", isLoggedIn, anotherOne);
    }
  }, [anotherOne, isLoggedIn]);

  return (
    <div>
      <button onClick={() => setLoggedIn(true)}>Login</button>
    </div>
  );
}

const useMountedRef = () => {
  const mountedRef = React.useRef(false);

  React.useEffect(() => {
    setTimeout(() => {
      mountedRef.current = true;
    });
  }, []);

  return mountedRef;
};

Demo: https://stackblitz.com/edit/react-eelqp2

One thing important is that you have to use setTimeout to make a reasonable delay to make sure that the ref value is set to true after all initial useEffects.

Lynwoodlynx answered 6/5, 2021 at 8:7 Comment(1)
I think this is bad, no matter how much time you give to setTimeout, you'll never be sure that's enough. The only true fix is to have an useEffect that is ALWAYS LAST (order is important!) and sets mountedRef.current = true;Aves
A
2

You can use custom hook to run use effect after mount.

const useEffectAfterMount = (cb, dependencies) => {
  const mounted = useRef(true);

  useEffect(() => {
    if (!mounted.current) {
      return cb();
    }
    mounted.current = false;
  }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};

useEffectAfterMount(() => {

  console.log("fetching");
  dispatch(fetchData(page, pageSize, search, sorts));

}, [page, pageSize, search, sorts]);

Here is the typescript version:

const useEffectAfterMount = (cb: EffectCallback, dependencies: DependencyList | undefined) => {
  const mounted = useRef(true);

  useEffect(() => {
    if (!mounted.current) {
      return cb();
    }
    mounted.current = false;
  }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Anticlinorium answered 26/9, 2022 at 15:42 Comment(0)
D
0

I know this is late to the game but I think it's also worth noting that for what the OP is trying to accomplish, data caching would be a more wholistic and scalable solution. Libraries like react-query are great for this.

Demigod answered 16/12, 2022 at 16:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.