How act() works inside react? 
Tags: react, testing, javascript, inside
Published: 2021-02-17 22:30:49

Hola! Lazy dev here. 

React testing is hard. Especially react testing outside the browser environment, like with Jest and JSdom. Let's try to reverse engineer react's `act()`, understand why do we need it, and think about UI testing overall.

## History

Today I meet this tweet by @floydophone 

{% twitter 1362123964412096513 %}

And was inspired to write about how your tests work inside your terminal when you are testing in node.js. Let's start from the question – why do we need this "magic" `act()` function. 

## What is act() 

Here is a quote from [react.js docs](https://reactjs.org/docs/test-utils.html#act): 

> To prepare a component for assertions, wrap the code rendering it and performing updates inside an act() call. This makes your test run closer to how React works in the browser.

So the problem that `act()` is solving – **It delays your tests until all of your updates were applied** before proceeding to the next steps. When you are doing any kind of user interaction, like this 

```js
act(() => {
  button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
``` 

React is not updating UI immediately, thanks to the Fiber architecture. It will update it asynchronously in some time after the click, so we need to wait for UI to be updated. 

## And here is a problem 

The main problem here – `act()` is actually a crutch and you will probably agree that it is not a perfect solution. Tests that you (probably) are writing are synchronous. It means that commands and assertions that tests are doing are executed one-by-one without any waiting. 

UI works differently – UI is async by nature. 

## Reverse engineer it 

Let's look more closely at the implementation of this function, right from the react sources. We only need 2 files [ReactTestUtilsPublicAct](https://github.com/facebook/react/blob/b93f3e7d2d595444c6ee2964d4cce2e17f4ffa28/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js) and [ReactFiberWorkLoop](https://github.com/facebook/react/blob/e2fd460cca3b11fa0f5505f324043a9b2d9db894/packages/react-reconciler/src/ReactFiberWorkLoop.new.js). 

I will skip not interesting parts, but the code is not so big so you can read it yourself 🙃 Let's start from the main point of the act function: 

```ts
  let result;
  try {
    result = batchedUpdates(callback);
  } catch (error) {
    // on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
    onDone();
    throw error;
  }
```

And this magic `batchedUpdates` function has a pretty simple yet powerful implementation. 

```ts
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}
```

This particular function is called inside the react when during the render phase react exactly knows that all updates are done and we can render the dom. And it starts the reconciliation and synchronous dom updating after.

After `batchedUpdates` our code has 2 branches depends on how you used it. If you passed a synchronous function inside the `act`, like

```jsx
 act(() => {
   ReactDOM.render(<Counter />, container);
 });
```

It will call the function `flushWork` which is nothing more than a sync `while` loop  

```ts
const flushWork =
  Scheduler.unstable_flushAllWithoutAsserting ||
  function() {
    let didFlushWork = false;
    while (flushPassiveEffects()) {
      didFlushWork = true;
    }

    return didFlushWork;
  };
```

> It looks like for concurrent mode a new specific hook implemented to stop all the effects together (`unstable_flushAllWithoutAsserting`)

But for now, it is just a sync while loop that stops the synchronous execution until all the DOM updating work is done. Pretty clumsy solution, don't you think? 

## Async execution 

More interesting is coming when you are passing an async function as a callback. Lets go to another code branch: 

```ts
if (
  result !== null &&
  typeof result === 'object' &&
  typeof result.then === 'function'
)

// ... not interesting

result.then(
  () => {
    if (
      actingUpdatesScopeDepth > 1 ||
      (isSchedulerMocked === true &&
        previousIsSomeRendererActing === true)
    ) {
      onDone();
      resolve();
      return;
    }
    // we're about to exit the act() scope,
    // now's the time to flush tasks/effects
    flushWorkAndMicroTasks((err: ?Error) => {
      onDone();
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  },
  err => {
    onDone();
    reject(err);
  },
);
```

Here we are waiting for our passed callback (the `result` is returned by `batchedUpdates` function) and if after we are going to more interesting function `flushWorkAndMicroTasks`. Probably the most interesting function here :) 

```ts

function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
  try {
    flushWork();
    enqueueTask(() => {
      if (flushWork()) {
        flushWorkAndMicroTasks(onDone);
      } else {
        onDone();
      }
    });
  } catch (err) {
    onDone(err);
  }
}
```

It is doing the same as the sync version (that only calling `flushWork()`). But it wraps the call `enqueueTask`, which is a hack only to avoid `setTimeout(fn, 0)`. 

{% details an enqueueTask function %}
```ts
export default function enqueueTask(task: () => void) {
  if (enqueueTaskImpl === null) {
    try {
      // read require off the module object to get around the bundlers.
      // we don't want them to detect a require and bundle a Node polyfill.
      const requireString = ('require' + Math.random()).slice(0, 7);
      const nodeRequire = module && module[requireString];
      // assuming we're in node, let's try to get node's
      // version of setImmediate, bypassing fake timers if any.
      enqueueTaskImpl = nodeRequire.call(module, 'timers').setImmediate;
    } catch (_err) {
      // we're in a browser
      // we can't use regular timers because they may still be faked
      // so we try MessageChannel+postMessage instead
      enqueueTaskImpl = function(callback: () => void) {
        if (__DEV__) {
          if (didWarnAboutMessageChannel === false) {
            didWarnAboutMessageChannel = true;
            if (typeof MessageChannel === 'undefined') {
              console.error(
                'This browser does not have a MessageChannel implementation, ' +
                  'so enqueuing tasks via await act(async () => ...) will fail. ' +
                  'Please file an issue at https://github.com/facebook/react/issues ' +
                  'if you encounter this warning.',
              );
            }
          }
        }
        const channel = new MessageChannel();
        channel.port1.onmessage = callback;
        channel.port2.postMessage(undefined);
      };
    }
  }
  return enqueueTaskImpl(task);
}
```
{% enddetails %}

The main goal of this function is **only** to execute a callback in the next tick of the event loop. That's probably why react is not the best in terms of bundle size :) 

## Why async? 

It is a pretty new feature, probably needed more for concurrent mode, but it allows you to immediately run stuff like `Promise.resolve` aka microtasks for example when mocking API and changing real promise using `Promise.resolve` with fake data. 

```jsx
import * as ReactDOM from "react-dom";
import { act } from "react-dom/test-utils";

const AsyncApp = () => {
  const [data, setData] = React.useState("idle value");

  const simulatedFetch = async () => {
    const fetchedValue = await Promise.resolve("fetched value");
    setData(fetchedValue);
  };

  React.useEffect(() => {
    simulatedFetch();
  }, []);

  return <h1>{data}</h1>;
};


let el = null;
beforeEach(() => {
  // setup a DOM element as a render target
  el = document.createElement("div");
  // container *must* be attached to document so events work correctly.
  document.body.appendChild(el);
});

it("should render with the correct text with sync act", async () => {
  act(() => {
    ReactDOM.render(<AsyncApp />, el);
  });
  expect(el.innerHTML).toContain("idle value");
});

it("should render with the correct text with async act", async () => {
  await act(async () => {
    ReactDOM.render(<AsyncApp />, el);
  });

  expect(el.innerHTML).toContain("fetched value");
});
```

Both tests passing 😌. Here is a live example (you can open sandbox and run tests inside using "Tests" tab): 

{% codesandbox eloquent-bhaskara-vl9pw module=/src/App.test.js %}

It is fun that it works, but if you will change `Promise.resolve` to literally anything like this: 

```js
const fetchedValue = await new Promise((res, rej) => {
  setTimeout(() => res("fetched value"), 0);
});

// it doesn't work ¯\_(ツ)_/¯
```

## Replace 

It is pretty easy to replace any `act()` call by using simple `setTimeout(fn, 0)`: 

```js
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
await new Promise((res, rej) => {
 setTimeout(res, 0);
});
```

will work in most cases :) [some sources](https://codesandbox.io/s/cool-buck-2mqez?file=/src/Counter.test.js)

## But why

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9w63kf1jav2j7gzpm2kj.png)
 
The main question – why do we need it? So much ~not good~ code that confuses everybody? The answer – our tests that are running inside node.js and trying to be "sync" while the UI as **async**. 
 
And that's why you will never need any kind of `act()` if you are rendering React components in the real browser and using async test-runner, like [Cypress for component testing](https://docs.cypress.io/guides/component-testing/introduction.html#What-is-Cypress-Component-Testing) 

## Thank you

Thank you for reading, hope it is more clear why do we need `act()` for most plain react unit testing. 

And no `act()` was not harmed in the making of this article :D