React useEffect is a react hook function that was introduced in React version 17; used for executing side effects following the mounting of a React component or as a result of changes in state declared variables. The hook expects 2 arguments; A callback function and an optional array of dependencies,it optionally return a function.
Visually, the useEffect function was developed by the React team with its functionality abstracted away and only the business logic (arguments, and return function) accessible to end users.
Here is a React function component we will be using for our use cases:
function BlogPosts() {
const [posts, setPosts] = React.useState([]);
const [error, setError] = React.useState("");
const [pending, setPending] = React.useState(false);
const textInput = React.useRef(null);
const form = React.useRef(null);
const getPosts = async () => {
let url = `https://jsonplaceholder.typicode.com/posts/`;
try {
setPending(true);
const response = await fetch(url);
if (response.ok) {
const payload = await response.json();
setError(""); //clear previous error message if any
return payload;
} else throw new Error("Something went wrong....");
} catch (error) {
setError(error.message);
return error;
} finally {
setPending(false);
}
};
/*we will add a useEffect hook here*/
// render some jsx
if (pending) return <p>Loading Posts...</p>; // pending state
if (error.length > 0) return <p>{error}</p>; // rejected
// you can create a <PostsComponent/> of your own
if (posts.length > 0) return <h3>Received {posts.length} posts.</h3>; // resolved
return null; /* just before we fetch posts show blank screen*/
}
Use cases:
- Performing side effects
- Tracking dependencies
- Setup and tear down processes
- Manipulating the DOM
- UseEffect can NOT be converted to async function.
1. Performing side effects
The hook can be used to perform executions after a component is mounted. This includes: network requests by fetch data from a rest API, initializing animations,setting up app related event listeners, setting timeouts and intervals.
Add this piece of code below the getPosts function
React.useEffect(() => {
// fetch data
getPosts().then((res) => {
setPosts(res);
})
// start timers
setTimeout(() => {
console.log("I am executed after 5 seconds");
}, 5000);
setInterval(() => {
console.log("I am executed after every 5 seconds");
}, 5000);
// app event listeners
addEventListener("resize", () => {
console.log("Window resized");
});
addEventListener("offline", () => {
console.log("App is offline");
});
addEventListener("online", () => {
console.log("App is back online");
});
});
2. Tracking Dependencies
Our Our useEffect function up there runs every time there is a render introducing something that looks like a bug.
UseEffect hook runs everytime the UI is rendered and when the component unmounted. The UI is always re-rendered whenever a state variable (variable declared by useState hook) changes.
Sometimes, though, you only want to run useEffect once or when a particular state variable changes.
The second optional argument supplied to useEffect is an array of dependencies, it determines how the useEffect hook is called; an empty array triggers the useEffect during initial render and when the component is unmounted while an array with elements will trigger the hook whenever there are changes are in one of the array elements.
we add an empty array as our second argument so that our hooks runs once
React.useEffect(() => {
// fetch data
getPosts().then((res) => {
setPosts(res);
})
// start timers
setTimeout(() => {
console.log("I am executed after 5 seconds");
}, 5000);
setInterval(() => {
console.log("I am executed after every 5 seconds");
}, 5000);
// app event listeners
addEventListener("resize", () => {
console.log("Window resized");
});
addEventListener("offline", () => {
console.log("App is offline");
});
addEventListener("online", () => {
console.log("App is back online");
});
},[]);
Primitive data types such as numbers,strings and booleans, UseEffect can easily track their changes while non-primitives such as as objects and arrays are mutable data types stored by reference, useEffect has a problem tracking their changes since “non-primitives refer to the same instance” that is they always look identical.
To make useEffect track changes in non primitivies:
- UseMemo hook can be used in conjuction with useEffect to track changes in an array. It calcualates changes between elements previous array and the next array through memoization.
// useMemo will trigger useEffect if posts array has changed
Const _postChanges=React.useMemo(()=>posts,[posts]) /*return the posts array inside useMemo hook*/
/* now the useEffect will be able to listen to changes on posts array by tracking _postChanges array from useMemo */
React.useEffect(()=>{
console.log('you have changes in your array')
},[_postChanges])
- Tracking an object property instead, we assume every object always has a unique property mostly
id
property.
const [post,setPost]=React.useState({id:1,title:"UseEffect hook"})
const nextPost={id:2,"UseReducerHook"}
React.useEffect(()=>{
console.log(`The new post id is ${post.id}`)
},[post?.id]) // runs whenever post id changes
return(<div>
<p>The current post is {post?.title} </p>
<button onClick={()=>setPost(nextPost)}>Next Post</button> </div>)
3. Setup and tear down processes
UseEffect hook comes in handy when setting up processes that can be used when the components mounts and removed when it unmounts, e.g setTimeout, setInterval, adding and removing event listeners.
The hook returns a function that can be used to perform these tear down processes.
_ we now declare two new variables for clearing setInterval and setTimeout, we then return a teardown function_
React.useEffect(() => {
// fetch data
getPosts().then((res) => {
setPosts(res);
})
let timeout,interval; // for clearing intervals and timeout
// start timers
timeout=setTimeout(() => {
console.log("I am executed after 5 seconds");
}, 5000);
interval= setInterval(() => {
console.log("I am executed after every 5 seconds");
}, 5000);
// app event listeners
addEventListener("resize", () => {
console.log("Window resized");
});
addEventListener("offline", () => {
console.log("App is offline");
});
addEventListener("online", () => {
console.log("App is back online");
});
return function(){
// tear down events and intervals
if(timeout)clearTimeout(timeout);
if(interval)clearInterval(interval)
removeEventListener("resize", () => {
console.log("Window event removed");
});
}
},[]);
4. Manipulating the DOM
React implements a virtual DOM before reconcilling changes to the real DOM on the browser. Most a times you want to access to a DOM element such as form or form input elements. We can gain access to the DOM using useRef hook then manipulate it inside the useEffect hook. Sometimes you can use DOM API methods such as document.getElementById()
since we know the DOM is always mounted. It is not advisable though; go for useRef hook.
React.useEffect(() => {
// fetch data
getPosts().then((res) => {
setPosts(res);
})
let timeout,interval; // for clearing intervals and timeout
// start timers
timeout=setTimeout(() => {
console.log("I am executed after 5 seconds");
}, 5000);
interval= setInterval(() => {
console.log("I am executed after every 5 seconds");
}, 5000);
// app event listeners
addEventListener("resize", () => {
console.log("Window resized");
});
addEventListener("offline", () => {
console.log("App is offline");
});
addEventListener("online", () => {
console.log("App is back online");
});
/*MANIPULATE THE DOM */
// access the input on the DOM and make put placeholder after 2 seconds
setTimeout(
() => {
if (textInput.current) {
textInput.current.setAttribute("placeholder", "Enter post title....");
textInput.current.focus();
}
},
3000
);
// manipulate the DOM, give some time out to make sure its there
setTimeout(() => {
const formWrapper = document.getElementById("form-wrapper");
if (formWrapper) formWrapper.style.backgroundColor = "red";
}, 100);
// use DOM API to change background to light gray
}, []);
/*CLEAN UP THE COMPONENT*/
return function(){
// tear down events and intervals
if(timeout)clearTimeout(timeout);
if(interval)clearInterval(interval)
removeEventListener("resize", () => {
console.log("Window event removed");
});
}
// Also update the jsx returned
let style = { display: "block", width: 200, padding: 5 };
let btnStyle = {
...style,
background: "hotpink",
padding: 5,
borderRadius: 5,
marginTop: 10,
color: "white",
};
// render components
if (pending) return <p>Loading Posts...</p>; // pending state
if (error.length > 0) return <p>{error}</p>; // rejected
return (
<div style={{ padding: "1rem" }} id="form-wrapper">
<form onSubmit={handleSubmit} ref={form}>
{posts.length > 0 && <h3>Received {posts.length} posts.</h3>}
<div>
<label htmlFor="username" style={style}>
Enter Post Title
</label>
<input id="username" style={style} ref={textInput} />
</div>
<button style={btnStyle}>Submit</button>
</form>
</div>
);
},[]);
5. UseEffect can NOT be converted to async function.
React hook has few special rules;
1.) React hooks only work inside React components.
2.) React hooks can only be declared on top level scope inside a react component. They cannot be declared conditionally.
Const [deadline,setDeadline]=React.useState(true);
// DON’T
if(deadline){
useEffect(()=>{},[])
}
// DO
useEffect(()=>{
if(deadline){
// do something about it
}
},[])
Most React beginners are always eager break these rules with ease.
There is another unwritten rule that React frowns upon; making the callback provided to useEffect as async/await function.
// DON'T
React.useEffect(async()=>{
const payload=await getPosts();
setPosts(payload);
},[])
When callback used as an async/await function it becomes a promise that blocks the main thread until it settles instead of simply running effects and returning a function.
We run async functions inside useEffect this way:
React.useEffect(()=>{
getPosts().then(res=>setPosts(payload)).catch(err=>console.error(err.message));
},[])
Alternatively we can use an immediately invoked function expressions (IIFE) inside useEffect like so:
React.useEffect(()=>{
(async()=>{
const payload=await getPosts();
if(payload) setPosts(payload)
})()
},[])
Top comments (3)
The reason not to make a
useEffect
async is nothing to do with blocking the main thread. AuseEffect
returns a function to be called when it is unmounted/no longer the current dependency array value - as an async function always returns aPromise
it isn't correctly returning the termination function.Alright, great point there..noted..
A small note, whether async/await or promisses block the main thread depends on what happens inside the promise. If you execute a fetch request, or set a timeout for example, it won't block the main thread.