React useEffect in depth / use of useEffect?

useEffect(callback);

// Example
useEffect(() => {
  console.log("executed after render phase");

  return () => {
    console.log("cleanup function after render");
  };
});
  • Runs on every component render.
  • Typically used for debugging, analogous to function’s body execution on every
    render:
const Component = () => {
  callback()
  return <></>;
};

Note: There is still a difference, in execution time (see the next note). Check this sandbox logs.


useEffect(callback,[]);

// Example
useEffect(() => {
  const fetchUsers = async () => {
    const users = await fetch();
    setUsers(users);
  };

  fetchUsers();
  console.log("called on component's mount");

  return () => {
    console.log("called on component's unmount");
  };
}, []);
  • Usually used for initializing components state by data fetching etc.
  • Runs once on a component mount.
  • The cleanup function will run on component’s unmount.

Gotchas:

Remember, there is a first render and then a mount.

Roughly saying, most of bugs regarding useEffect is not knowing how closures works and not paying attention to linting warnings.

Make sure the array includes all values from the component scope that change over time and that are used by the effect. Otherwise, your code will reference stale values from previous rendersnote in React docs.


useEffect(callback,[arg]);

// Example
useEffect(() => {
  console.log({ users });

  return () => {
    console.log("user value is changing");
  };
}, [users]);
  • Runs on change of arg value.
  • Usually used to run events on props/state change.
  • Multiple dependencies can be provided: [arg1,arg2,arg3...]
  • The cleanup function runs on arg value change.

Gotchas:

i.e compares the value of arg from the previous render and the current one, prevArg === arg ? doNothing() : callback().

  • Because in Javascript {} === {} || [] === [] is a falsy statement, if arg (users in our example) is an object, the callback will run on every render.

  • Runs on mount too, since the first comparison always falsy


Additional Good to Know Points

const timeoutIdRef = useRef();

useEffect(() => {
  const timeoutId = timeoutIdRef.current;
  return () => {
  /*
    Using timeoutIdRef.current directly here is not safe
    since you can't guarantee the ref to exists in this point
    (especially when the component unmounts)
  */
    // Should get a lint warning here
    clearTimeout(timeoutIdRef.current); // BAD

    // Closure on timeoutId value
    clearTimeout(timeoutId); // GOOD
  };
}, [arg]);
const isMounted = useRef(false);

useEffect(() => {
  if (isMounted.current) {
    // first mount
  } else {
    isMounted.current = true;
  }
}, [arg]);

Keep reading:

Leave a Comment