Hi, I am Sanjeet Tiwari...
Let's talk about

Back to Notes

React Component Lifecycle

React

Introduction

Hooks were introduced to React in 2018, which changed the game completely as it gave then stateless functional components the ability to manage state, and added reactivity to them.

It gave us developers means to keep our React components concise, simple and readable when compared to the class component alternatives which, to be completely honest, had a bulky boilerplate.

In React, every component instance has got a state and a lifecycle, and some lifecycle methods were provided to developers so that they can run some code in different stages of the component lifecycle.

Developers can tap into these lifecycle methods in Class components as well as Functional components in different manners.

Lets take a look at all the different lifecycle methods and phases that a React Component goes through.

Phases of a React Component

A React Component instance, after creation, goes through 4 phases -

Every one of these phases has got some lifecycle method attached to them.

Mounting

constructor

The constructor is invoked as soon as a React component instance is created. It is the only method in the lifecycle where the state of the component can be directly updated.

class Comp extends React.Component {
    constructor(props) {
        super(props);
        this.state = { count = 1 };
        this.handleBtnClick = this.handleBtnClick.bind(this);
    }

    handleClick() {
        // ...some code
    }
}

In constructor, we assign a default state, call super(props) so that component can utilize the props passed to it via this.props and bind all the methods of the component so that there this points to the newly created component instance.

In modern JavaScript class syntax, this can be shortened further, so that it looks something like -

class Comp extends React.Component {
    this.state = { count = 1 }

    handleClick = () => {
        // ...some code
    }
}

A constructor should not contain any side effects or subscriptions. In Strict Mode, this constructor is called twice to create 2 instances of the component, and one of those components is discarded away just so that the developer can notice any unwanted accidental side effects.

We don’t have a constructor equivalent in Functional components, however, while using useState hook, we can pass an initializer function to the hook which is run only once upon state initialization.

const initializeCount = () => 2;

function Comp {
    const [count, setCount] = useState(initializeCount);
    // rest of the code
}

getDerivedStateFromProps

This method is called on every render regardless of the cause. It is used to keep the state and the props in sync. It’s a static method which cannot access the component’s current props and state. Instead, the incoming props and state are passed to it as parameters.

It returns an object which is nothing but the updated state, and returns a null if nothing needs to be updated. It is used in rare cases when the value of the state depends on whether some value in the prop changed or not overtime.

This method is invoked just before the render method is executed.

static getDerivedStateFromProps(props, state) {
    if (props.userID !== state.prevUserID) {
        return { userID: props.userID }
    }
}

Please note, this function requires you to keep track of previous value of a prop in the state.

Same thing can be done in functional components by calling the set<> method returned from useState hook directly in the function and not in an handler. The rule that is needed to be followed for such an implementation in functional components is to update the state only based upon a condition, otherwise it will result in an infinite loop.

function Comp(props) {
    const [prevCount, setPrevCount] = useState(props.count);
    const [message, setMessage] = useState('');

    if (props.count > prevCount) {
        setMessage('Something else')
    }

    // remaining code
}

This pattern is often avoided, and can lead to lots of unwanted bugs.

render

This is the only method that is required in a Class Component. It returns JSX elements which we want to display on the UI.

render method should be a pure function which means, it should be same if props (this.props), state (this.state) and context (this.context) are unchanged. Render method can conditionally show or hide UI elements via this.props,this.state or this.context.

render() {
    {this.props.enabled ? <h1>Enabled</h1> : <h1>Disabled</h1>}
}

In Functional Components, the render method transforms into the return method of the function in Functional Components. And the only output of these functions is an Element Tree.

Render should not contain any side effects or subscriptions.

componentDidMount

This is the place in this phase, where subscriptions can be setup, DOM nodes can be manipulated and some data can be fetched.

This is usually used alongside componentDidUpdate and componentWillUnmount, because whatever is set up in componentDidMount needs to be handled in componentDidUpdate, and needs to be undone in componentWillUnmount.

All 3 of the above functions can be emulated by useEffect in Functional Components. However, just componentDidMount alone is equivalent to -

useEffect(() => {
    // whatever needs to be done on the initial render
}, [])

Please take good notice of the empty dependency array.

Updating

getDerivedStateFromProps

Whenever a change is made to either the state or the props of the component, this method is the first one to be invoked again which can be utilized to keep the state in sync with props.

shouldComponentUpdate

The role of this function is to optimize the performance of the component by letting React know whether a re-render is in fact needed or not.

It gets the next set of props, state and context as its parameters which can be compared against this.props, this.state and this.context respectively to arrive at a decision to go ahead with the rerender or not.

The same role is played by memo in Functional components, which prevents a component to rerender if none of the props have changed. A function can be passed to it as well, as an optional parameter which determines whether the props are equal or not.

const newComp = React.memo(MyComp, arePropsEqual)

arePropsEqual(prevProps, newProps) {
    // do some computation
}

Here, if the arePropsEqual function returns true, then that means props are equal and re-render can be skipped.

render

After the decision has been made to re-render the component, render method is called again which outputs a new element tree based on new props, state and context.

getSnapshotBeforeUpdate

This function is called after render method and immediately before React updates the DOM. And is used to preserve any kind of information from the current render cycle, which can be used in the next render cycle.

The state has been updated by now, and prev props and state are available to this method as parameters, on the basis of which a value can be calculated which is needed in the next render cycle. Whatever value is returned from getSnapshotBeforeUpdate is made available as the 3rd parameter for componentDidUpdate.

A common use case for getSnapshotBeforeUpdate would be to preserve scroll positions in a chat window even after re-renders caused by new messages.

getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevProps.msgList.length < this.props.msgList.length) {
        // new messages were added
        // preserve the scroll position
        const list = this.listRef.current;
        return list.scrollHeight - list.scrollTop;
    }
}

componentDidUpdate(prevProps, prevState, snapshot) {
    // here snapshot is the same value returned by getSnapshotBeforeUpdate
    if (snapshot) {
        const list = this.listRef.current;
        list.scrollTop = list.scrollHeight - snapshot;
    }
}

There is no counterpart of getSnapshotBeforeUpdate for Functional Components. Already the use cases which force developers to use this method are very rare. But if in case, such a scenario is not avoidable, we need to use Class Components instead.

componentDidUpdate

This function is called when the component has re-rendered with updated state and props. This function can be used to handle all the sideEffects such as DOM updates or network requests caused by difference in props.

It also receives previous props and state as parameters, and also the value returned by getSnapshotBeforeUpdate.

componentDidUpdate(prevProps, prevState, snapshot) {
    // here snapshot is the same value returned by getSnapshotBeforeUpdate
    if (snapshot) {
        const list = this.listRef.current;
        list.scrollTop = list.scrollHeight - snapshot;
    }
}

This method is typically used with componentDidMount and componentWillUnmount, and has got a counterpart in Functional Components.

useEffect(() => {
    // whatever is written here will be executed on every render
})

and if some changes are needed to be executed only when certain props or state are updated -

useEffect(() => {
    // whatever is written here will be executed on every render
}, [var1, var2, var3])

where var1, var2 and var3 are dependencies, which when updated, causes the useEffect to re-run the callback with the updated values.

Unmounting

componentWillUnmount

This method is called just before the component is removed from the screen. componentWillUnmount is the exact mirror of componentDidMount, which means, this method is used undo everything that is done in componentDidMount. It can some subscriptions which were set up which are needed to be unsubscribed, and it can also be some data fetching requests that needs to be canceled.

Its counterpart in Functional Components can be achieved via the return callback of useEffect.

useEffect(() => {
    const subs = messages.subscribe();

    return () => {
        subs.unsubscribe();
    }
}, [])

Remember, the return of every useEffect callback is the componentWillUnmount utility which is responsible for handling cleanups.

Error handling

This phase exposes 2 lifecycle methods which are used to capture any errors encountered in the lifespan of the component. Whichever component makes use of these 2 methods are called Error Boundary components. The errors when encountered gets bubbled up to the nearest Error Boundary parent.

There are no counterparts of Error Boundaries in Functional Components. Although an NPM package - react-error-boundary gives us an already created Error Boundary Class component and provides us with different utilities to use it.

Not all errors are captured by these Error Boundaries, which includes -

  1. Errors from asynchronous code.
  2. Errors from Server Side components.
  3. Errors from Event handlers.
  4. Errors from the Error Boundary component itself.

componentDidCatch

This method is invoked as soon as an error is encountered in the React Component Tree (including distant children as well), and is used to record or log these errors somewhere. It is typically used with getDerivedStateFromError which sets a state value which inturn displays an error state UI onto the screen.

It takes in 2 params - error and info, wherein error is the Error object encapsulating the actual error which was encountered, and info containing more information about the error. If we want to view the stack trace of the error, we can do so by accessing info.componentStack.

getDerivedStateFromError

This static method is again invoked as soon as an error is encountered in the React Component Tree, and is used to set some state so that appropriate UI can be displayed to the user when the error has been encountered.

It receives only 1 parameter - the Error object, and returns an object which depicts the new state.

This is how Error Boundaries generally look like -

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    static getDerivedStateFromError(_error) {
        return { hasError: true }
    }
    
    componentDidCatch(error, _info) {
        console.error("An error has occurred", error);
    }

    render() {
        if (this.state.hasError) return this.props.fallback;
        return this.props.children;
    }
}

Source

Sanjeet's LinkedIn

Component

React Official Docs

Last updated on 20-09-2024