React class and hooks lifecycle explained

2022-09-207 min
React class and hooks lifecycle explained

React lifecycle introduction

Component lifecycle is one of the most important concepts of React. Lifecycle methods give us a lot of possibilities to take control over the app updates and more!

Mounting

The moment of the first render of the Component (inserting into the DOM) is called "mounting". It can happen only once in component lifecycle.

Updating

This lifecycle method will update the DOM when the state or props change.

Unmounting

This lifecycle method removes the component from the DOM.

How we can interact with lifecycle in React?

Currently, we have two different ways of interacting with React component lifecycle. For more traditional "class components", we can use specific methods ("componentDidMount", "componentDidUpdate", "componentWillUnmount" and few more).

On the other hand, since React 16.8, we have got hooks that give us a lot of new possibilities. All of the hooks are awesome, but in my opinion, the game changer is two core hooks - "useEffect" which hooks up the lifecycle to the "functional component" and "useState" which makes the functional component stateful.

To better understand the basics of lifecycle methods and see the differences between traditional React usage and more modern functional approach, we should discuss both.

React class component methods

I won't go too deep into the class lifecycle since this topic is already mature and well covered. Instead, I'll try to make a quick recap of the most important aspects.

React API exposes a few lifecycle methods for class components. The most important of these are componentDidMount, componentDidUpdate, componentWillUnmount.

React class lifecycle

On the diagram above you can see the most important lifecycle methods. To see all of the methods, please visit this cool diagram page

Mounting and componentDidMount

Order of component mounting:

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

I hope you noticed that componentDidMount is called just after the constructor invocation and the first render. It means that the component is already applied to the DOM and you can interact with it. componentDidMount is a good place to set a subscription, timeout, interval or invoke an API request.

Updating with componentDidUpdate

An update is caused by props or state changes. It goes with the following order

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

componentDidUpdate happens just after the component re-render. This method is not called to the first render. You should use this method to make a request, set a state, or operate on the DOM. Please do not forget to compare current and previous props/state to avoid an infinite loop.

Unmounting the component

componentWillUnmount() is invoked just before a component is unmounted. You should use this method to clean up necessary timers, and subscriptions or cancel network requests.

React Hooks Lifecycle

In this section, I will explain how the lifecycle works with the functional component. To understand the React hooks lifecycle, we will use a cool diagram inspired by donavon/hook-flow

React Hooks Flow

So, what happens here?

Mounting

As you can see on the diagram above several things are happening in a specific order.

  • First react run lazy initialisers
  • First render
  • React updates DOM
  • Run LayoutEffects
  • Browser is painting the screen
  • Run Effects

What's going on here?

First goes lazy initializers, after that React does the first render and updates the DOM, then React runs LayoutEffects. The next activity is browser screen painting and after all, React run Effects.

At this moment We are ready to read from the DOM, trigger an API request, set interval, timeout, etc.

Updating

During each update, React starts from re-render caused by state or props change. There's no lazy initializers invocation.

  • Render
  • React updates DOM
  • Cleanup LayoutEffects
  • Run LayoutEffects
  • Browser is painting the screen
  • Cleanup Effects
  • Run Effects

Please notice that after rendering React clean up LayoutEffects to run these right after. The browser then draws the screen and after that React cleans up Effects and runs it just after.

The main difference between mounting and updating are:

  • Lazy initializers only on mount
  • Cleaning phase does not exist on the mount, because there is no reason to run it at this stage.

Unmounting

During unmounting React is cleaning up all effects.

  • Clean up LayoutEffects
  • Clean up Effects

How to use React.useEffect hook?

React hooks lifecycle

On the diagram above you can notice all about React.useEffect lifecycle. We can decide whether useEffect should be called on every component update, or only as a result of specific property/state changes.

To determine the useEffect call, we can manipulate the dependency array, which is the second argument of the useEffect hook.

Run only on mounting - Use an empty array as a second argument of useEffect hook

React.useEffect(() => {
      // function body
}, [])

Run on props/state update - There are two ways to call the useEffect function on component update

  • Pass dependency array as a second argument to run the hook only on specific props/state changes.
  • Skip the second argument to run the hook on each component update.
React.useEffect(() => {
    // it will be called only when "exampleProp" changes
}, [exampleProp])
React.useEffect(() => {
    // it will be called for every component update
})

Unmount - Clean this up

To clean up the subscriptions, API requests, intervals, etc, you need to return the callback function.

React.useEffect(() => {
    return () => {
        // unsubscribe
    }
}, ])

Be aware of running a cleanup function with "no dependency array" or with passed dependencies as a second argument. It will trigger the cleanup on dependencies change and might unsubscribe or remove your intervals etc.

React.useEffect(() => {
    return () => {
        // unsubscribe
        // this cleanup function will be triggered on each dependency change
    }
}, [dependency])

If you return the callback function for hook with an empty dependency array, the cleanup will run only once during component unmounting.

Proof of concept

To prove all of the theories We can take a look at a code snippet example.

In the code below I created the Parent and Child component. Parent component has

  • lazy initializer
  • render start log
  • render end log
  • useEffects logs
  • useEffects cleanups logs

Child component has

  • render start log
  • render end log
  • useEffects logs
  • useEffects cleanups logs
import * as React from 'react'

const Child = () => {
  console.log('%c Child Render start','background: #8024C9; color: white')
  React.useEffect(() => {
    console.log('%c Child - useEffect with []','background: #17E0C1; color: #230341')

    return () => console.log('%c Child - Cleanup useEffect with []','background: #17E0C1; color: #230341')
  }, [])

  console.log('%c Child Render end','background: #8024C9; color: white')
  return <p>child</p>
}

const Parent = () => {
  const [shouldRenderChild, setShouldRenderChild] = React.useState(() => {
    console.log('%c Parent Lazy initializer','background: #8024C9; color: white')
    return 0
  })

  console.log('%c Parent Render start','background: #8024C9; color: white')

  React.useEffect(() => {
    console.log('%c Parent - useEffect with []','background: #17E0C1; color: #230341')

    return () => console.log('%c Parent - Cleanup useEffect with []','background: #17E0C1; color: #230341')
  }, [])

  React.useEffect(() => {
    console.log('%c Parent - useEffect with [dependency]','background: #17E0C1; color: #230341')

    return () => console.log('%c Parent - Cleanup useEffect with [dependency]','background: #17E0C1; color: #230341')
  }, [shouldRenderChild])

  console.log('%c Parent Render end','background: #8024C9; color: white')
  return (
    <div>
      <button onClick={() => setShouldRenderChild((shouldRenderChild) => !shouldRenderChild)}>Should render child?</button>
      {shouldRenderChild ? <Child /> : null}
    </div>
  )
}

We expect to run all of it in the same order as seen on the hooks flow diagram above. There you can see the browser logs.

React useEffect order

Everything was exactly as We expected.

Conclusion

I believe we now have a perfect understanding of the component lifecycle, both classes, and functions. The two approaches are completely different, but ultimately both give us similar possibilities. If you're wondering which option is better for you, I can say - "It depends, there are many pros and cons to both, but maybe we could compare them in another article?". Stay tuned.