useEffect Dependency Array, Part 2: Reactive Functions

Understanding Reactive Functions

In the previous reading, you learned about dependency arrays in useEffect. Now, let's examine a more complex challenge: dealing with reactive functions inside a component.

Why Functions Inside Components Are Reactive

Functions defined inside a component are reactive values, meaning a new instance of the function is created every time the component renders.

Example:


function App() {
  const [num, setNum] = useState(1);
  const message = `${num} is ${isEven(num) ? "" : "not "}even`;

  function isEven(num) {
    return num % 2 === 0;
  }

  useEffect(() => {
    if (isEven(num)) console.log(num);
  }, [num, isEven]);

  return 

{message}

; }

Here, isEven is a function defined inside the component. Since it gets re-created on every render, the dependency array will always detect a change and re-run the effect unnecessarily.

Why This Happens

Every time a function is created inside a component, a new function instance is generated, even if its logic is the same.


a = () => console.log('hi');
b = () => console.log('hi');
console.log(a === b); // false
    

Since a and b are separate instances, comparing them returns false. This is why isEven triggers unnecessary re-renders.

Fixing the Issue

Solution 1: Move the Function Outside the Component

The easiest way to prevent unnecessary re-renders is to move the function outside the component:


function isEven(num) {
  return num % 2 === 0;
}

function App() {
  const [num, setNum] = useState(1);
  const message = `${num} is ${isEven(num) ? "" : "not "}even`;

  useEffect(() => {
    if (isEven(num)) console.log(num);
  }, [num]);

  return 

{message}

; }

Since isEven is now a stable function, it no longer needs to be in the dependency array.

Solution 2: Use useCallback

If moving the function outside the component is not an option, wrap it in useCallback to ensure it does not get recreated on every render:


import { useCallback } from 'react';

function App() {
  const [num, setNum] = useState(1);

  const isEven = useCallback((num) => num % 2 === 0, []);

  useEffect(() => {
    if (isEven(num)) console.log(num);
  }, [num, isEven]);

  return 

{num} is {isEven(num) ? "" : "not "}even

; }

useCallback ensures that isEven remains stable unless its dependencies change.

useState Setter Functions Are Stable

Unlike functions created inside a component, useState setter functions are always stable and do not need to be included in the dependency array.


const [count, setCount] = useState(0);

useEffect(() => {
  setCount(prev => prev + 1);
}, []); // No need to include setCount
    

Hooks from Other Packages

Some hooks, like useNavigate (React Router) and useDispatch (Redux), return stable functions but must still be included in the dependency array.


import { useDispatch } from "react-redux";

function App() {
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchData());
  }, [dispatch]);
}
    

What You Have Learned

In this reading, you explored:

Further Exploration

To continue learning, visit:

Conclusion

Handling functions inside dependency arrays requires an understanding of function instances in JavaScript. Moving functions outside the component or using useCallback can prevent unnecessary re-renders and improve performance.