A complete overview of react Hook, useEffect.

Hello everyone,

As we all know a functional react component uses props and state to calculate the output. If the functional component makes calculations that don’t target the output value, then these calculations are named side-effects. An example of a side-effect can be performing fetch requests using custom API. The component rendering and side-effect logic are independent. So it would be a mistake to perform side-effects directly in the body of the component. How often the component renders isn’t something you can control but with the help of useEffect we can make sure the right content is getting updated on UI after every render cycle. Now let's explore all the features of useEffect:

useEffect and component Mounting

In my last blog, I discussed in detail about useCallback and useMemo. The signature of useEffect is similar to them. But unlike useCallback or useMemo, useEffect runs on every render cycle and with dependency, we can control when side effect runs. Let's talk about useEffect and how its dependencies control the running of this hook.

  • If no dependency is provided, then whenever component having useEffect renders, functions defined inside useEffect also run.
import {React, useEffect, useState} from 'react';

export default function App() {
  const [state, setState] = useState('Hello');

  useEffect(() => {
    console.log('Render once only');
  });

  return (
    <div className="App">
      <input type='text' value={state} 
      onChange={(e)=>setState(e.target.value)}/>
      {state}
    </div>
  );
}

Run the above code and observe the output on the console. You can use the sandbox provided below.

  • If an empty array is provided in dependency then useEffect runs only "once" after initial rendering. This is equivalent to calling "compountDidMount" after the initial render cycle has run in the mount phase.
import {React, useEffect, useState} from 'react';

export default function App() {
  const [state, setState] = useState('Hello');

  useEffect(() => {
    console.log('Render once only');
  },[]);

  return (
    <div className="App">
      <input type='text' value={state} 
      onChange={(e)=>setState(e.target.value)}/>
      {state}
    </div>
  );
}

Run the above code and observe the output on the console. You can use the sandbox provided below.

  • If we provide at least one dependency, then the function contained in useEffect runs once component with useEffect renders but after that whenever any of the items provided in the dependency array alters/ changes, function inside useEffect will be executed. This is equivalent to calling "compountDidUpdate" after the initial render cycle has run in the update phase.
import "./styles.css";
import {React, useEffect, useState} from 'react';

export default function App() {
  const [state, setState] = useState('Hello');
  const [val, setVal] = useState(false);

  useEffect(() => {
    console.log('Render once only');
  },[state, val]);

  return (
    <div className="App">
      <input type='text' value={state} onChange={(e)=>setState(e.target.value)}/>
      <button onClick={()=>setVal(!val)}>Toggle</button>
      {state}
    </div>
  );
}

Run the above code and observe the output on the console. You can use the sandbox provided here.

An example of fetching data from API as a side-effect

In the code mentioned below, I have rendered a component that displays a list of items fetched from an API by a side-effect. Check out this example.

In this code, I am fetching a list of movies using a custom API. The moment App Component mounts the first time useEffect runs as a side effect and updates the movie list which is displayed after the input bar. If the user enters a keyword and clicks the "Go" button the list is updated with movies of that keyword in their name. Now as the useEffect depends on the state of "API", the moment we hit "Go" the state of "API" is updated and the App component re-renders. But useEffect doesn't run with the initial value rather it takes the new value of API set via,

const handleChange=(e)=>{
  setAPI(SEARCH_API+value);
}

And that's how we see new movies list as useEffect ran as a side-effect and rendered a new list.

Clean up function

Some side-effects need cleanup: closing a socket, clearing timers, or updating the database. If calling the useEffect returns a function, then useEffect() considers this as an effect cleanup:

useEffect(() => {
  console.log('Side-effect');
  return function cleanup() {
  console.log('Side-effect cleanup');
  };
}, [dependencies]);

Cleanup works the following way:

  • After initial rendering, useEffect() invokes the callback having the side-effect. The cleanup function is not invoked.
  • On later renderings, before invoking the next side-effect via useEffect() invokes the cleanup function from the previous side-effect execution (to clean up everything after the previous side-effect), then runs the current side-effect.
  • Finally, after unmounting the component, useEffect() invokes the cleanup function from the last side-effect.

Here is an example of cleanup as a side-effect.

The following component accepts a prop message. The console logs every 2 seconds any message that’s been ever typed into the input. If you comment on the code you will observe that every 2 seconds all the messages since the initial render are getting logged on console, but you need to log only the latest message. With cleanup, we make sure that the last interval is cleared and hence only the last message is displayed on the console. Without cleanup, the link of useEffect to the initial render cycle event is not broken hence even after re-render occurs multiple times, useEffect keeps displaying data from the last render cycle.

Using useEffect with useCallback

Before moving forward check out this blog, to understand react hooks useCallback and useMemo in detail. https://shrey027.hashnode.dev/an-overview-on-react-hooks-usememo-and-usecallback

useCallback() is often used in conjunction with useEffect() because it allows you to prevent the re-creation of a function. For this, it's important to understand that functions are just objects in JavaScript. Therefore, if you have a function (A) inside of a function (B), the inner function (=A) will be recreated (i.e. a brand-new object is created) whenever the outer function (B) runs. That means that in a functional component, any function you define inside of it is re-created whenever the component rebuilds. Check this code below for more understanding.

const OuterComponent = props => {
    const InnerFunction = () => {
        // do something!
    };

    useEffect(() => {
        InnerFunction();
        /**
             The effect calls InnerFunction, 
             hence it should declare it as a dependency
             Otherwise, if something about InnerFunction 
             changes (e.g. the data it uses), the effect would
             run the outdated version of InnerFunction
        */
    }, [InnerFunction]);
};

The useEffect re-runs whenever InnerFunction changes. As stated, it is re-created whenever OuterComponent re-builds. Because functions are objects and objects are reference types, that means that the useEffect will re-run for every render cycle as references of the same object are treated as separate elements in JS. It becomes a problem when InnerFunction does something that causes OuterComponent to re-build (i.e. if it either does something that changes the props or the state). We would have an infinite loop.

To prevent this never-ending cycle of re-render and side-effect calling, we can make use of useCallback like shown in the code below.

const OuterComponent = props => {
    const InnerFunction= useCallback(() => {
        // do something!
    });

    useEffect(() => {
        InnerFunction();
        /**
             The effect calls InnerFunction, 
             hence it should declare it as a dependency
             Otherwise, if something about InnerFunction 
             changes (e.g. the data it uses), the effect would
             run the outdated version of InnerFunction
        */
    }, [InnerFunction]);
};

By wrapping it around useCallback and defining the dependencies, we have ensured that the function is only re-created if its dependencies changed. Hence the function will not rebuild on every render cycle anymore and so the infinite loop is broken.

Conclusion

In conclusion, I would like to summarize what e have covered so far. useEffect is a hook that manages the side effects in functional components. It has two arguments a callback function to put the side-effect logic and a list of dependencies of your side-effect(which can be props or state values). useEffect(callback, dependencies) invokes the callback after initial mounting, and on later renderings, if any value inside dependencies has changed. Because the useEffect() hook heavily relies on closures, we need to provide a clean-up function too. And lastly, we must use the useEffect() with useCallback to avoid the infinite loop pitfall.