As we gain more experience, we constantly evolve our coding practices and design patterns. This is the case with React too.
React has also gone through many transitions, and as it has progressed, certain practices that were believed to be good in the past are no longer fit for the future roadmap.
One significant change happened with the release of v16 where it went through a re-write onto React Fiber’s architecture. The major focus was on scheduling (i.e. deciding when a piece of work should be performed while keeping in mind the priorities of different tasks such as animations, UI updates, and so on).
At about the same time, a new Context API was added in React.
Also, intending to provide Concurrent Mode in future versions where the rendering phase is split into multiple parts, a lot of change has come about. The start of it saw the introduction of React Hooks, deprecation of certain lifecycle methods, and more.
This article will look at all the deprecated patterns that StrictMode
in React helps us identify.
What Is React.StrictMode and How Can We Use It?
React.StrictMode
is a tool for highlighting potential problems in an application. It works by rendering it as a component encapsulating either part of or your entire application. StrictMode
does not render any visible element in the DOM but enables certain checks and provides warnings in development mode.
Note: StrictMode
doesn’t run any checks or show warnings in production mode.
You can enable React.StrictMode
for your entire application like so:
import ReactDOM from 'react-dom';
import React from 'react';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
<React.StrictMode>,
document.getElementById("app")
);
You can similarly enable it in part of your application by wrapping it with <React.StrictMode>
.
The following functionalities are supported in StrictMode
as of v17 of React:
Identifying legacy string refs.
Detecting deprecated
findDOMNode
method.Detecting use of legacy Context API.
Detecting unsafe lifecycle methods that have been deprecated by React.
Detecting unexpected side effects in React components.
1. Identifying Legacy String refs
Refs in the initial versions of React were assigned using strings. However, there were many problems associated with it, as pointed out by Dan Abramov in this Github Issue:
“It requires that React keeps track of currently rendering component (since it can’t guess
this
). This makes React a bit slower.It doesn’t work as most people would expect with the “render callback” pattern (e.g.
<List renderRow={this.renderRow} />
) because the ref would get placed onList
for the above reason.It is not composable, i.e. if a library puts a ref on the passed child, the user can’t put another ref on it. Callback refs are perfectly composable.”
For these reasons and many others, such as the issues with typing refs in TypeScript where they need to be casted, better alternatives were introduced for class components:
Callback refs
React.createRef
2. Detecting deprecated findDOMNode
method
The ReactDOM.findDOMNode
method was previously used to get the DOM node given the class instance. The usage of findDOMNode
can always be avoided by adding a ref directly to the DOM element instead of the class instance.
There are two main problems with the findDOMNode
API:
This would only return the first child in a class component instance. However, with the introduction of Fragments in v16, you could return multiple elements from a component instance and this could cause a problem, as you may want to target a wrapper of all the elements or a specific element from the list of elements returned.
The
findDOMNode
API was request-only (i.e. it would evaluate and return the result when it was called). If, for instance, the rendered element is conditionally changed in the child, the parent may not know about it.
The alternative to findDOMNode
is to use React.forwardRef
and pass on the ref to the desired element in the child or to pass the ref by a separate name (such as innerRef
) and use it from props in the child component to set a ref on the desired element.
3. Legacy Context API
Version 16.3 of React introduced a new Context API. Before this, the old error-prone API was in use and would cause the consumers to not update if a component somewhere in the parent hierarchy stopped re-renders of the children element by implementing shouldComponentUpdate
.
Even though React continues to support the old API in v16.x, StrictMode
will point out the usages of the old Context API by showing warnings so that these can be moved to the latest version.
4. Detecting unsafe lifecycle methods
In v16.3.0 of React, some breakthrough changes were made to the React APIs. One of those changes was the deprecation of lifecycle methods like componentWillMount
, componentWillReceiveProps
, and componentWillUpdate
. New lifecycles were also added, such as getDerivedStateFromProps
and getSnapShotBeforeUpdate
.
Although these lifecycle methods continue to be available in further versions of React and have been renamed with a prefix UNSAFE_
added to them, React may remove them altogether in future versions.
Why were these lifecycle methods deprecated?
To understand this, we must first know that React typically works in two phases:
Render phase: During this phase, React checks what changes need to be made to the DOM. React invokes a render
function during this phase and compares the result with the previous render. The render phase lifecycles included componentWillMount
, componentWillReceiveProps
, componentWillUpdate
, and render
.
Commit phase: This is the phase during which React actually commits the changes to the DOM and invokes commit phase lifecycles such as componentDidMount
and componentDidUpdate
.
The commit phase is fast, but the render phase can be slow. To optimize it with the vision of Concurrent Mode, React decided to break the rendering into pieces and pause and resume work to avoid blocking the browser.
So when they do this, the render phase lifecycles could be called multiple times, and if these contain side effects or incorrect practices, they may cause the application to behave inconsistently. Also, some of these lifecycles encourage bad developer practices. These include:
componentWillMount
componentWillReceiveProps
componentWillUpdate
Let us look at a few of these practices.
Calling setState in componentWillMount
// Incorrect
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentWillMount() {
this.setState({
selectedTheme: this.props.defaultTheme,
})
}
// Rest of code
}
// Correct approach
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedTheme: props.defaultTheme,
};
}
// Rest of code
}
As you can see in the snippet above, componentWillMount
was used to set a state before the initial render, but that can easily be refactored by setting the initial state in the constructor or with state
as a class property.
Async request in componentWillMount
Having an async fetch request in componentWillMount
is problematic for both server-side rendering as well as the upcoming Concurrent Mode. With server-side rendering, the data fetched in componentWillMount
will not be used. With async rendering, the fetch request may go multiple times.
// Incorrect way to fetchData
class ExampleComponent extends React.Component {
state = {
data: []
}
componentWillMount() {
fetchData().then(res => {
this.setState({
data: res.data
});
})
}
// Rest of the code
}
// Correct way to fetchData and update state
class ExampleComponent extends React.Component {
state = {
data: [],
isLoading: true,
}
componentDidMount() {
fetchData().then(res => {
this.setState({
data: res.data,
isLoading: false
});
})
}
// Rest of the code
}
There is a common misconception that any data fetched inside componentWillMount
will be available before the initial render. This is not true and you should use a loading state to avoid using the data in the initial render and make an API call to fetch data in componentDidMount
.
Adding subscriptions or listeners in componentWillMount
There are two problems with adding subscriptions/listeners in componentWillMount
:
With server-side rendering, the
componentWillUnmount
function is not called on the server and hence cleanups will not happen and may result in memory leaks.With async rendering, multiple subscriptions may be attached, as rendering phase lifecycles may be invoked multiple times.
// Incorrect way
class ExampleComponent extends React.Component {
componentWillMount() {
this.unlisten = this.props.dataSource.listen(
this.handleDataSourceChange
);
}
componentWillUnmount() {
this.unlisten();
}
handleDataSourceChange = data => {};
}
// Correct way
class ExampleComponent extends React.Component {
componentDidMount() {
this.unlisten = this.props.dataSource.listen(
this.handleDataSourceChange
);
}
componentWillUnmount() {
this.unlisten();
}
handleDataSourceChange = data => {};
}
The correct way to add and remove listeners is to pair up the componentDidMount
and componentWillUnmount
lifecycle methods.
Updating state or calling side effects on prop change
Previously, the componentWillReceiveProps
lifecycle was used for updating state or calling side effects in the children whenever parent props changed. Although there was not much wrong with it, developers had a misconception that this lifecycle was only called when props updated.
However, it was invoked whenever parent props re-rendered.
So any invocation of functions or state updates may have inconsistent behaviors if not done properly after comparing previous and current props.
Reading DOM properties before an update
Sometimes you may want to save certain DOM properties, such as scroll position before an update to revert it when the update is applied to prevent the items currently in view for the user from going out of view if new items are added or removed.
Previously, you would do so in the componentWillUpdate
lifecycle method. However, with async rendering, there may be a gap between the time when componentWillUpdate
is called and when componentDidUpdate
is called, which may lead to inconsistencies if the user interacted with the DOM in a way that actually changed the scroll position, such as resizing the window or actually scrolling more content. getSnapshotBeforeUpdate
is suggested as an alternative to componentWillUpdate
for this reason since it is called just before the DOM mutations are made.
Now that we have gone through a few reasons as to why the usages were removed, let us get back to the point.
We may be tempted to think, “Why do we even need some utility to point us to the unsafe functions? We can simply search and update them with the recommended practices.”
While you are correct and can do so in your own code base, you will not be able to easily identify unsafe lifecycles within libraries that you use as dependencies in your codebase. StrictMode
will help you point those out too so that you can update them (or replace them with alternatives if the latest versions are not compatible).
5. Detecting unexpected side effects
As we established in the previous section that React wanted to optimize the rendering phase in the upcoming Concurrent Mode, it decided to break down the rendering phase. As a result, rendering phase lifecycles can be called multiple times, causing unexpected behaviors if side effects are used within them.
In the latest version of React, these functions include:
constructor
getDerivedStateFromProps
shouldComponentUpdate
render
setState
updater functions in both class and functional componentsfunctions passed to
useMemo
,useState
,useReducer
While side effects are non-deterministic, StrictMode
helps by making it a little more deterministic to the developer by double-invoking the functions above. This way, if any side effect is incorrectly written in a rendering phase function, it can be in Development Mode itself due to the obvious inconsistencies presented by it.
For example, if a WebSocket connection is being established in a constructor
function, a double invocation of constructor
in Development Mode can help make it easier to spot, as two connections will be established.
Key Takeaways
React.StrictMode
can be enabled for part of or the entire application.It is only run in Development Mode to provide warnings for legacy ref usage, the deprecated
findDOMNode
method, the legacy Context API, unsafe lifecycles, and unexpected side effects.StrictMode
leads to an intentional double invocation of rendering phase lifecycles and functions to make it easier to spot unexpected side effects implemented in these functions.
Thank you for reading.
If you found this article useful and informative, please don't forget to like and share it with your friends and colleagues.
If you have any suggestions, please feel free to comment.
Follow me on Twitter for more web development content.
Top comments (1)
Thank you Luke. I am super happy you found this useful.