Pre loader

What is React Strict Mode, and why is my Application Double Re-Rendering?

Categories

What is React Strict Mode, and why is my Application Double Re-Rendering?

React StrictMode is a development tool intended for producing hints about potential issues in React applications. With the release of React v18, Strict Mode was extended with a new feature which brought some unusual behaviour. Specifically, implicitly calling the rendering related functions extra times.
While this feature is useful for finding bugs, the unexpected flow it introduced compared to the normal React lifecycle may be redundant and requires extra effort to deal with in some applications.
In this article we explore what is React StrictMode, why this causes your application to double re-render, and how this impacts performance and memory when rendering charts in React apps. Finally, how to solve the problem by applying additional logic to handle strict mode.

What is Strict Mode in React?

React StrictMode applies a set of behaviours and warnings that can help spotting different kinds of React related bugs. The list includes incorrect logic of the lifecycle methods (e.g. missing cleanup logic) and usage of the deprecated APIs.

StrictMode can be enabled by wrapping a whole component tree or a part of it with <StrictMode> component:

import { StrictMode } from "react";

// ...

<StrictMode>
  <App />
</StrictMode>

The component doesn’t add any UI elements to the render result and doesn’t affect the production build as this is a development-only. React-based frameworks can expose a config option to toggle the StrictMode on and off, so it is worth to be aware if it is enabled by default.

What Happens in Strict Mode?

To help detecting a missing state reset or cleanup, React StrictMode executes extra calls of a render function as well as effect hooks. Extra execution of effects happens only upon an initial render, while the component function is always called twice.

The behaviour effect is similar for different arguments provided as an effect hook dependencies list. The most confusing and error-prone case is when a hook is expected to run only once. So let’s look at a component that has an effect hook with an empty dependencies list.

function TestComponent() {
  console.log("Rendering TestComponent");

  useEffect(() => {
    console.log("Executing Effect");

    return () => {
      console.log("Executing Effect Destructor");
    };
  }, []);

  return (
    <div className="App">
      <h1>Random generated number</h1>
      <h2>{Math.random()}</h2>
    </div>
  );
}

We will create a simple setup which allows us to render a component and then unmount it using a conditional rendering.

As expected during normal rendering we would get a following console output:

Rendering TestComponent
Executing Effect
Executing Effect Destructor // - appears after the component is unmounted

With StrictMode on:

Rendering TestComponent
Rendering TestComponent
Executing Effect
Executing Effect Destructor
Executing Effect
Executing Effect Destructor // - appears after the component is unmounted

As you can see useEffect is executed twice when StrictMode is enabled, and the cleanup function (or destructor) is also executed twice. This can cause unwanted effects, or performance problems if useEffect has a lot of code to execute, or if the cleanup function is not implemented properly.

Why Strict Mode is useful

So what do we get from this behaviour:

  • Calling the component twice could uncover potential issues of side effects that may occur within the render function.
  • Calling the effect and effect destructor twice guarantees that the destructor will run at least once, so the cleanup logic will be tested immediately.

So as result of these, an error is more likely to happen and thus more likely to be discovered during development. Usually these problems are caused by breaking the Rules of React.

Why Strict Mode may cause problems

Depending on the logic and structure of an application side effects and their cleanup could be implemented differently.

  • In some cases it may be harder to implement logic that would handle Strict Mode gracefully
  • Sometimes code that is breaking in Strict Mode could be still safely executed in production.
  • Enabling Strict Mode could make the app execution different from what it would be in production.
  • This could greatly affect the performance of the application as well.

The complexity of handling the cleanup logic becomes even more difficult in combination with asynchronous code.

Handling React StrictMode Correctly

Obviously if Strict Mode interrupts your development process a lot or you just don’t want to use it, you can always turn it off. The affected area of an application could be limited to specific component tree area.

But if that is not viable option or you need to create a sharable component that works properly with or without StrictMode, than you have to build the component accordingly to the Rules of React.

Preventing useEffect from running twice on mount

There are techniques that allow handling the double useEffect execution in StrictMode by conditionally preventing the effect logic execution in one of the calls. Be aware that they aren’t something you should use.

Let’s assume we have heavy computation logic that we don’t wont to be executed more than once when a component renders. Also the execution of such logic may create side effects which require cleanup.

const performSideEffectLogic = () => {
  console.log("Perform Side Effect");
};

const cleanupSideEffectResult = () => {
  console.log("Cleanup Side Effect Result");
};

function TestComponent() {
  console.log("Rendering TestComponent");

  useEffect(() => {
    console.log("Executing Effect");
    performSideEffectLogic();

    return () => {
      console.log("Executing Effect Destructor");
      cleanupSideEffectResult();
    };
  }, []);

  return (
    <div className="App">
      <h1>Random generated number</h1>
      <h2>{Math.random()}</h2>
    </div>
  );
}

If we want to just ignore the double execution of the effect and call performSideEffectLogic only once, we can add a flag to our component state that will identify if the effect has already been executed.
Since the second execution of the effect with empty dependencies list in same instance component would normally run only once, if it does otherwise we can treat it as being in Strict Mode.

const effectWasExecuted = useRef(false);

const [data, setData] = useState<number>(undefined);

useEffect(() => {
  console.log("Executing Effect");
  if (effectWasExecuted.current) {
    performSideEffectLogic();
  }

  const cleanup = () => {
    console.log("Cleanup");
    setData;
  };

  return () => {
    console.log("Executing Effect Destructor");

    if (process.env.NODE_ENV !== "production" && effectWasExecuted.current) {
      cleanupSideEffectResult();
    }
    effectWasExecuted.current = true;
  };
}, []);

Note that this is an easy workaround, but definitely not a recommended approach. Also StrictMode behaviour is related to the new features that may appear in React and this trick is likely to no longer work in the future releases.

The correct approach is to build logic the way it would handle the double effect execution.

How React Strict Mode Affects JavaScript Charts

When developing applications in React, it is critical it is to understand the proper lifecycle of a React component. This is amplified when an application uses heavy performance and memory operations.

The SciChart.js library is designed to be a high performance chart for use in data-intensive applications, and setup and cleanup of big-data charts needs to be made as efficient as possible. The performance metrics of SciChart.js are profiled often, and the documentation has many tips on how to use the library optimally for best performance in your application.

However, Strict Mode can cause double re-render which means setup and teardown logic of the charts can occur twice, impacting performance in development mode and even causing ‘false positive’ memory leaks.

Testing with a disabled Strict Mode is important as well as testing the production build, and ensuring that you adjust your application to handle and deal with double re-render.

How Double Re-Render Affects SciChart

SciChart.js is a high performance JavaScript Chart library that requires a few setup prerequisites.

First of all it loads a WebAssembly modules. This may require a network request and compilation execution. Both are asynchronous operations.

Then a chart could be created via an asynchronous function. A chart instance is an entity that requires an explicit cleanup by calling the sciChartSurface.delete() function. In addition to that SciChart creates DOM elements on a page.

This presents a tricky problem of embedding the logic into a component lifecycle. Things become even more confusing when the StrictMode behaviour takes effect.

How we solved the problem of StrictMode with SciChart-React

SciChart-React is an open source library which applies additional logic to handle both normal flow and strict mode when using SciChart.js – High Performance React Charts.

It doesn’t prevent the double execution of lifecycle methods. To handle async function execution we chain the deletion to to the init promise. Also we track if the component is mounted and check if the destructor was called. It is also important to set the state accordingly.

Here’s an extract from scichart-react SciChart.tsx file showing how we do this:

useEffect(() => {
        // generate guid to distinguish between effect calls in StrictMode
        const chartId = generateGuid();
        groupContext?.addChartToGroup(chartId, false, null);

        const rootElement = innerContainerRef.current;
        rootElement!.appendChild(chartRoot as Node);

        const initializationFunction = initChart
            ? initChart
            : (createChartFromConfig<TSurface>(config) as TInitFunction<TSurface, TInitResult>);

        let cancelled = false;

        const runInit = async (): Promise<TInitResult> =>
            initializationFunction(chartRoot as HTMLDivElement).then(result => {
                if (!result.sciChartSurface) {
                    throw new Error(wrongInitResultMessage);
                }
                // check if the component was unmounted before init finished
                if (isMountedRef.current && chartRoot) {
                    groupContext?.addChartToGroup(chartId, true, result);
                    initResultRef.current = result;
                    setIsInitialized(true);

                    if (onInit) {
                        cleanupCallbackRef.current = onInit(result);
                    }
                } else {
                    cancelled = true;
                }

                return result;
            });

        // workaround to handle StrictMode
        const initPromise = initPromiseRef.current ? initPromiseRef.current.then(runInit) : runInit();
        initPromiseRef.current = initPromise;

        const performCleanup = (initResult: TInitResult) => {
            if (!cancelled && cleanupCallbackRef.current) {
                cleanupCallbackRef.current();
                cleanupCallbackRef.current = undefined;
            }

            if (!cancelled && onDelete) {
                onDelete(initResult);
            }

            initResultRef.current = null;
            setIsInitialized(false);

            groupContext?.removeChartFromGroup(chartId);
            initResult.sciChartSurface!.delete();
        };

        return () => {
            // wait for init to finish before deleting it
            initPromise.then(performCleanup);
        };
    }, []);

    const groupContext = useContext(SciChartGroupContext);

    return (
        <!-- DOM elements omitted for brevity ->
    );

Using code like this we’re able to handle the asynchronous creation of SciChart.js charts and the proper cleanup, whether the application is run in strict mode or not. Extra handling around asynchronous calls is required to ensure that cleanup code is executed once only, and always when the component is unmounted.

Conclusion: Should you use React StrictMode?

We believe you should. Strict Mode will help you by reminding about proper component lifecycle handling and will emphasize the potential issues. Thus, it is worth using it.

However,

  • it is important to be aware whether an application has the mode enabled.
  • Application testing should better be performed both with the mode enabled and disabled.
  • You must ensure that you correctly handle useEffect cleanup, especially when initialization of a component has asynchronous code

Further Reading

Here’s some further resources you might find helpful related to this topic:

By Jim Risen | Jun 19, 2024

Leave a Reply