useEffect is Dangerous! ๐งจ๐ฃ
Always avoid using useEffect if you have any alternate solution.
It runs only after the commit phase, i.e after the re-render changes have been committed to the DOM.
Common pitfallsโ
Don't use it to update the local state if the props change.โ
function Message({ msg }) {
const [msgState, setMsgState] = useState(msg);
useEffect(() => setMsgState(msg), [msg]);
return <div>{msgState}</div>;
}
- When the
msgprop changes from parent, it'll call the<Message/>component-function again passing the new updatedmsg(re-render triggered). - Then Message component will re-render by re-calculating the changes in the UI elements, and commit the changes to the browser. Only after that, your
useEffectwill run, setting the new msg in the state. That will again trigger re-render. But you already got the updated msg in previous render, you do not need another re-render!!
function Message({ msg }) {
return <div>{msg}</div>;
}
Don't use useEffect for maintaining derived stateโ
function Message({ error }) {
const [msgState, setMsgState] = useState('');
useEffect(() => setMsgState(`${error.status}: Something went wrong.`), [error]);
return <div>{msgState}</div>;
}
A derived value from other state(s) is called a derived state.
Here the error is the prop or can be a state and the msgState is the derived state.
- When the
errorprop changes from parent, it'll call the<Message/>component-function again passing the new updatederrorobject (re-render triggered). - Then Message component will re-render by re-calculating the changes in the UI elements, and commit the changes to the browser. Only after that, your
useEffectwill run, setting the new msg in the state. That will again trigger re-render. But you already got the updated error object in previous render, you do not need another re-render!!
function Message({ error }) {
const msg = `${error.status}: Something went wrong.`;
return <div>{msg}</div>;
}
function Message({ error }) {
const msg = useMemo(() => `${error.status}: Something went wrong.`, [error.status]);
return <div>{msg}</div>;
}
Read: Why useMemo helps improve performance in this caseโ
Guess how many re-renders will it take to update the screen correctly when both firstName and lastName change!
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ๐ฉ Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
// small computation
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
Answer: How many re-renders?
Problem: When firstName or lastName change, it'll trigger a re-render, but useEffect will only run after the changes have been committed to the DOM. Then useEffect will set a state which will again trigger a re-render.
Of course the firstName and lastName both wont change at the exact same time. There will be some delay in between. Now lets count..
| What triggered re-render | render / re-render count |
|---|---|
firstName changes | 1 |
useEffect triggered by firstName change, updates fullName | 2 |
lastName changes | 3 |
useEffect triggered by lastName change, updates fullName | 4 |
Imagine if there was 1 more dependency or 2 more dependencies!
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// โ
Good: calculated during rendering
const fullName = firstName + ' ' + lastName;
// ...
}
Only use useMemo when the computation is expensive.
function TodoList({ todos, filter }) {
// ๐ฉ Avoid: redundant state and unnecessary Effect
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
// some expensive computation
const filteredTodos = getFilteredTodos(todos, filter);
setVisibleTodos(filteredTodos);
}, [todos, filter]);
// ...
}
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(
() =>
// some expensive computation
getFilteredTodos(todos, filter),
[todos, filter],
);
// ...
}
Avoid useEffect for event driven functionalityโ
function Message({ msg, updateMsg }) {
const [msgInput, setMsgInput] = useState(msg);
useEffect(() => {
updateMsg(msgInput);
}, [msgInput]);
const onMsgChange = e => setMsgInput(e.target.value);
return (
<>
<input value={msgInput} onChange={onMsgChange} />
</>
);
}
There are many problems with the above code, I'll list down a fewโ
- If you are using
eslint, it'll complain:updateMsgis not provided in the dependency list at line no: 6.- How to fix this issue? Disable
eslint? - NO!! eslint is complaining because
updateMsgis a prop coming from parent, what if the parent updates the function at some point? And the callback we are passing to theuseEffectgets memoized! (we're memoizing the side-effect remember?), so when this callback gets executed, it wont get the latest version of theupdateMsgfunction. - If we want the latest version, we do need to add it to the dependency array!
- How to fix this issue? Disable
- But if we add
updateMsginto the dependency array, we'll have another problem. Because it's an argument of our component function, on every re-render this variable will be re-defined, which will trigger "useEffect" and we'll get infinite loop. There are two ways to fix this.- wrap it with
useCallbackto memoize the prop. but it'll again have some other implications. And additionally it needs to run more checks on every re-render for memoization purposes. - Store it in a ref. Eslint won't ask you to add it to dependency array. If
updateMsgdoes change, it won't trigger a re-render. And we'll always get the latest version of the function.
- wrap it with
function Message({ msg, updateMsg }) {
const [msgInput, setMsgInput] = useState(msg);
const updateMsgRef = useRef(updateMsg);
useEffect(() => {
updateMsgRef.current(msgInput);
}, [msgInput]);
const onMsgChange = e => setMsgInput(e.target.value);
return (
<>
<input value={msgInput} onChange={onMsgChange} />
</>
);
}
The above solution still relies on the re-render cycle of the component. Because it'll run only after the component has re-rendered. So it's better to remove the "useEffect" all-together.
function Message({ msg, updateMsg }) {
const [msgInput, setMsgInput] = useState(msg);
const onMsgChange = e => setMsgInput(e.target.value);
const updateParent = () => updateMsg(msgInput);
return (
<>
<input value={msgInput} onChange={onMsgChange} />
<button onClick={updateParent}>Submit</button>
</>
);
}
Most often, the initial data fetching might need a useEffect,
but after that everything happens based on some user interaction. The user scrolls or clicks something or drags something etc. Execute your functions in the event handlers of these user-events.
Don't use prop destructured values in useEffect dependency arrayโ
function Message(props) {
const { msg } = props;
useEffect(() => {
// do something
}, [msg]);
return null;
}
Whenever the component re-renders, it'll re-define the msg variable, which will trigger useEffect again and again. Memoize it if you really need, otherwise find an alternate solution not involving useEffect.
Don't use useEffect to reset all the states when some prop changes.โ
function Profile({ userId }) {
const [userName, setUserName] = useState();
const [email, setEmail] = useState();
useEffect(() => {
setUserName(null);
setEmail(null);
}, [userId]);
return null;
}
It's inefficient, when the userId changes, the component will be re-rendered first, and only after the re-render useEffect will run and reset the states, which will trigger another re-render.
Instead, pass a unique key prop to the <Profile /> component from the parent, so that when the key changes, react will treat it as a brand new component, it'll discard the old component and create a new instance where all the states will be already reset with initial values.
function Parent() {
const userId = useUserId();
return (
<Profile
key={userId}
userId={userId}
/>
);
}
function Profile({ userId }) {
const [userName, setUserName] = useState();
const [email, setEmail] = useState();
return null;
}
Don't chain useEffectsโ
useEffect
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// ๐ฉ Avoid: Chains of Effects that adjust the state solely to trigger each other
// ๐ฉ๐ฉ Chain: card change -> goldCardCount update -> round update -> isGameOver update -> Alert UI
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1);
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
return <div>Game UI</div>;
}
If one state change affects another state change, which then again affects another and another and another.. Then all of these can probably be wrapped up into a single function
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// โ
Calculate all the next state in the event handler
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
return <div>Game UI</div>;
}