Read this before using React 18...

josiahayres

Josiah Ayres

Posted on May 21, 2022

Read this before using React 18...

React 18 was released on 29 March 2022.

After reading the How to Upgrade to React 18 blog post these were my key takeaway points:

  1. Updates to Client Rendering APIs, one minor change to apply in your main index.tsx file:
// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<App tab="home" />);
Enter fullscreen mode Exit fullscreen mode
  1. Updates to TypeScript definitions. Developers must now explicitly list the children prop when defining props, for example:
interface MyButtonProps {
  color: string;
  children?: React.ReactNode;
}
Enter fullscreen mode Exit fullscreen mode

The blog post explains the other changes and additions to React 18, however most React developers won't need to do anything.

This gave me confidence to jump right in to upgrading my application to React 18.

The Upgrade to 18

Straight away I noticed some weird behaviours:

Jumping into the console, I could see that my component "setup" useEffect, with an empty dependency array was being triggered twice.
I was sure I hadn't changed the component code, and could not understand why I was seeing my component double rendering.

Catastrophe No GIF by Cultura - Find & Share on GIPHY

Discover & share this Cultura GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.

favicon giphy.com

A quick Google search pointed to the <StrictMode /> component being the cause.

The (interim) fix

Changing my code from

import { StrictMode } from "react";
import * as ReactDOMClient from "react-dom/client";

import App from "./App";

const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

to

import * as ReactDOMClient from "react-dom/client";

import App from "./App";

const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);

root.render(
    <App />
);
Enter fullscreen mode Exit fullscreen mode

seemed to fix all the issues and double rendering of setup functions.

The cause

From the bottom end of the React 18 Upgrade Guide post they explain:

To demonstrate the development behavior you’ll see in Strict Mode with this feature, consider what happens when React mounts a new component. Without this change, when a component mounts, React creates the effects:

* React mounts the component.
  * Layout effects are created.
  * Effects are created.

With Strict Mode starting in React 18, whenever a component mounts in development, React will simulate immediately unmounting and remounting the component:

* React mounts the component.
    * Layout effects are created.
    * Effect effects are created.
* React simulates effects being destroyed on a mounted component.
    * Layout effects are destroyed.
    * Effects are destroyed.
* React simulates effects being re-created on a mounted component.
    * Layout effects are created
    * Effect setup code runs

On the second mount, React will restore the state from the first mount. This feature simulates user behavior such as a user tabbing away from a screen and back, ensuring that code will properly handle state restoration.

The proper fix

Adding in a return statement to the useEffect to handle the cleanup of the effect on unmount.

import { useEffect, useState } from "react";

const intervalInMs = 100;
const minuteInMs = 1000;

export const React18SafeTimer = () => {
  const [timeInSeconds, setTime] = useState(0);
  const [updateTimerInterval, setUpdateTimerInterval] = useState(0);

  useEffect(() => {
    console.log("React 18 Timer Setup");
    const timerInterval = setInterval(
      () => setTime((t) => t + intervalInMs / minuteInMs),
      intervalInMs
    );
    setUpdateTimerInterval(timerInterval);
    // THE FIX: Add next line to properly clean up useEffect
    return () => clearInterval(timerInterval);
  }, []);

  const handleClearTimer = () => {
    clearInterval(updateTimerInterval);
  };

  return (
    <>
      <p>seconds since page load: {timeInSeconds.toFixed(1)}</p>
      <button onClick={handleClearTimer}>Stop Timer</button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

See more at the Hooks API Reference page.

If you're having issues after upgrading to React 18, take a look at any of the following, as StrictMode changes around unmounting and remounting changes includes:

  • componentDidMount
  • componentWillUnmount
  • useEffect
  • useLayoutEffect
  • useInsertionEffect

See this page for more details.

Why should I use StrictMode?

StrictMode is a (development mode) tool for highlighting potential problems in an application. It does not run in Production environment. Like Fragment, StrictMode does not render any visible UI. It activates additional checks and warnings for its descendants.

StrictMode currently helps with:

  • Identifying components with unsafe lifecycles
  • Warning about legacy string ref API usage
  • Warning about deprecated findDOMNode usage
  • Detecting unexpected side effects
  • Detecting legacy context API
  • Ensuring reusable state
  • Additional functionality will be added with future releases of React.

Why has React made these changes?

In the future, we’d like to add a feature that allows React to add and remove sections of the UI while preserving state. For example, when a user tabs away from a screen and back, React should be able to immediately show the previous screen. To do this, React support remounting trees using the same component state used before unmounting.

This feature will give React better performance out-of-the-box, but requires components to be resilient to effects being mounted and destroyed multiple times. Most effects will work without any changes, but some effects do not properly clean up subscriptions in the destroy callback, or implicitly assume they are only mounted or destroyed once.

To help surface these issues, React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.

Additional discussions on GitHub that gave a lot more context into the "why" behind the changes to StrictMode:

Summary

React 18 introduces changes which can lead to an unexpected Developer experience and messier console logs with same messages being triggered twice. It also introduces different application behaviour when running in development mode vs production, which is arguably undesirable.

While these changes have been done for a good reason, and should be embraced rather than ignored, React devs need to know what to expect before upgrading. Hopefully this guide helps save you time and answers your questions.

💖 💪 🙅 🚩
josiahayres
Josiah Ayres

Posted on May 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related