React useEffect mental model
useEffect
- side effect is a state change that can be observed outside of its local environment
- side effect is anything that interacts with the outside world
- mutating non-local variables, making network requests and updating the DOM are all examples of common side-effects
- useEffect allows you to perform side effects in functional components
- 3 parts to useEffect
- add an effect
- skip re-invoking the effect
- optionally clean up the effect
To add a side effect to a React component, you invoke useEffect passing it a function which defines your side effect
React.useEffect(() => {
//side effect
}, []//depedencies in the side effect)
By default, React will re-invoke the effect after every render. Counter example below with the useEffect flow
function Counter () {
const [count, setCount] = React.useState(0)
React.useEffect(() => {
console.count('In useEffect, after render')
document.title = `Count: ${count}`
})
console.count('Rendering')
return (
<React.Fragment>
<button onClick={() => setCount((c) => c - 1)}>-</button>
<h1>Count: {count}</h1>
<button onClick={() => setCount((c) => c + 1)}>+</button>
</React.Fragment>
)
}
-
On initial render,
- count state will be set to 0. The effect will be set and be invoked after render.
- UI will be
Description of UI: - 0 + - React will update the DOM
- Browser will repaint the DOM with updates (console.log will get printed out - console.count(‘Rendering’))
- React will invokes the effect - console.count(‘In useEffect, after render’) and document.title =
Count: 0
-
User click on + button triggering a rerender
- count will be set to 1. The effect will be set and be invoked after render.
- UI will be
Description of UI: - 1 + - React will update the DOM
- Browser will repaint the DOM with updates(console.log will get printed out - console.count(‘Rendering’))
- React will invokes the effect - console.count(‘In useEffect, after render’) and document.title =
Count: 1
-
React will always prioritise the UI
Let’s look at another example with API call
function Profile () {
const [profile, setProfile] = React.useState(null)
React.useEffect(() => {
getGithubProfile('tylermcginnis')
.then(setProfile)
})
if (profile === null) {
return <p>Loading...</p>
}
return (
<React.Fragment>
<h1>@{profile.login}</h1>
<img
src={profile.avatar_url}
alt={`Avatar for ${profile.login}`}
/>
<p>{profile.bio}</p>
</React.Fragment>
);
}
-
On initial render
- profile state will be null. Effect will be set and be invoked after render
- since profile is null. UI will display Loading…
- React will update the DOM and browser the repaints the DOM changes
- effect will run
() => { getGithubProfile('tylermcginnis') .then(setProfile) }. setProfile will trigger a rerender - profile state will be set to the profile data. Effect will be set and be invoked after render
- UI will display the profile information
- React will update the DOM and browser will repaint the DOM changes - effect will run
() => { getGithubProfile('tylermcginnis') .then(setProfile) }. setProfile will trigger a rerender - as you can notice by now. This is causing an infinite loop!!
-
What we require is to opt-out of the useEffect default functionality of being re-invoked on every render.
-
To skip side-effects, you need to pass it an array of all of the outside values your effect depends on.
React.useEffect(() => {
// Will be invoked on the initial render
// and all subsequent re-renders.
})
React.useEffect(() => {
// Will be invoked on the initial render
// and when "id" or "authed" changes.
}, [id, authed])
React.useEffect(() => {
// Will only be invoked on the initial render
}, [])
So we are required to make changes to the code to be allow the useEffect to be invoked only when a value that the useEffect uses changes
function Profile({ username }) {
const [profile, setProfile] = React.useState(null)
React.useEffect(() => {
getGithubProfile(username)
.then(setProfile)
}, [username])
if (profile === null) {
return <p>Loading...</p>
}
return (
<React.Fragment>
<h1>@{profile.login}</h1>
<img
src={profile.avatar_url}
alt={`Avatar for ${profile.login}`}
/>
<p>{profile.bio}</p>
</React.Fragment>
);
}
-
So now anytime, the value username changes and once the component re-renders and the browser repaints the view, our effect will be invoked and the profile state will be synchronized with the result of the API request
-
Next up is the cleanup functions that are usually in the scenarios of using the subscription based calls
-
Cleanup functions will be invoked on two scenarios. First when the username changes, before the new effect is invoked with the new username
-
Second, right before Profile is removed from the DOM.. Below is the workflow example of useEffect cleanup function
import { subscribe, unsubscribe } from './api'
function Profile ({ username }) {
const [profile, setProfile] = React.useState(null)
React.useEffect(() => {
subscribe(username, setProfile)
return () => {
unsubscribe(username)
setProfile(null)
}
}, [username])
if (profile === null) {
return <p>Loading...</p>
}
return (
<React.Fragment>
<h1>@{profile.login}</h1>
<img
src={profile.avatar_url}
alt={`Avatar for ${profile.login}`}
/>
<p>{profile.bio}</p>
</React.Fragment>
);
}
-
Initial render
- username prop is set to yaminmhd
- profile state is set to null
- effect and cleanup function is set
- UI will show Loading…
- React will update the DOM and browser will repaint the DOM with new updates
- effect will run and setProfile will trigger re-render
-
Next render
- username prop is set to yaminmhd
- profile state is set to data from github api
- effect and cleanup function is set
- UI will show github profile data on screen
- React will update the DOM and browser will repaint the DOM with new updates
- effect will be skipped as username didn’t change
-
User event changes username yaminmhd to gitbot
- username prop is set to gitbot
- profile state is still information from previous user github info
- effect and cleanup function is set
- UI will show github profile data of yaminmhd on screen
- React will update the DOM and browser will repain the DOM with new updates
- Cleanup function gets invokes - () => unsubscribe(‘yaminmhd’), setProfile to null triggering a rerender
- effect get invoked - () => subscribe(‘gitbot’, setProfile)
-
Next render when setProfile is called in effect
- username prop is set to gitbot
- profile state is the new github profile data
- effect and cleanup is set
- UI will show profile info for new user
- React will update the DOM and browser will repaint the DOM with changes
- effect will be skipped since username didn’t change