With great features, comes great understanding — useEffect, the right way 🌀

Lara Mo
5 min readAug 26, 2024

--

Photo by Artem Sapegin on Unsplash

After React 16.8, life was never the same as hooks were introduced. This was around the time I started coding so I didn’t get to experience the pain of class components so much.

I like to say “with great features, comes great understanding”. If you are using a useEffect in the wrong way, you might introduce more bugs and have a negative impact on performance. Removing unnecessary useEffects will make your code easier to read, faster to execute and with fewer bugs.

These are the topics that are we going to cover, but feel free to skip the basics:

· 📗 Intro
· 📝 Syntax
· 👊 Behavior
· ✨ Better Usages
· 👀 Maybe we don’t even need a useEffect

📗 Intro

The useEffect hook that allows us to perform side effects in functional components. It serves purposes similar to lifecycle methods in class components (componentDidMount, componentDidUpdate, and componentWillUnmount).
In other words, the hook will execute when the component is rendered, changed or unmounted.

📝 Syntax

useEffect(callback,[dependencies]);

In the code snippet above, the callback refers to the function executed when the useEffect hook is triggered. This function can be written as an arrow function. The dependencies parameter is an optional array of variables; when any of these variables change, the useEffect hook re-runs the callback.

👊 Behavior

There are several ways the useEffect will act:

1. We can trigger the useEffect each time the screen is rendered.
If you don’t provide an arguments as the dependency array, the useEffect will execute every time your component updates.

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');


useEffect(() => {
console.log("I have executed");
});

In this example, useState initializes the state variables firstName and lastName, along with their respective setter functions, setFirstName and setLastName. The useEffect hook, when used without a dependency array, runs on every render of the component. As a result, "I have executed" will print not only when firstName or lastName changes but also on every re-render of the component, since each render is considered a component update.

2. Specific variable change
You can make the useEffect run when specific state changes.
The useEffect will execute if either of the values in the list has changed. In this case, either firstName or lastName

const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

useEffect(() => {
console.log("I have executed");
}, [firstName, lastName]);

3. Initial re-render
As we saw, passing a dependency array will execute each time there is a change in the dependency value, an actual comparison occurs.

Hence, if we pass an empty dependency array, there would never be a change again, and so the useEffect will only execute once, on the initial render.

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

4. Cleanup
Every time the component is destroyed we will run the code in the return block


useEffect(() => () => {
// class components: componentWillUnmount
console.log('I will run when the component is destroyed');
});

✨ Better Usages

It’s always important to keep in mind clean code. When using useEffects everywhere in the project, it might be hard to pinpoint the exact piece of code that we are looking for. Some great advice I have learnt at work is to encapsulate the hook with the logic.

The benefits are clear: we avoid repetition and have the logic in a dedicated hook that we can later modify as needed and even reuse else where.

For instance, when you first see this component , there is a lot of logic:

const ButtonProps = {
handleOnClick: VoidFunction,
};

const Button = ({ handleOnClick }: ButtonProps) => {
const [isFirstTimePress, setIsFirstTimePress] = useState(true);
const [title, setTitle] = useState('');
useEffect(() => {
const title = isFirstTimePress ? 'hello' : 'welcome back';
setIsFirstTimePress(false);
setTitle(title);
}, []); // <-- great ex of where we only need the hook to execute only once

return <button onClick={handleOnClick}>{title}</button>;
};

We can make it better by separating the UI with the logic of how to render title. Like so:

// Button.tsx
const ButtonProps = {
handleOnClick: VoidFunction,
};

const Button = ({ handleOnClick }: ButtonProps) => {
const { title } = useFindTitle().states;

return <button onClick={handleOnClick}>{title}</button>;
};
// useFindTitle.ts
const useFindTitle = () => {
const [isFirstTimePress, setIsFirstTimePress] = useState(true);
const [title, setTitle] = useState('');

useEffect(() => {
const title = isFirstTimePress ? 'hello' : 'welcome back';
setIsFirstTimePress(false);
setTitle(title);
}, []); // <-- great ex of where we only need the hook to execute only once

return {
states: {
title,
},
};
};

Look how clean and logic-independent Button.tsx has become.
useFindTitle can also be used in other components as needed.
The custom hooks can return states , actions or selectors which would make it even more reusable.

👀 Maybe we don’t even need a useEffect

Don’t use a useEffect to handle user events (ex: click)
For example, we make a game where the user number guess the number. We want to print a snack bar to let the user know if they are close or far from the correct number (1 in our case).

// Game.tsx

const [number, setNumber] = useState(1);

const renderSnackBar = (msg: string) => {
// will render a snackbar (notification)
}

useEffect(() => {
if(number === 1){
renderSnackBar("You guessed it!!!!");
}
if(number > 5){
renderSnackBar("Colder");
}
if(number < 0 ){
renderSnackBar("Buddy, its a positive number");
}
if(number < 5 && number > 2){
renderSnackBar("Hotter");
}
,[number]});

<label> Add your number here: </label>
<input onChange={(e) => setNumber(e.target.value)}/>

We have a useEffect that has a dependency array of number which will execute the callback each time number changes.Instead of this complexity, we can handle the logic directly in the onClick callback. This approach is often more efficient because you know exactly when the action will be executed—immediately when the user clicks the button or triggers the event.

// Game.tsx
const [number, setNumber] = useState(1);

const renderSnackBar = (msg: string) => {
// will render a snackbar (notification)
}

const handleOnChange = (e: Event) => {
cosnt number = e.target.value;
setNumber(number); // update the state
if(number === 1){
renderSnackBar("You guessed it!!!!");
}
if(number > 5){
renderSnackBar("Colder");
}
if(number < 0 ){
renderSnackBar("Buddy, its a positive number");
}
if(number < 5 && number > 2){
renderSnackBar("Hotter");
}
}

<label> Add your number here: </label>
<input onChange={handleOnChange}/>

Thats it!
& be careful, when using useEffects, it has some side effects ;)

Lara,
Until next time 🍃

P.S
For more detailed examples and explanations, visit the React documentation.

--

--

Lara Mo
Lara Mo

Written by Lara Mo

Student @Concordia University | Front-end developer @Plusgrade Passionate about React, JS, CSS, HTML and coffee :) | Coding & coffee is my moto ☕👩‍💻

No responses yet