What StrictMode in React

What StrictMode in React

Everything you need to know about React.StrictMode

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 on List 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 components

  • functions 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.

Did you find this article valuable?

Support Frontend Delight by becoming a sponsor. Any amount is appreciated!