Skip to main content

useEffect is Dangerous! ๐Ÿงจ๐Ÿ’ฃ

danger

Always avoid using useEffect if you have any alternate solution.

Remember ๐Ÿง 

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.โ€‹

DON'T ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
function Message({ msg }) {
const [msgState, setMsgState] = useState(msg);

useEffect(() => setMsgState(msg), [msg]);

return <div>{msgState}</div>;
}
  • When the msg prop changes from parent, it'll call the <Message/> component-function again passing the new updated msg (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 useEffect will 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!!
DO โœ…
function Message({ msg }) {
return <div>{msg}</div>;
}

Don't use useEffect for maintaining derived stateโ€‹

DON'T - Example 1 ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
function Message({ error }) {
const [msgState, setMsgState] = useState('');

useEffect(() => setMsgState(`${error.status}: Something went wrong.`), [error]);

return <div>{msgState}</div>;
}
Derived State

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 error prop changes from parent, it'll call the <Message/> component-function again passing the new updated error object (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 useEffect will 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!!
If the derived state is a simple computation โœ…
function Message({ error }) {
const msg = `${error.status}: Something went wrong.`;

return <div>{msg}</div>;
}
If the derived state is an expensive computation โœ…
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!

DON'T - Example 2 ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
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-renderrender / re-render count
firstName changes1
useEffect triggered by firstName change, updates fullName2
lastName changes3
useEffect triggered by lastName change, updates fullName4
๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ

Imagine if there was 1 more dependency or 2 more dependencies!

DO โœ…
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// โœ… Good: calculated during rendering
const fullName = firstName + ' ' + lastName;

// ...
}
tip

Only use useMemo when the computation is expensive.

DON'T ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
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]);

// ...
}
DO โœ…
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โ€‹

DON'T ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
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: updateMsg is not provided in the dependency list at line no: 6.
    • How to fix this issue? Disable eslint?
    • NO!! eslint is complaining because updateMsg is a prop coming from parent, what if the parent updates the function at some point? And the callback we are passing to the useEffect gets memoized! (we're memoizing the side-effect remember?), so when this callback gets executed, it wont get the latest version of the updateMsg function.
    • If we want the latest version, we do need to add it to the dependency array!
  • But if we add updateMsg into 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.
    1. wrap it with useCallback to 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.
    2. Store it in a ref. Eslint won't ask you to add it to dependency array. If updateMsg does change, it won't trigger a re-render. And we'll always get the latest version of the function.
Example with useRef โœ…
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} />
</>
);
}
It's not good enough

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.

Event Driven Solution โœ…
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>
</>
);
}
Always try to implement event driven solution

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โ€‹

DON'T ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
function Message(props) {
const { msg } = props;
useEffect(() => {
// do something
}, [msg]);

return null;
}
infinite loop

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.โ€‹

DON'T ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
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.

DO โœ…
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

DON'T ๐Ÿ’ฉ๐Ÿงจ๐Ÿ’ฃ
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>;
}
tip

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

DO โœ…
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>;
}