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