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 -
- Mounting - When the component is prepared to be inserted onto the DOM.
- Updating - When the changes of state of the component are reflected onto the DOM.
- Unmounting - When the component is prepared to be removed from the DOM.
- Error Handling - When the component encounters any bug or error which needs to be caught.
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 -
- Errors from asynchronous code.
- Errors from Server Side components.
- Errors from Event handlers.
- 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;
}
}