Originally I have submitted this tutorial to Facebook Community Challenge 2020, you can access and read it from this link.
If you’ve written React class components before, you should be familiar with lifecycle methods like componentDidMount
, componentDidUpdate
, and componentWillUnmount
. The useEffect
Hook is all three of these lifecycle methods combined. It's used for side effects (all things which happen outside of React) like network requests, managing subscriptions, DOM manipulation, setting up event listeners, timeouts, intervals, or local storage, etc.
useEffect
functions run after every rerender by default.
It doesn't matter what caused the render like changing the state, or maybe new props, the effect will be triggered after rendering.
Setting the title of the page will also be a side effect.
useEffect
takes a callback function, we can tell useEffect
when the code we want to be executed with the second argument. This will control the effect.
For the second argument, we can use useEffect
in three different ways:
1. useEffect without a Dependency Array
// runs after every rerender
useEffect(() => {
console.log('I run after every render and at initial render');
});
This renders every time our app renders and at initial render. But we don't want to render each time, this can cause an infinite loop and we should avoid this.
We need to optimize our components. We can pass a list of dependencies. The dependency will trigger an effect on the change of the dependencies.
Let's see it in a simple example.
// src/components/UseEffect.js
import React, { useState, useEffect } from 'react';
const UseEffect = () => {
const [count, setCount] = useState(0);
const [isOn, setIsOn] = useState(false;)
// useEffect to set our document title to isOn's default state
useEffect(() => {
document.title = isOn;
console.log('first render');
});
const handleClick = () => {
setIsOn(!isOn);
setCount(count + 1)
}
return (
<div>
<h1>{isOn ? "ON" : "OFF"}</h1>
<h1>I was clicked {count} times</h1>
<button onClick={handleClick} className="btn">Click me</button>
</div>
);
}
export default UseEffect;
In our example, we have two states: count
and isOn
. We are rendering these with our button
and h1
tags. When the button gets clicked, we are setting the isOn
state to the opposite of its state.
For the purpose of this example, we are setting useEffect
hook and changing our document title to our isOn
's default value(false).
With our console.log
, we can see that we rerender our component with our initial render and whenever we click the button. Because we don't have any array dependency.
2. useEffect with an Empty Dependency Array
// runs at initial render
useEffect(() => {
console.log('I only run once');
}, []);
This only runs once when the component is mounted or loaded.
It looks exactly like the behavior of componentDidMount
in React classes. But we shouldn't compare with React class components.
3. useEffect with a Non-empty Dependency Array
// runs after every rerender if data has changed since last render
useEffect(() => {
console.log('I run whenever some piece of data has changed)');
}, [id, value]);
If the variable is inside this array, we will trigger this effect only when the value of this variable changes, and not on each rerender. Any state or props we list in this array will cause useEffect
to re-run when they change.
We can put our variables inside the dependency array from our component like any variables that we want for; for example, state variables, local variables, or props.
They adjust the array of dependencies.
// src/components/UseEffect.js
import React, { useState, useEffect } from 'react';
const UseEffect = () => {
const [ count, setCount ] = useState(0);
const [ isOn, setIsOn ] = useState(false);
useEffect(() => {
document.title = isOn;
// only difference from our previous example
setCount(count + 1);
});
const handleClick = () => {
setIsOn(!isOn);
};
return (
<div>
<h1>{isOn ? 'ON' : 'OFF'}</h1>
<h1>I was clicked {count} times</h1>
<button onClick={handleClick} className="btn">Click me</button>
</div>
);
}
export default UseEffect;
We have just changed one line of code from the previous example and changed useEffect
a little, we will not increase our count with the button click. However, we will trigger our effect whenever the useEffect
triggers. Let's see what will happen.
We are in an infinite loop; but why? React rerenders our component when the state changes. We are updating our state in our useEffect
function, and it's creating an infinite loop.
I think no one wants to stuck in a loop; so, we need to find a way to get out of the loop and only run our function whenever our isOn
state changes. For that, we will add our dependency array and pass our isOn
state.
The array of variables will decide if it should execute the function or not. It looks at the content of the array and compares the previous array, and if any of the value specified in the array changes compared to the previous value of the array, it will execute the effect function. If there is no change, it will not execute.
// src/components/UseEffect.js
import React, { useState, useEffect } from 'react';
const UseEffect = () => {
const [ count, setCount ] = useState(0);
const [ isOn, setIsOn ] = useState(false);
useEffect(() => {
document.title = isOn;
setCount(count + 1);
// only add this
}, [isOn]);
const handleClick = () => {
setIsOn(!isOn);
};
return (
<div>
<h1>{isOn ? 'ON' : 'OFF'}</h1>
<h1>I was clicked {count} times</h1>
<button onClick={handleClick} className="btn">Click me</button>
</div>
);
}
export default UseEffect;
It seems like working, at least we got rid of the infinite loop, when it updates count
it will rerender the component. But if you noticed, we start counting from 1 instead of 0. We render first at initial render, that's why we see 1. This effect behaves as a componentDidMount
and componentDidUpdate
together. We can solve our problem by adding an if
condition.
if(count === 0 && !isOn) return;
This will only render at the first render, after that when we click the button, setIsOn
will be true. Now, our code looks like this.
import React, { useState, useEffect } from 'react';
const UseEffect = () => {
const [ count, setCount ] = useState(0);
const [ isOn, setIsOn ] = useState(false);
useEffect(() => {
document.title = isOn;
// add this to the code
if(count === 0 && !isOn) return;
setCount(count + 1);
}, [isOn]);
const handleClick = () => {
setIsOn(!isOn);
};
return (
<div>
<h1>{isOn ? 'ON' : 'OFF'}</h1>
<h1>I was clicked {count} times</h1>
<button onClick={handleClick} className="btn">Click me</button>
</div>
);
}
export default UseEffect;
Okay, now it starts from 0. If you're checking the console, you may see a warning:
We will not add count
inside our dependency array because if the count changes, it will trigger a rerender. This will cause an infinite loop. We don't want to do this, that's why we will not edit our useEffect
. If you want, you can try it out.
useEffect
Cleanup
useEffect
comes with a cleanup function that helps unmount the component, we can think of it is like componentWillUnmount
lifecycle event. When we need to clear a subscription or clear timeout, we can use cleanup functions. When we run the code, the code first will clean up the old state, then will run the updated state. This can help us to remove unnecessary behavior or prevent memory leaking issues.
useEffect(() => {
effect;
return () => {
cleanup;
};
}, [input]);
// src/components/Cleanup.js
import React, { useState, useEffect } from 'react';
const Cleanup = () => {
const [ count, setCount ] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
// cleanup function
return () => clearInterval(intervalId);
}, []);
return (
<div>
<h1>{count}</h1>
</div>
);
}
export default Cleanup;
We have defined a setInterval
method inside our useEffect
hook, and our interval will run in the background. We are passing a function inside setInterval
and it will update our count
piece of state every second.
Our useEffect
hook is only gonna run one time because we have our empty dependency array.
To clean up our hook, we are creating our return
function, getting our interval id, and passing inside our clearInterval
method.
- We can use multiple useEffect's in our application.
- We cannot mark useEffect as an async function.
- React applies effect in the order they are created.
- We can make API calls to React in four different ways:
- Call fetch/Axios in your component
- Make another file and store your API calls.
- Create a reusable custom hook.
- Use a library like react-query, SWR, etc.
We will use fetch
in our application for simplicity. Now, ready to move on with our final demo app? Time to combine everything you have learned with a real-life application. This will be fun!!! 😇
RECIPE APP
It's time to create our demo app!
We will create a Food Recipe app, we will fetch data from an API and we will use both useState
and useEffect
hooks.
First, create a new file under src > components
and name it FoodRecipe.js
.
To be able to get a response for search queries, we need an APP ID
and an APP KEY
.
How Can I Fetch Data?
- Go to edamam.com
- Choose
Recipe Search API
and click onSign Up
- Choose
Developer
and click onStart Now
- Fill out the form.
- Go to
Dashboard
- Click on
Applications
>View
. You should see your Application ID and Application Keys on this page. - Copy your keys and paste them inside the code.
- API can give some errors, if you see any CORS errors, add a cors browser extension for the browser you are using. Firefox / Chrome
- Still, there is a problem? You need to wait until your API keys are available. Also, for the free version, we can only make 5 requests per minute. You can check out the documentation.
// src/components/FoodRecipe.js
import React, {useEffect} from 'react';
const FoodRecipe = () => {
// paste your APP_ID
const APP_ID = '';
// paste your APP_KEY
const APP_KEY = '';
// url query is making a search for 'chicken' recipe
const url = `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`;
// useEffect to make our API request
useEffect(() => {
getData();
}, []);
// created an async function to be able to fetch our data
const getData = async (e) => {
const response = await fetch(url);
const result = await response.json();
// console log the results we get from the api
console.log(result);
};
return (
<div>
<h1>Food Recipe App </h1>
<form>
<input type="text" placeholder="Search for recipes" />
<button type="submit" className="btn">
Search
</button>
</form>
</div>
);
};
export default FoodRecipe;
Let's see what we did in our code:
- Created some JSX elements(form, input, and button properties).
- We are calling our function to fetch our data.
- Created a
fetch
request to get our data, and useduseEffect
hook to call our function. We are using our empty dependency array because we will only make a request when our app loads.
We got our API response, and we got a lot of information. You can see from the gif. Now, we need to create a state for our recipes, and we will update the recipes with the API data. We will only extract hits
and their contents from our response. Let's do it!
// src/components/FoodRecipe.js
import React, {useState, useEffect} from 'react';
const FoodRecipe = () => {
// state for our API data
const [recipes, setRecipes] = useState([]);
const APP_ID = '';
const APP_KEY = '';
const url = `https://api.edamam.com/search?q=chicken&app_id=${APP_ID}&app_key=${APP_KEY}`;
useEffect(() => {
getData();
}, []);
const getData = async () => {
const response = await fetch(url);
const result = await response.json();
console.log(result);
// set the state for our results and extract the 'hits' data from API response
setRecipes(result.hits);
};
// some ui
};
export default FoodRecipe;
Okay, here we have added our recipes
state and updated with setRecipes
. From our API call, we see that hits
is an array, that's why for the default value we put an empty array.
We need to display our recipes, for that let's create a Recipe
component.
Go to src > components
, create a new component, and name it Recipe.js
. Copy this code, this will allow us to display individual recipes.
Here, I have used some Semantic UI components to display our individual recipes.
// src/components/Recipe.js
import React from 'react';
const Recipe = () => {
return (
<div class="ui column grid">
<div className="column recipe">
<div className="content">
<h2>Label</h2>
<p>Calories: </p>
<ul>
<li>Ingredients</li>
</ul>
<a href="" target="_blank">
URL
</a>
</div>
<div className="ui fluid card">
<img />
</div>
</div>
</div>
);
};
export default Recipe;
Now, we need to map over our recipes state, and display the results.
// src/components/FoodRecipe.js
// ..............
return (
<div>
<h1>Food Recipe App </h1>
<form>
<input type="text" placeholder="Search for recipes" />
<button type="submit" className="btn">
Search
</button>
</form>
<div className="recipes">
{/* map over our array and pass our data from API*/}
{recipes !== [] &&
recipes.map((recipe) => (
<Recipe
key={recipe.recipe.url}
label={recipe.recipe.label}
calories={recipe.recipe.calories}
image={recipe.recipe.image}
url={recipe.recipe.url}
ingredients={recipe.recipe.ingredients}
/>
))}
</div>
</div>
);
For now, I am getting our Recipe.js
without any props, of course.
Now, we can go to our Recipe
component and pass our props to it. We are getting these props from the parent FoodRecipe.js
. We will use destructuring to get our props.
// src/components/Recipe.js
import React from 'react';
// destructure label, calories etc
const Recipe = ({label, calories, image, url, ingredients}) => {
return (
<div class="ui column grid">
<div className="column recipe">
<div className="content">
<h2>{label}</h2>
<p>{calories}</p>
<ul>{ingredients.map((ingredient) =>
<li key={ingredient.text}>{ingredient.text}</li>)}
</ul>
<a href={url} target="_blank">
URL
</a>
</div>
<div className="ui fluid card">
<img src={image} alt={label} />
</div>
</div>
</div>
);
};
export default Recipe;
Tadaa!! We got our chickens!
Now, we need to use our search bar, we will search the recipe from our input field. To get the state of our search bar, we will create a new piece of state.
Go to FoodRecipe.js
and add a new search
state.
// src/components/FoodRecipe.js
// create a state for search query
const [search, setSearch] = useState('');
Set the value for input value search
, setSearch
will update our input with the onChange
event handler.
The input
is keeping track of its state with the search
state. We can get input's value from event.target.value
.
Then we can change our state with setSearch
function.
// src/components/FoodRecipe.js
<input
type="text"
value={search}
onChange={(event) => setSearch(event.target.value)}
/>
We need to update our state after we click on Search Button
. That's why we need another state. And we can update our url
from chicken query to any query. Make a new state, name it query
.
// src/components/FoodRecipe.js
const [query, setQuery] = useState('');
// when you send the form, we call onSubmit handler to query the results
const onSubmit = (e) => {
// prevent browser refresh
e.preventDefault();
// setQuery for the finished search recipe
setQuery(search);
};
Now, we need to pass our query
state to our onEffect
dependency array. Whenever we click on the Search button, we will call our API and change our state to a new query
state.
The query
will only run after the form submit. Use it as a dependency inside the array. Our final code now looks like this:
// src/component/FoodRecipe.js
import React, {useState, useEffect} from 'react';
import Recipe from './Recipe';
const FoodRecipe = () => {
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('');
const APP_ID = '';
const APP_KEY = '';
const url = `https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`;
useEffect(() => {
getData();
}, [query]);
const getData = async () => {
const response = await fetch(url);
const result = await response.json();
setRecipes(result.hits);
};
const onSubmit = (e) => {
e.preventDefault();
setQuery(search);
// empty the input field after making search
setSearch('');
};
return (
<div>
<h1>Food Recipe App </h1>
<form onSubmit={onSubmit}>
<input
type="text"
placeholder="Search for recipes"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button type="submit" className="btn">
Search
</button>
</form>
<div className="ui two column grid">
{recipes !== [] &&
recipes.map((recipe) => (
<Recipe
key={recipe.recipe.url}
label={recipe.recipe.label}
calories={recipe.recipe.calories}
image={recipe.recipe.image}
url={recipe.recipe.url}
ingredients={recipe.recipe.ingredients}
/>
))}
</div>
</div>
);
};
export default FoodRecipe;
Time to enjoy your ice creams! I hope you liked the project.
Wrapping Up
Now, go build something amazing, but don't pressure yourself. You can always go back to the tutorial and check how it is done, also check the official React documentation. Start small, try creating components first, then try to make it bigger and bigger. I hope you enjoyed this tutorial. I'm looking forward to seeing your feedback.
If you run into any issues with your app or you have questions, please reach out to me on Twitter or Github.
Credits:
References:
Here are the references I used for this tutorial:
- React Js Documentation
- Overreacted A Complete Guide to useEffect
- Digital Ocean's How To Build a React-To-Do App with React Hooks
- Tutorial Example
- Tania Rascia's React Tutorial
- Software on the Road/React Hooks: everything you need to know!
- Upmostly tutorials/Simplifying React State and the useState Hook
- SitePoint/React Hooks: How to Get Started & Build Your Own
Thanks for your time. Like this post? Consider buying me a coffee to support me writing more.
Top comments (0)