Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React: Infinite loop when using custom hook inside of useEffect?

Tags:

reactjs

I am trying to create a custom hook to wrap about Notistack (https://github.com/iamhosseindhv/notistack), a library for snackbars.

My hook looks like this:

import { useCallback } from 'react';
import { useSnackbar as useNotistackSnackbar } from 'notistack';

import { SNACKBAR_TYPES } from '../constants/properties';

const useSnackbar = () => {
  const { enqueueSnackbar } = useNotistackSnackbar();

  const showSnackbarVariant = useCallback(
    ({ text, action_text, onActionClick, variant }) =>
      enqueueSnackbar(
        {
          variant,
          text,
          action_text,
          onActionClick,
        },
        { autoHideDuration: action_text && onActionClick ? 9000 : 4000 }
      ),
    [enqueueSnackbar]
  );

  return {
    showSuccessSnackbar: ({ text, action_text, onActionClick }) =>
      showSnackbarVariant({
        variant: SNACKBAR_TYPES.SUCCESS,
        text,
        action_text,
        onActionClick,
      }),
    showErrorSnackbar: ({ text, action_text, onActionClick }) =>
      showSnackbarVariant({
        variant: SNACKBAR_TYPES.ERROR,
        text,
        action_text,
        onActionClick,
      }),
    showWarningSnackbar: ({ text, action_text, onActionClick }) =>
      showSnackbarVariant({
        variant: SNACKBAR_TYPES.WARNING,
        text,
        action_text,
        onActionClick,
      }),
    showDownloadSnackbar: ({ text, action_text, onActionClick }) =>
      showSnackbarVariant({
        variant: SNACKBAR_TYPES.DOWNLOAD,
        text,
        action_text,
        onActionClick,
      }),
    showPlainSnackbar: ({ text, action_text, onActionClick }) =>
      showSnackbarVariant({
        variant: SNACKBAR_TYPES.PLAIN,
        text,
        action_text,
        onActionClick,
      }),
  };
};

export default useSnackbar;

I need to use it in 2 places:

  1. outside of useEffect (but still within the component)
  2. inside of useEffect

However, even if I just add it as just a dependency on useEffect, it causes the infinite loop inside useEffect:

export default function MyComponent() {
  const { showSuccessSnackbar, showErrorSnackbar } = useSnackbar();
  const { mutate: activateConnectedAccount } =
    useCustomHook({
      onSuccess: async () => {
        showSuccessSnackbar({
          text: 'Direct deposit has been enabled.',
        });
      },
      onError: () => {
        showErrorSnackbar({
          text: 'An error occurred. Please double check your bank information.',
        });
      },
    });

  useEffect(
    () => {
      activateConnectedAccount()
      console.log("yooo");
    },
    [
      // showSuccessSnackbar
    ]
  );

  return (
    <div>
      Foobar
    </div>
  );
}

Codesandbox link: If you comment in line 30, it will cause the browser to freeze because it keeps running that loop

https://codesandbox.io/s/currying-browser-5vomm?file=/src/MyComponent.js

like image 399
bigpotato Avatar asked Aug 19 '21 22:08

bigpotato


People also ask

How do you fix the infinite loop inside useEffect React hooks?

To get rid of your infinite loop, simply use an empty dependency array like so: const [count, setCount] = useState(0); //only update the value of 'count' when component is first mounted useEffect(() => { setCount((count) => count + 1); }, []); This will tell React to run useEffect on the first render.

Is it possible to use a custom hook inside useEffect in React?

You can't use a hook inside another hook because it breaks the rule Call Hooks from React function components and the function you pass to useEffect is a regular javascript function. What you can do is call a hook inside another custom hook.

Why does my useEffect keep running?

Changing state will always cause a re-render. By default, useEffect always runs after render has run. This means if you don't include a dependency array when using useEffect to fetch data, and use useState to display it, you will always trigger another render after useEffect runs.

How do I make useEffect run forever?

To make your useEffect run only once, pass an empty array [] as the second argument, as seen in the revised snippet below. You could pass in any number of values into the array and useEffect will only run when any one of the values change.

What does useeffect() do in ReactJS?

That's an infinite loop. it generates an infinite loop of component re-renderings. After initial rendering, useEffect () executes the side-effect callback that updates the state. The state update triggers re-rendering.

What the Hell is useeffect () in react-hooks?

If you start using React-Hooks, and your component might need a life cycle method at some point. And, that is when you start using useEffect () (a.k.a Effect Hook ). Then boom!!, you have encountered an infinite loop behavior, and you have no idea why the hell is that.

How do you stop an infinite loop in react?

An efficient way to avoid the infinite loop is to properly manage the hook dependencies — control when exactly the side-effect should run. useEffect(() => { // No infinite loop setState(count + 1); }, [whenToUpdateValue]); Alternatively, you can also use a reference. Updating a reference doesn’t trigger a re-rendering:

How to fix an infinite loop when using a hook?

If you are building a custom hook, you can sometimes cause an infinite loop with default as follows function useMyBadHook (values = {}) { useEffect ( ()=> { /* This runs every render, if values is undefined */ }, [values] ) } The fix is to use the same object instead of creating a new one on every function call:


Video Answer


2 Answers

You are returning anonymous functions from useSnackbar hook, which creates a new function every time a re-render happens

Using useCallback on showSnackbarVariant function does the trick for me

Please find the updated useSnackbar hook below

useSnackbar.js

import { useCallback, useMemo } from "react";
import { useSnackbar as useNotistackSnackbar } from "notistack";

const useSnackbar = () => {
  const { enqueueSnackbar } = useNotistackSnackbar();

  const showSnackbarVariant = useCallback(
    ({ text, action_text, onActionClick, variant }) =>
      enqueueSnackbar(
        {
          variant,
          text,
          action_text,
          onActionClick
        },
        { autoHideDuration: action_text && onActionClick ? 9000 : 4000 }
      ),
    [enqueueSnackbar]
  );

  const showSuccessSnackbar = useCallback(
    ({ text, action_text, onActionClick }) => {
      showSnackbarVariant({
        variant: "success",
        text,
        action_text,
        onActionClick
      });
    },
    [showSnackbarVariant]
  );

  return {
    showSuccessSnackbar,
    showErrorSnackbar: ({ text, action_text, onActionClick }) => {
      console.log("eee");
      showSnackbarVariant({
        variant: "error",
        text,
        action_text,
        onActionClick
      });
    }
  };
};

export default useSnackbar;

Please find sandbox for reference:

Edit gallant-wildflower-l5zy3

Please Let me know if any explanation is needed

like image 168
Sumanth Madishetty Avatar answered Nov 15 '22 11:11

Sumanth Madishetty


you could save the result of your custom hook in a useState and then use this value in a useEffect to trigger your event at the right time.

 const { showSuccessSnackbar, showErrorSnackbar } = useSnackbar();
  const [isSuccess, setIsSuccess] = useState(false);
  const { mutate: activateConnectedAccount } = useCustomHook({
    onSuccess: async () => {
      showSuccessSnackbar({
        text: "Direct deposit has been enabled."
      });
      setIsSuccess(true);
    },
    onError: () => {
      showErrorSnackbar({
        text: "An error occurred. Please double check your bank information."
      });
    }
  });

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

  useEffect(() => {
    if (isSuccess) {
      console.log("yooo");
    }
  }, [isSuccess]);

Here is the sandbox

Although I don't really understand why you would need to do this, as you could already do whatever needs to be done in your onSuccess callback. Your useCustomHook could also contain a useEffect within its body if you want it to hold the logic.

As the previous answer says if you use a function/complex object as a dependency on useEffect without useCallback/useMemo, the shallow comparison will fail and React will re-run the useEffect on every render. If you pass primitive value, React is smart enough to re-run the useEffect only when that value changes.

I like to pass only primitive values as dependencies to avoid these issues of too many calls/ infinite loop

like image 34
Kevin Amiranoff Avatar answered Nov 15 '22 10:11

Kevin Amiranoff