How to Fix TypeError: Cannot Read Properties of Null (Reading ‘createEvent’)

My team recently solved an error, “Cannot read properties of null (reading ‘createEvent’)” by upgrading to React v18.0. We were seeing this error occur almost daily in our CI pipeline as part of our React component tests:


/my-project/node_modules/react-dom/cjs/react-dom.development.js:3905
var evt = document.createEvent('Event');
^
TypeError: Cannot read properties of null (reading 'createEvent')
at Object.invokeGuardedCallbackDev (/my-project/node_modules/react-dom/cjs/react-dom.development.js:3905:26)
at invokeGuardedCallback (/my-project/node_modules/react-dom/cjs/react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (/my-project/node_modules/react-dom/cjs/react-dom.development.js:23543:11)
at unstable_runWithPriority (/my-project/node_modules/scheduler/cjs/scheduler.development.js:468:12)
at runWithPriority$1 (/my-project/node_modules/react-dom/cjs/react-dom.development.js:11276:10)
at flushPassiveEffects (/my-project/node_modules/react-dom/cjs/react-dom.development.js:23447:14)
at Object..flushWork (/my-project/node_modules/react-dom/cjs/react-dom-test-utils.development.js:992:10)
at Immediate. (/my-project/node_modules/react-dom/cjs/react-dom-test-utils.development.js:1003:11)
at processImmediate (node:internal/timers:466:21)

We could never reproduce the error locally or pin it on one specific test case. Rerunning the CI job usually resolved the issue, but eventually, it got too time-consuming, so I decided to track down the root cause.

Potential causes

Unhelpfully, it turns out that this is an error that can be caused by a number of different issues in your tests:

The first step to getting rid of this error is to make sure all your API calls are mocked and that you are correctly using async and await where needed. You might get lucky and find that it solves random failures.

Upgrade to React 18

I made sure the issues above were fixed, but the tests were still randomly failing with this error. It seemed like there must be some deeper issue.

After a lot of Googling, I finally came across this comment on a Jest pull request that seemed to describe the real issue we were facing:

I think this is not an issue with Jest, but a bug in React 17’s act implementation, when the act callback returns a promise. Then, after the promise resolves, React will synchronously flush all updates scheduled before resolving. But that code is buggy: it leaves the act environment before doing that flush, so these updates no longer work with the act queue, but are using the regular scheduler. setImmediate etc. And then they run long after the JSDOM env has been teared down.

React 18 has that fixed: it will flush the updates while still inside the act environment.

In your repro example, upgrading to React 18 in package.json immediately fixes the test for me.

We were in fact using React 17, and I decided to try upgrading to React 18. That did the trick, and we no longer had failing tests.

However, the upgrade did introduce some new errors, even though the tests were technically passing:


Warning: An update to [something] inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
/* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act

It seems like React 18 tightened up the rules around using act(), so we also had to make changes in many of our component tests.

Fix bad component testing patterns

Our component tests had several different bad patterns that we had to hunt down and fix:

  • Making userEvent calls outside of act. The main culprits were calls to userEvent.clear or userEvent.type. It’s just as important for those calls to be done inside of act as userEvent.click since they also usually change state.
  • Not using await for userEvent calls. All of the userEvent functions are asynchronous so they all need to be awaited.
  • Clicking components through screen. All interactions need to be done through a userEvent, not by calling things like screen.getByTestId('some id').click()

It took some experimentation, but we finally settled on following this pattern for all component unit tests that require user interactions:


import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ApiClient } from 'src/client/api-client';

// mock if needed
jest.mock('src/client/api-client');

it('my test case', async () => {
  // mock any API client behaviors if needed
  jest.mocked(ApiClient).doSomething.mockResolvedValue({
    success: true,
    data: {
      key: 'value',
    },
  });

  // render the component
  render(&ltMyComponent /&gt);

  // interact with the component - always do this within an act() call
  await act(async () => {
    // always use userEvent, not screen.getWhatever().click. and make sure to await the call
    await userEvent.click(screen.getByTestId('my-button'));
  });

  // do any assertions on expected API calls or UI appearance outside of the act() call
  await waitFor(() => expect(jest.mocked(ApiClient).doSomething).toHaveBeenCalled());
  await waitFor(() => expect(screen.getByTestId('something').toBeEnabled()));
  expect(screen.getByTestId('another-thing')).toHaveTextContent('some text');
});

The most important thing here is to perform any user interactions inside of act, and to always do it via a userEvent.

Once you fix any incorrect user interactions in your component tests, you shouldn’t see any errors in the test output or intermittent pipeline failures.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *