A Deep Dive into React Class Lifecycle Methods

Mounting, Update, Unmounting, and Error Handling stage methods

Ayush Verma
JavaScript in Plain English

--

What are React lifecycle methods?

You can think of React lifecycle methods as the series of events that happen from the birth of a React component to its death.

Every component in React goes through a lifecycle of events. I like to think of them as going through a cycle of birth, growth, and death.

  • Mounting — Birth of your component. This is the stage where the Component is inserted into the DOM.
  • Update — Growth of your component. This is the stage in which the Component’s state and props can change, leading to the process of re-rendering the Component with the updated state/props
  • Unmount — Death of your component. This is the final stage of the Component’s life, in which it is removed from the DOM.

Now that we understand the series of lifecycle events let’s learn more about how they work.

The Component Lifecycle

Each component has several “lifecycle methods” that you can override to run code at particular times in the process. You can use this lifecycle diagram as a cheat sheet. In the list below, commonly used lifecycle methods are marked as bold. The rest of them exist for relatively rare use cases.

Mounting

These methods are called in the following order when an instance of a component is being created and inserted into the DOM:

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

Note:These methods are considered legacy and you should avoid them in new code: UNSAFE_componentWillMount().

1. constructor ()

constructor(props)

The constructor for a React component is called before it is mounted. When implementing the constructor for a React.Component subclass, you should call super(props) before any other statement. Otherwise, this.props will be undefined in the constructor, which can lead to bugs.

Typically, in React constructors are only used for two purposes:

  • Initializing local state by assigning an object to this.state.
  • Binding event handler methods to an instance.

You should not call setState() in the constructor(). Instead, if your component needs to use a local state, assign the initial state to this.state directly in the constructor:

constructor(props) {
super(props);
// Don't call this.setState() here!
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}

If you don’t initialize state and you don’t bind methods, you don’t need to implement a constructor for your React component.

Constructor is the only place where you should assign this.state directly. In all other methods, you need to use this.setState() instead.

Avoid introducing any side-effects or subscriptions in the constructor. For those use cases, use componentDidMount() instead.

Note: Avoid copying props into state! This is a common mistake:

constructor(props) {
super(props);
// Don't do this!
this.state = { color: props.color };
}

The problem is that it’s both unnecessary (you can use this.props.color directly instead), and creates bugs (updates to the color prop won’t be reflected in the state). Only use this pattern if you intentionally want to ignore prop updates.

Most Common Use Case For Constructor: Setting up the state, Creating refs, and Method binding.

2. static getDerivedStateFromProps()

static getDerivedStateFromProps(props, state)

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates. This method exists for rare use cases where the state depends on changes in props over time.

This is a static function that does not have access to “this“. getDerivedStateFromProps() returns an object to update state in response to prop changes. It can return a null if there is no change to the state.

static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
// Return null to indicate no change to state.
return null;
}

An example use case where this method may come in handy would be a <Transition> component that compares its previous and next children to decide which ones to animate in and out.

Keep in mind that this lifecycle method is fired on every render, regardless of the cause. This is in contrast to UNSAFE_componentWillReceiveProps, which only fires when the parent causes a re-render and not as a result of a local setState.

Deriving state leads to verbose code and makes your components difficult to think about. Make sure you’re familiar with simpler alternatives:

  • If you need to perform a side effect (for example, data fetching or an animation) in response to a change in props, use componentDidUpdate lifecycle instead.
  • If you want to re-compute some data only when a prop changes, use a memoization helper instead.

The derived state is used to ensure an expensive value used in render is recomputed only when the inputs change. This technique is known as memoization. Using derived state for memoization isn’t necessarily bad, but it’s usually not the best solution. There is inherent complexity in managing derived states, and this complexity increases with each additional property. For example, if we add a second derived field to our component state then our implementation would need to separately track changes to both.

Let’s look at an example of one component that takes one prop — a list of items — and renders the items that match a search query entered by the user. We could use the derived state to store the filtered list:

class Example extends Component {
state = {
filterText: "",
};
// *******************************************************
// NOTE: this example is NOT the recommended approach.
// See the examples below for our recommendations instead.
// *******************************************************
static getDerivedStateFromProps(props, state) {
// Re-run the filter whenever the list array or filter text change.
// Note we need to store prevPropsList and prevFilterText to detect changes.
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}

This implementation avoids recalculating filteredList more often than necessary. But it is more complicated than it needs to be because it has to separately track and detect changes in both props and state in order to properly update the filtered list. We could add a memoization helper to avoid unnecessarily re-filtering our list:

import memoize from "memoize-one";class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
}

This is much simpler and performs just as well as the derived state version!

When using memoization, remember a couple of constraints:

  1. In most cases, you’ll want to attach the memoized function to a component instance. This prevents multiple instances of a component from resetting each other’s memoized keys.
  2. Typically you’ll want to use a memoization helper with a limited cache size in order to prevent memory leaks over time. (In the example above, we used memoize-one because it only caches the most recent arguments and result.)
  3. None of the implementations shown in this section will work if props.list is recreated each time the parent component renders. But in most cases, this setup is appropriate.
  • If you want to “reset” some state when a prop changes, consider either making a component fully controlled or fully uncontrolled with a key instead.

Fully controlled component — One way to avoid the problems mentioned above is to remove state from our component entirely. If the email address only exists as a prop, then we don’t have to worry about conflicts with state. We could even convert EmailInput to a lighter-weight function component:

function EmailInput(props) {
return <input onChange={props.onChange} value={props.email} />;
}

This approach simplifies the implementation of our component, but if we still want to store a draft value, the parent form component will now need to do that manually.

Fully uncontrolled component with a keyAnother alternative would be for our component to fully own the “draft” email state. In that case, our component could still accept a prop for the initial value, but it would ignore subsequent changes to that prop:

class EmailInput extends Component {
state = { email: this.props.defaultEmail };
handleChange = event => {
this.setState({ email: event.target.value });
};
render() {
return <input onChange={this.handleChange} value={this.state.email} />;
}
}

In order to reset the value when moving to a different item, we can use the special React attribute called key. When a key changes, React will create a new component instance rather than update the current one. Keys are usually used for dynamic lists but are also useful here. In our case, we could use the user ID to recreate the email input any time a new user is selected:

<EmailInput
defaultEmail={this.props.user.email}
key={this.props.user.id}
/>

Each time the ID changes, the EmailInput will be recreated and its state will be reset to the latest defaultEmail value.

Most Common Use Case For getDerivedStateFromProps (during mount): Returning a state object based on the initial props.

3. render()

render()

The render() method is the only required method in a class component.

When called, it should examine this.props and this.state and return one of the following types:

  • React elements. Typically created via JSX. For example, <div /> and <MyComponent /> are React elements that instruct React to render a DOM node, or another user-defined component, respectively. Below is an example of a simple render() in React.
class Hello extends Component{
render(){
return <div>Hello {this.props.name}</div>
}
}
  • Arrays and fragments. Let you return multiple elements from render.
class Hello extends Component{
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
}
  • Portals. Let you render children into a different DOM subtree.
class Hello extends Component{
render() {
return ReactDOM.createPortal(
this.props.children,
domNode
);
}
}
// React does *not* create a new div. It renders the children into `domNode`.
// `domNode` is any valid DOM node, regardless of its location in the DOM.
  • String and numbers. These are rendered as text nodes in the DOM.
  • Booleans or null. Render nothing. (Mostly exists to support return test && <Child /> pattern, where test is boolean.)
class Hello extends Component {
render() {
return null;
}
}

The render() function should be pure, meaning that it does not modify component state, it returns the same result each time it’s invoked, and it does not directly interact with the browser. This means that you can not setState() within a render().

Most Common Use Case For Render: Returning component JSX.

4. componentDidMount()

componentDidMount()

Now your component has been mounted and ready, that’s when the next React lifecycle method componentDidMount() comes into play. This is a good place to initiate API calls if you need to load data from a remote endpoint.

Unlike the render() method, componentDidMount() allows the use of setState(). Don’t call setState() here directly unless it is in the then block of a promise after you send an HTTP request. But don’t call setState() in here synchronously. So you can definitely set up some code that executes in the future, which then updates the state.

Calling the setState() directly here will update the state and cause another rendering but it will happen before the browser updates the UI. This is to ensure that the user will not see any UI updates with the double rendering.

You can modify the component state within the componentDidMount(), but use it with caution.

Caution: It is recommended that you use this pattern with caution since it could lead to performance issues. The best practice is to ensure that your states are assigned in the constructor(). The reason React allows the setState() within this lifecycle method is for special cases like tooltips, modals, and similar concepts when you would need to measure a DOM node before rendering something that depends on its position.

componentDidMount() {
fetch(`https://api.mydomain.com/`)
.then(res => res.json())
.then(json => this.setState({ data: json }));
}
async componentDidMount() {
const response = await fetch(`https://api.mydomain.com/`);
const json = await response.json();
this.setState({ data: json });
}

Most Common Use Case for componentDidMount: Starting AJAX calls to load in data for your component. Also if you need to initialize anything that relies on the DOM, you can do this here (e.g. initializing third-party libraries like D3). And last but not least, you can add event listeners inside componentDidMount.

5. UNSAFE_componentWillMount()

UNSAFE_componentWillMount()

Note:
This lifecycle was previously named
componentWillMount. That name will continue to work until version 17. Use the rename-unsafe-lifecycles codemod to automatically update your components.

UNSAFE_componentWillMount() is invoked just before mounting occurs. It is called before render(), therefore calling setState() synchronously in this method will not trigger an extra rendering. Generally, we recommend using the constructor() instead for initializing state.

The componentWillMount() is a chance for us to handle configuration, update our state, and in general prepare for the first render. At this point, props and initial state are defined. We can safely query this.props and this.state, knowing with certainty they are the current values. This means we can start performing calculations or processes based on the prop values.

componentWillMount() {
let mode;
if (this.props.age > 70) {
mode = 'old';
} else if (this.props.age < 18) {
mode = 'young';
} else {
mode = 'middle';
}
this.setState({ mode });
}

In the example above we call this.setState() and update our current state before render. If we need state values on calculations passed in props, this is where we should do the logic.

Avoid introducing any side-effects or subscriptions in this method. For those use cases, use componentDidMount() instead.

The exception is any setup that can only be done at runtime — namely, connecting to external API’s. For example, if you use Firebase for your app, you’ll need to get that set up as your app is first mounting.

But the key is that such configuration should be done at the highest level component of your app (the root component). That means 99% of your components should probably not use componentWillMount.

This is the only lifecycle method called on server rendering.

Most Common Use Case: App configuration in your root component, Registering to global events such as a Flux store. If your Component needs to respond to global Native UI events, such as window resizing or focus changes, this is a good place to do it

Here side effects refer to sending HTTP requests or storing something in your local storage or sending analytics to Google Analytics.

Updating

An update can be caused by changes to props or state. These methods are called in the following order when a component is being re-rendered:

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

Note:
These methods are considered legacy and you should
avoid them in new code:UNSAFE_componentWillUpdate()and UNSAFE_componentWillReceiveProps()

1. shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState)

This lifecycle can be handy sometimes when you don’t want React to render your state or prop changes. The default behavior is to re-render on every state change, and in the vast majority of cases, you should rely on the default behavior.

  • shouldComponentUpdate() is invoked before rendering when new props or state are being received. Defaults to true. This method is not called for the initial render or when forceUpdate() is used.
  • This method only exists as a performance optimization. Do not rely on it to “prevent” a rendering, as this can lead to bugs. Consider using the built-in PureComponent instead of writing shouldComponentUpdate() by hand. PureComponent performs a shallow comparison of props and state, and reduces the chance that you’ll skip a necessary update.
  • If you are confident you want to write it by hand, you may compare this.props with nextProps and this.state with nextState and return false to tell React the update can be skipped. Note that returning false does not prevent child components from re-rendering when their state changes.

We do not recommend doing deep equality checks or using JSON.stringify() in shouldComponentUpdate(). It is very inefficient and will harm performance.

Currently, if shouldComponentUpdate() returns false, then UNSAFE_componentWillUpdate(), render(), and componentDidUpdate() will not be invoked. In the future, React may treat shouldComponentUpdate() as a hint rather than a strict directive, and returning false may still result in a re-rendering of the component.

shouldComponentUpdate(nextProps, nextState) {
return this.props.title !== nextProps.title ||
this.state.input !== nextState.input
}

As shown in the example above, this lifecycle should always return a boolean value to the question, “Should I re-render my component?”.

Most Common Use Case: Controlling exactly when your component will re-render.

2. getSnapShotBeforeUpdate()

getSnapshotBeforeUpdate(prevProps, prevState)
  • getSnapshotBeforeUpdate() is invoked right before the most recently rendered output is committed to e.g. the DOM. It enables your component to capture some information from the DOM (e.g. scroll position) before it is potentially changed. Any value returned by this lifecycle method will be passed as a parameter to componentDidUpdate().
  • This use case is not common, but it may occur in UIs like a chat thread that need to handle scroll position in a special way.
  • A snapshot value (or null) should be returned.

For example:

class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}

In the above examples, it is important to read the scrollHeight property in getSnapshotBeforeUpdate because there may be delays between “render” phase lifecycles (like render) and “commit” phase lifecycles (like getSnapshotBeforeUpdate and componentDidUpdate).

Keep in mind that this method should also be used rarely or not used at all.

Resizing the window during an async rendering is a good use-case of when the getSnapshotBeforeUpdate() can be utilized.

Most Common Use Case: Taking a look at some attribute of the current DOM, and passing that value on to componentDidUpdate.

3. componentDidUpdate()

componentDidUpdate(prevProps, prevState, snapshot)
  • componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
  • Use this as an opportunity to operate on the DOM when the component has been updated. This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}

You may call setState() immediately in componentDidUpdate() but note that it must be wrapped in a condition like in the example above, or you’ll cause an infinite loop. Here also, don’t call setState() here directly unless it is in the then block of a promise after you send an HTTP request. It's fine to do as a result of some async tasks but don’t call setState() in here synchronously. If called, it would also cause an extra re-rendering which, while not visible to the user, can affect the component performance. If you’re trying to “mirror” some state to a prop coming from above, consider using the prop directly instead.

If your component implements the getSnapshotBeforeUpdate() lifecycle (which is rare), the value it returns will be passed as a third “snapshot” parameter to componentDidUpdate(). Otherwise, this parameter will be undefined.

Note: componentDidUpdate() will not be invoked if shouldComponentUpdate() returns false.

Most Common Use Case: Updating the DOM in response to prop or state changes, Update of a 3rd party UI library like D3 to pass on the new data.

4. UNSAFE_componentWillReceiveProps()

UNSAFE_componentWillReceiveProps(nextProps)

Note: This lifecycle was previously named componentWillReceiveProps. That name will continue to work until version 17. Use the rename-unsafe-lifecycles codemod to automatically update your components.

  • UNSAFE_componentWillReceiveProps() is invoked before a mounted component receives new props. If you need to update the state in response to prop changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions using this.setState() in this method.
  • Note that if a parent component causes your component to re-render, this method will be called even if props have not changed. Make sure to compare the current and next values if you only want to handle changes.
  • React doesn’t call UNSAFE_componentWillReceiveProps() with initial props during mounting. It only calls this method if some of component’s props may update. Calling this.setState() generally doesn’t trigger UNSAFE_componentWillReceiveProps().
componentWillReceiveProps(nextProps){
if(this.props.id !== nextProps.id){
this.setState({
id: nextProps.id,
pastId: this.props.id
})
}
}

Most Common Use Case: Acting on particular prop changes to trigger state transitions i.e if you have a state that is a calculation from multiple props, you could do the calculation here.

5. UNSAFE_componentWillUpdate()

UNSAFE_componentWillUpdate(nextProps, nextState)

Note: This lifecycle was previously named componentWillUpdate. That name will continue to work until version 17. Use the rename-unsafe-lifecycles codemod to automatically update your components.

  • UNSAFE_componentWillUpdate() is invoked just before rendering when new props or state are being received. Use this as an opportunity to perform preparation before an update occurs. This method is not called for the initial render.
  • Note that you cannot call this.setState() here; nor should you do anything else (e.g. dispatch a Redux action) that would trigger an update to a React component before UNSAFE_componentWillUpdate() returns.

Typically, this method can be replaced by componentDidUpdate(). If you were reading from the DOM in this method (e.g. to save a scroll position), you can move that logic to getSnapshotBeforeUpdate().

Note:UNSAFE_componentWillUpdate() will not be invoked if shouldComponentUpdate() returns false.

// dispatching an action based on state change
componentWillUpdate(nextProps, nextState) {
if (nextState.open == true && this.state.open == false) {
this.props.onWillOpen();
}
}

Most Common Use Case: Used instead of componentWillReceiveProps on a component that also has shouldComponentUpdate (but no access to previous props), set a variable based on state changes, dispatching events/actions, and starting animations.

Unmounting

This method is called when a component is being removed from the DOM:

  • componentWillUnmount()
componentWillUnmount()

componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, clearing any caches in storage, canceling network requests, or cleaning up any subscriptions that were created in componentDidMount().

You should not call setState() in componentWillUnmount() because the component will never be re-rendered. Once a component instance is unmounted, it will never be mounted again.

componentWillUnmount() {
clearInterval(this.interval);
}
componentWillUnmount() {
window.removeEventListener('resize', this.resizeListener)
}

Error Handling

These methods are called when there is an error during rendering, in a lifecycle method, or in the constructor of any child component.

  • static getDerivedStateFromError()
  • componentDidCatch()

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

A class component becomes an error boundary if it defines either (or both) of the lifecycle methods static getDerivedStateFromError() or componentDidCatch(). Updating state from these lifecycles lets you capture an unhandled JavaScript error in the below tree and display a fallback UI.

Only use error boundaries for recovering from unexpected exceptions; don’t try to use them for control flow.

Note: Error boundaries only catch errors in the components below them in the tree. An error boundary can’t catch an error within itself.

static getDerivedStateFromError()

static getDerivedStateFromError(error)

This lifecycle is invoked after an error has been thrown by a descendant component. It receives the error that was thrown as a parameter and should return a value to update the state.

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}

Note:getDerivedStateFromError() is called during the “render” phase, so side-effects are not permitted. For those use cases, use componentDidCatch() instead.

componentDidCatch()

componentDidCatch(error, info)

This lifecycle is invoked after an error has been thrown by a descendant component. It receives two parameters:

  1. error - The error that was thrown.
  2. info - An object with a componentStack key containing information about which component threw the error.

componentDidCatch() is called during the “commit” phase, so side-effects are permitted. It should be used for things like logging errors:

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// Example "componentStack":
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logComponentStackToMyService(info.componentStack);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}

Production and development builds of React slightly differ in the way componentDidCatch() handles errors.

On development, the errors will bubble up to window, this means that any window.onerror or window.addEventListener('error', callback) will intercept the errors that have been caught by componentDidCatch().

On production, instead, the errors will not bubble up, which means any ancestor error handler will only receive errors not explicitly caught by componentDidCatch().

Note: In the event of an error, you can render a fallback UI with componentDidCatch() by calling setState, but this will be deprecated in a future release. Use static getDerivedStateFromError() to handle fallback rendering instead.

Conclusion

We have covered the lifecycle of a React component consists of three stages: Mounting, Updating, and Unmounting.

Also, you’ve learned that React calls a certain set of lifecycle methods at each of those stages. You can use them according to the use case you want to fulfill.

Thanks for reading!

More content at plainenglish.io

--

--