React Hooks review #2: useEffect
This is the second chapter of the React Hooks series. The goal is simple: summarize all the eleven official React hooks:
- The three basic hooks:
useState
,useEffect
,useContext
. - The seven additional hooks:
useReducer
,useCallback
,useMemo
,useRef
,useImperativeHandle
,useLayoutEffect
,useDebugValue
. - And understand the endless potential of custom hooks.
useEffect
useEffect is an async encapsulated area to affect or connect to elements outside the component.
Syntax:
useEffect(effect [, dependencies]);
- Effect: The function that performs the effect.
- Dependencies: An array of variables that triggers the effect.
Since the main component is stateless and is need rerendered, executing async or external code will have inconsistent effects. useEffect
is built to run only when you need to execute something outside the component.
useEffect gets executed only after the browser has painted the component.
Dependency list
The second value in the useEffect
is the dependency list. The useEffect
listens to the values in the list, and on any value change, it re-execute the Hook.
Let’s say we have a component with two simple counters:
const Counter: React.FC<{}> = ()=>{
const [counter,setCounter] = React.useState(0);
const [other,setOther] = React.useState(0);
return (
<div>
<p onClick={()=>{setCounter(counter + 1)}}>Add number</p>
<p onClick={()=>{setOther(other + 1)}}>Add other value</p>
<Number number={counter} otherValue={other} />
</div>
)
}
No dependency list – execute on each re-render
To see how it triggers on each re-rendering, we can add it to the useState
counter and see the log on each counter increment:
const Number: React.FC<NumberProps> = ({number}) => {
React.useEffect(()=>{
console.log('Will be seen only once');
});
return <div>{number}</div>
}
Empty dependency list – execute only after the first render
In this snippet, since the list is empty, the effect will run once on the first component mounting.
const Number: React.FC<NumberProps> = ({number}) => {
React.useEffect(()=>{
console.log('Will be seen only once');
},[]);
return <div>{number}</div>
}
Adding dependency items
If we want the effect to be triggered only on a specific mutation, you can control when to invoke the useEffect
by listing the dependencies in the dependencies list:
const Number: React.FC<NumberProps> = ({number, otherValue}) => {
React.useEffect(()=>{
console.log('effect');
},[number])
return <div>Number: {number}, Other: {otherValue}</div>
}
In this snippet, you can see that clicking the “add number”, will execute the useEffect
since it changes the value inside the track dependencies list. But clicking the “Add other value”, though it will rerender the component, will not trigger the effect.
Note that mutating values inside the dependency list will create an endless rendering loop:
const EndlessLoop: React.FC = ()=>{
const [counter,setCounter] = React.useState(0);
React.useEffect(()=>{
setCounter(counter + 1);
},[counter]);
return <p>Ops {counter}</p>
}
Different types of side effects
As mentioned, the useEffect
is not built for props or inner component actions, but for side effects as fetch requests, DOM manipulating, timer functions, and more.
useEffect(()=>{
document.title = 'My doc';
,[])
Fetch values each time a prop had changed:
const Page: React.FC<PageProps> = ({pageId}) => {
const [page,setPage] = React.useState(null);
React.useEffect(()=>{
const data = await fetch("https://www.get.some/"+pageId
);
setPage(data);
},[pageId])
return !page?<div>Loading...</div>:<div>{page.title}</div>
}
Time intervals:
React.useEffect(()=>{
setTimeout(() => {
setMessage("I'm waiting");
}, 1000);
},[])
Cookie or browser storage:
React.useEffect(()=>{
const userName = window.localStorage.getItem("name");
},[])
Subscribing to some listener
React.useEffect(()=>{
someService.subscribe(userId);
},[userId])
Or adding event to the DOM
React.useEffect(()=>{
window.addEventListener('resize', handleResize);
},[])
Clean up
useEffect
has a callback function to clean up when the component is unmounted or cleanup before re-executing the component and its effect.
const Number: React.FC<{}> = () => {
React.useEffect(()=>{
someService.subscribe();
return () => {
someService.unSubscribe();
}
},[])
return <div>...</div>
}
Remove time intervals:
const Interval: React.FC = () => {
React.useEffect(() => {
const intervalId = setInterval(() => {
console.log('Interval Plooped');
}, 500);
return () => {
clearInterval(intervalId);
console.log('The end of the Interval plooping')
}
}, []);
return <div>Interval</div>;
}
const ChangeComponent: React.FC = () => {
const [component,setComponent] = React.useState(false);
React.useEffect(()=>{
setTimeout(()=>{
setComponent(true)
},3000)
},[]);
return component? <></>: <Interval />
}
ReactDom.render(
<ChangeComponent />,
document.querySelector('#app')
);
Remove DOM event listeners
React.useEffect(()=>{
window.addEventListener('resize', handleResize);
return ()=> {
window.removeEventListener('resize', handleResize);
}
},[])
Not: the cleanup functions gets invoked every time before the execution of the next scheduled effect. not only before destroying the React component.
Async functions inside useEffect
Since the useEffect returns the cleanup method, it is not possible to use the hook as an async function:
useEffect(async () => {
const data = awite fetch('https://someUrl.con');
doSomeStuff(data);
},[])
The two options are to use the JS Promises chaining:
useEffect(()=>{
fetch('https://someUrl.con')
.then((data)=>{
doSomeStuff(data);
})
})
Or to execute an external async function:
async function fetchAndDo(){
const data = awite fetch('https://someUrl.con');
doSomeStuff(data);
}
useEffect(() => {
fetchAndDo();
},[])
Multiplue useEffects
You can have and sould have as meny useEffects as you want in the same component. So you sould split the effect to diffeten methods if:
- Each dependency value had a different code to execute.
- The effect block had more than one “effect” in the same method.