DEV Community

Cover image for React Hooks: When Not to Use useEffects
Viviana Yanez
Viviana Yanez

Posted on • Edited on

React Hooks: When Not to Use useEffects

I am working on a client-side application using React and Firebase. This week, I paired with another team member to add a new feature allowing users to submit a list name and create a new list in the database.

Just imagine you have a form element inside a component that receives existing lists' data as a prop from its parent and renders all the user's existing lists.

Here's a simplified example:

import './Home.css';

export function Home({ data }) {
    const [newList, setNewList] = useState('');

    return (
        <div className="Home">
            <form>
                <label htmlFor="new list name">Create a new list</label>
                <input
                    type="text"
                    id="new list name"
                    value={newList}
                />
                <button type="submit">Create list</button>
            </form>
            <ul>
                {data.map((list, i) => <SingleList key={i} name={list.name} />)}
            </ul>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

When users submit the form, you want to show a message confirming whether a new list was or was not created. How could you check that the list was successfully created using the data you receive via prop?

My first attempt was to use useEffect.

Understanding useEffect in React

First, a bit of context on the useEffect` React Hook. As described in the React docs, _useEffect`` is a React Hook that lets you synchronize a component with an external system_. It accepts two arguments:

  • setup: A function that React will run when your component is added to the DOM and after every re-render with changed dependencies.

  • optional dependencies: A list of the reactive values referenced inside the setup function. The setup code will execute if at least one of the provided dependencies has changed since the previous run. If you omit this argument, the effect will re-run after every re-render of the component. And passing an empty array will make the effect to run only after the initial render. See examples here

Here is a complete guide to the useEffect with more information about its correct usage.

Why should I NOT use useEffect

Coming back to my example, I wanted to check if the last element in my lists data matches the new list name whenever the data value changes:

import './Home.css';

export function Home({ data }) {
    const [newList, setNewList] = useState('');

    useEffect(() => {
        const lastAddedItem = data[data.length - 1];
        if (lastAddedItem?.name === newList) {
            // Confirm the list was added
        } else {
            // Display an error message
        }
    }, [data, newList]); 
    // ... 
}
Enter fullscreen mode Exit fullscreen mode

However, after more reading, I found out that using useEffect here might not be the best approach.

Effects are a good choice when you need to update your component depending on external elements. But you should not use them for handling user events or for transforming data for rendering.

Instead, apply updates to the component state based on props or state changes at the top level of your components, where the code will automatically re-run when props or state change.

This was the case for my example, I wanted to update some state depending on a prop changes. To adopt a React pattern, I should base my approach on storing information from previous renders.

Based on that, something like this would be a better solution for my example:

const [prevData, setPrevData] = useState(data);

if (prevData.length !== data.length) {
    // do something and then set the prevData to the new data
    setPrevData(data); 
}
Enter fullscreen mode Exit fullscreen mode

If you find this interesting, there are a few more scenarios well detailed in the React docs where you might not need an effect and you can achieve more readable, faster, and less error-prone code by removing them.

Thanks for reading!

Top comments (5)

Collapse
 
imhollymolly profile image
imhollymolly

This is where memoization is useful. Memoization is the concept of keeping memory of a function return value to avoid repeated calls to functions that process the same input.

function Home({ data }) {
    const [name, setName] = React.useState("");
    const isLastItemAdded = React.useMemo(() => data[data.length - 1] == name, [data, name]);

    // isLastItemAdded is a boolean. It will be true if name is the loosely equal to the last item inside of the data array prop.

    const addToList = (e) => {
        e.preventDefault();
        isLastItemAdded ? notify() : displayErrors();
    }

    return (
        <div className="Home">
            <form onSubmit={addToList}>
                <label htmlFor="new-list-name">Create a new list</label>
                <input
                    type="text"
                    id="new-list-name"
                    value={name}
                    onChange={e => setName(e.currentTarget.value)}
                />
                <button type="submit">Create list</button>
            </form>
            <ul>
                {data.map((item, i) => <li key={i}>{item}</li>)}
            </ul>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

react.dev/reference/react/useMemo

useMemo is a React Hook that lets you cache the result of a calculation between re-renders.
Enter fullscreen mode Exit fullscreen mode

On the initial render, useMemo returns the result of calling the callback. During next renders, it will either return an already stored value from the last render if the dependencies haven’t changed, or call the callback again and return the result.

Be aware that proper structure of component architecture would have got you to handle that logic from the parent component instead.

Collapse
 
merri profile image
Vesa Piittinen

For the possibly interested I ported the "Storing information from previous renders" for @preact/signals-react:

Notable differences to React:

  • trend is declared with useComputed as it is derived state
  • trend signal cannot be used as JSX conditional so workaround via extra variable
Collapse
 
disturbedneo profile image
DisturbedNeo

Since you already have a parent component passing "data" down to your Home component, you can also pass an "onSubmit" function as a prop that you add to the form element, and remove all the internal state from Home.

The parent component handles managing the state, including updating the list and displaying a success message, and the child becomes a "dumb" component whose sole responsibility is displaying whatever data it is given.

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Totally agree here !

Collapse
 
get_pieces profile image
Pieces 🌟

Great article! πŸ”₯