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
msg
prop 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
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!!
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
error
prop changes from parent, it'll call the<Message/>
component-function again passing the new updatederror
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!!
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: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 theuseEffect
gets memoized! (we're memoizing the side-effect remember?), so when this callback gets executed, it wont get the latest version of theupdateMsg
function. - 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
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.- 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. - 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.
- 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>;
}