Web Dev Simplified Blog

Everything You Need To Know About useEffect

April 27, 2020

In my last blog post I talked all about the useState hook in React. In this article I want to talk about the useEffect hook which I think is the best part of React hooks. The useEffect hook is perfect for handling side effects caused by mounting, un-mounting, changing state, etc.

From Classes To Functions

In order to understand how the useEffect hook works we first need to look at how side effects are managed in class components. For this article we are going to use a simple component which displays the window size and a list of items from a URL for all examples.

class WindowSizeList extends React.Component {
  constructor(props) {
    super(props)
    this.state = { windowWidth: window.innerWidth, items: [] }
    this.updateWindowWidth = this.updateWindowWidth.bind(this);
  }

  updateWindowWidth() {
    this.setState({ windowWidth: window.innerWidth })
  }

  componentDidMount() {
    window.addEventListener('resize', this.updateWindowWidth)
    this.setState({ items: CustomApi.getList(this.props.url) })
  }

  componentDidUpdate(prevProps) {
    if (prevProps.url !== this.props.url) {
      this.setState({ items: CustomApi.getList(this.props.url) })
    }
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.updateWindowWidth)
  }

  render() {
    return (
      <>
        <div>Window Width: {this.state.windowWidth}</div>
        {this.state.items.map(item => {
          return <div key={item}>{item}</div>
        })}
      </>
    )
  }
}

Essentially all this component does is display the window width and a list of items. There is also some basic code setup to manage changes to the window width or the url so we can update the list if the url for the list changes. Now let’s look at how we can convert this class component to a function component with useEffect. To start with we will use the following base code.

function WindowSizeList({ url }) {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth)
  const [items, setItems] = useState([])

  const updateWindowWidth = () => {
    setWindowWidth(window.innerWidth)
  }

  // TODO: Update list when url changes or on mount
  // TODO: Setup resize event listener on mount
  // TODO: Cleanup resize event listener on un-mount

  return (
    <>
      <div>Window Width: {windowWidth}</div>
      {items.map(item => {
        return <div key={item}>{item}</div>
      })}
    </>
  )
}

Creating Your First Side Effect

In the class component example all side effects are handled with life cycle methods. This makes it easy to define simple side effects, but once you start defining multiple side effects that need to be cleaned up it can become really confusing having them all crammed into a few life cycle methods. This is why the useEffect hook was created. With the useEffect hook, each side effect and all of its cleanup is defined in its own useEffect hooks.

The most basic way to use the useEffect hook is by passing a single function to useEffect. This function would be the side effect you want to run.

useEffect(() => {
  console.log('This is a side effect')
})

This side effect will now run on every single render of the component. That means when the component is first mounted, when the props change, and/or when the state changes. This is really nice since code no longer needs to be duplicated between the mounting and updating life cycle methods like in a class component. This obviously is not ideal if a side effect is only desired on mount or when certain props or state change. That is why useEffect takes an optional second parameter which is an array of values. This array of values is compared during each re-render with the previous render’s array values and the side effect will only be run if the values in the array changed since the last render. This means if you only want to run a side effect on mount then you can pass an empty array as the second parameter since that will never change between renders.

useEffect(() => {
  console.log('Only run on mount')
}, [])

Having this second array parameter is really nice since it allows side effects to be run whenever any value changes. For example if the url from our component changes we can run a side effect

useEffect(() => {
  console.log('Only run on url change')
}, [url])

With that knowledge we can actually write the code for updating our list when the url changes in our component.

function WindowSizeList({ url }) {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth)
  const [items, setItems] = useState([])

  const updateWindowWidth = () => {
    setWindowWidth(window.innerWidth)
  }

  useEffect(() => {    setItems(CustomApi.getList(url))  }, [url])  // TODO: Setup resize event listener on mount
  // TODO: Cleanup resize event listener on un-mount

  return (
    <>
      <div>Window Width: {windowWidth}</div>
      {items.map(item => {
        return <div key={item}>{item}</div>
      })}
    </>
  )
}

Cleaning Up Side Effects

We nearly have all the knowledge we need to setup the resize side effect, but right now we have no way to clean up a side effect. Luckily, cleaning up side effects with useEffect is really easy. If you return a function from the side effect inside useEffect then that function will be run every time the side effect is re-ran.

useEffect(() => {
  console.log('This is my side effect')

  return () => {
    console.log('This is my clean up')
  }
})

If we were to mount this component and then re-render it twice and then un-mount it you would get the following output.

// MOUNTED
// This is my side effect

// RE-RENDER 1:
// This is my clean up
// This is my side effect

// RE-RENDER 2:
// This is my clean up
// This is my side effect

// UN-MOUNT:
// This is my clean up

This is because the cleanup is run directly before the side effect is run as long as the side effect has occurred at least once. Also, the cleanup is run when a component un-mounts as well.

With this knowledge we now know everything we need in order to finish our component.

function WindowSizeList({ url }) {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth)
  const [items, setItems] = useState([])

  const updateWindowWidth = () => {
    setWindowWidth(window.innerWidth)
  }

  useEffect(() => {
    setItems(CustomApi.getList(url))
  }, [url])

  useEffect(() => {    window.addEventListener('resize', updateWindowWidth)    return () => {      window.removeEventListener('resize', updateWindowWidth)    }  }, [])
  return (
    <>
      <div>Window Width: {windowWidth}</div>
      {items.map(item => {
        return <div key={item}>{item}</div>
      })}
    </>
  )
}

Conclusion

Overall, useEffect drastically simplifies side effects in components by making it much easier to run side effects when props/state change. useEffect also makes organizing side effects easier since they are each given their own useEffect hook instead of being crammed into a few life cycle methods.


Kyle Cook

The official Web Dev Simplified blog by Kyle Cook.

Short and simple articles on web development.