unigraphique.com

Testing Concurrency Features in React 18: A Comprehensive Guide

Written on

Chapter 1: Introduction to React 18

React 18 has generated significant interest, especially with its new concurrency features. Have you had the chance to explore this capability yet? When concurrency is activated, React transitions from "synchronous updates" to "asynchronous, prioritized, interruptible updates." This shift complicates the writing of unit tests. In this article, we will delve into how the React team has approached testing these concurrency features.

The Dilemma

There are two main challenges to address:

  1. Expressing the Rendering Result

    React interacts with renderers across different host environments. The most commonly used renderer is ReactDOM, which serves the "browser" and "Node environment" (SSR). In certain instances, the output from ReactDOM can be utilized for testing. For instance, the following code tests if a stateless component renders as expected using ReactDOM's output (with Jest as the testing framework):

it('should render stateless component', () => {

const el = document.createElement('div');

ReactDOM.render(<MyComponent />, el);

expect(el.textContent).toBe('A');

});

However, this method has a drawback; it relies on the DOM API and the browser environment (e.g., document.createElement), making it challenging to create test cases for scenarios like "React's internal runtime mechanism."

  1. Testing Concurrent Environments

    Changing ReactDOM.render to ReactDOM.createRoot in the previous example will cause the test to fail:

// before

ReactDOM.render(<MyComponent />, el);

expect(el.textContent).toBe('A');

// after

ReactDOM.createRoot(el).render(<MyComponent />);

expect(el.textContent).toBe('A');

This failure occurs because many previously "synchronous updates" have now turned into "concurrent updates," meaning the page is still rendering when the render method is called. To make the above test pass, you can modify it as follows:

ReactDOM.createRoot(el).render(<MyComponent />);

setTimeout(() => {

// synchronous

expect(el.textContent).toBe('A');

});

How Does React Respond to This Change?

Next, we will explore how the React team has tackled these challenges, starting with the first question: How should the rendering result be expressed? Unlike React Native, which operates in a Native environment, ReactDOM targets the browser and Node environments.

Can we create a renderer specifically designed to examine the "internal runtime flow"? The answer is yes. This renderer is known as React-Noop-Renderer. Essentially, this renderer will only deal with JavaScript objects.

Implementing a Renderer

Internally, React has a package called Reconciler that references several APIs meant to "manipulate the host environment." For example, this method is utilized to "insert nodes into the container":

function appendChildToContainer(child, container) {

// Implement

}

In the browser context (ReactDOM), this is done using the appendChild method:

function appendChildToContainer(child, container) {

container.appendChild(child);

}

The Reconciler package is bundled with the aforementioned "API for browser environment" via the rollup tool, which constitutes the ReactDOM package. In React-Noop-Renderer, the DOM nodes in ReactDOM align with the following data structures:

const instance = {

id: instanceCounter++,

type: type,

children: [],

parent: -1,

props

};

Note the children field, which stores the child nodes. Thus, the appendChildToContainer method can be simplified in React-Noop-Renderer as follows:

function appendChildToContainer(child, container) {

const index = container.children.indexOf(child);

if (index !== -1) {

container.children.splice(index, 1);

}

container.children.push(child);

};

The packaging tool incorporates the Reconciler package along with the "API for React-Noop," creating the React-Noop-Renderer package. This allows for the testing of Reconciler's internal logic completely independent of the normal host environment.

How to Test Concurrent Environments

Regardless of the complexity of the "concurrency features," they ultimately boil down to "various strategies for executing code asynchronously." The APIs that carry out these operations include setTimeout, setInterval, Promise, etc. In Jest, you can simulate these asynchronous APIs and manage their execution timing. For instance, the asynchronous code above would look like this in a React test:

await act(() => {

ReactDOM.createRoot(el).render(<MyComponent />);

});

expect(el.textContent).toBe('A');

The act method from the jest-react package will execute jest.runOnlyPendingTimers, triggering any waiting timers. For example:

setTimeout(() => {

console.log('go');

}, 9999999);

jest.runOnlyPendingTimers(); // Outputs "go" immediately

This method allows for the control of React’s concurrent update pace without interfering with the framework’s code. Additionally, the Scheduler module, responsible for driving concurrent updates, has its own testing version. In this version, developers can manually manage the Scheduler’s input and output. For instance, if you want to test the execution order of the useEffect callback during component unmounting, you might write:

function Parent() {

useEffect(() => {

return () => Scheduler.unstable_yieldValue('Unmount parent');

});

return <Child />;

}

function Child() {

useEffect(() => {

return () => Scheduler.unstable_yieldValue('Unmount child');

});

return 'Child';

}

await act(async () => {

root.render(<Parent />);

});

expect(Scheduler).toHaveYielded(['Unmount parent', 'Unmount child']);

Summary

The strategy for creating test cases in React involves:

  • Use cases measurable with ReactDOM, typically combined with ReactDOM and ReactTestUtils (a browser environment helper).
  • For use cases requiring control of the intermediate process, utilize the Scheduler's test package and Scheduler.unstable_yieldValue to track process information.
  • To test React's internal processes separately from the host environment, employ React-Noop-Renderer.
  • For concurrent scenario testing, use the aforementioned tools in conjunction with jest-react.

For those interested in further exploring testing techniques in React, I recommend checking out RubyLouvre's work, which operates through over 800 React use cases and includes a simplified version of ReactTestUtils and React-Noop-Renderer.

Stay connected with me on Twitter, LinkedIn, and GitHub! Writing is my passion, and I enjoy helping and inspiring others. If you have any questions, don’t hesitate to reach out!

More content at PlainEnglish.io. Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord!

Chapter 2: Further Learning Resources

Discover the key changes in React 18, including concurrency, transitions, and suspense.

Learn about concurrent rendering in React 18 in this insightful video by Ifeoma Imoh.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Break Free from the Comparison Trap: Embrace Your Authentic Self

Discover how to escape the comparison trap and embrace your true self for a happier and more fulfilled life.

Navigating Digital Disillusionment: A Journey Towards Hope

Exploring the disillusionment with digital life and the hope for a better future through technology.

Safety First: The Roller Coaster Incident at Happy Valley Shenzhen

A tragic roller coaster accident at Happy Valley Shenzhen underscores the critical importance of safety in amusement parks.

Debunking Common Myths About the American Civil War

A look into misconceptions surrounding the American Civil War, challenging the narratives about slavery, Lincoln, and Northern opposition.

The Indispensable Role of Glass in Our Lives

Explore the essential role of glass in daily life, its historical significance, and the impact of lenses on our understanding of the world.

Essential Mac Applications for Software Engineers in 2022

Discover the top Mac apps for software engineers that enhance productivity and streamline workflows.

Navigating Your Aspirations: Passion, Wealth, or Recognition?

Explore the importance of understanding your aspirations and the role of persistence in achieving them.

Reinventing Yourself: Embrace Change and Find Joy in Life

Discover strategies to transform your life and embrace change for personal growth.