🐒 today and a small partner to discuss a problem, the whole process is quite interesting, as a small knowledge point to write down ~

Question:

Why does the following code print two ones?

function App() {
  setTimeout(() = >{
    console.log(1)},1)
  return 100
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>.document.getElementById('root'));Copy the code

Although this code looks strange, I tried it and printed it twice.

A quick search turned up the answer:

Zhihu: www.zhihu.com/question/38…

Making issue: github.com/facebook/re…

This is not a bug. And you’ll have the same behavior in Strict Mode too. We intentionally double-call render-phase lifecycles in development only (and function components using Hooks) to help people find issues caused by side effects in render. In our experience, them firing twice is enough for people to notice and fix such bugs.

If component output is always a function of props and state (and not outer scope variables, like in your example), the double rendering in development should have no observable effect.

React deliberately calls the Render phase twice during concurrent mode and Strict mode development to help developers locate problems, thinking it will get the developers’ attention.

In other words, there is something wrong with the above code: setTimeout is a Side Effect, but it is not written in useEffect. React alerts developers with this behavior (calling timeout twice).

The same is true if you replace timeout with Promise:

function App() {
  // debugger;
  Promise.resolve(1).then(() = > {
    console.log('testing',);
  })
  return 100
}
Copy the code

Asynchronous side effects that are not written in the useEffect are triggered twice in order to get the developer’s attention. This is also consistent with the idea of strict patterns: strict patterns.

In the App component after the debugger also found that it is indeed the render phase of two times. So that’s the end of the problem?

The old problem is under the brows, and the new one soon comes to mind:

I shared this conclusion with my partner, who found new problems in the process of verification:

function App() {
  console.log('render... ')
  setTimeout(() = >{
    console.log(1)},1)
  return 100
}
Copy the code

To verify that the App is called twice, it is natural to use console, as in the code above. But it didn’t work out as expected, and here’s what happened:

render...
1
1
Copy the code

Why did the timeout console go twice and the outer console go once? Don’t go through the Render phase twice? Wouldn’t the outer console have to go twice?

This is a really bad question

I even speculated that React did some parsing in Strict mode: when analyzing the statement for side effect, it inserted the same statement and executed code that wasn’t written by the developer.

But this speculation was quickly disproved because: 1) the behaviour did not fit the official explanation; 2) This is not what I just saw in the debug process;

Finally found the answer to the problem in the almighty stackOverflow (to say thank you, I decided to replace my computer’s Apple logo with a stackOverflow sticker and say YYds) :

Why is Promise.then called twice in a React component but not the console.log? (The questioner was asked almost the same question.)

This is Gao Zan’s answer:

In React strict mode react may run render multiple times, which could partly explain what you see.

But you correctly wondered if that was the case and render was called multiple times, why was render not printed twice too?

React modifies the console methods like console.log() to silence the logs in some cases. Here is a quote:

Starting with React 17, React automatically modifies the console methods like console.log() to silence the logs in the second call to lifecycle functions. However, it may cause undesired behavior in certain cases where a workaround can be used.

Apparently, it doesn’t do so when the console.log is called from Promise callback. But it does so when it is called from render. More details about this are in the answer by @trincot.

Paraphrasing briefly, he said:

Strict Mode’s two renderings only partially explain this. But you’re really wondering why not print it twice? React actually changes the console method so that it doesn’t print in some cases. He cites a citation saying that since Act17, the console method has been modified to not print on the second call to the lifecycle method, but this does cause some unexpected behavior.

StackOverflow’s second favorite answer is a little more detailed:

There is a second run of your render function when strict mode is enabled (only in development mode), but as discussed here, React will monkey patch console methods (calling disableLogs();) for the duration of that second (synchronous) run, so that it does not output.

He points out that the console was patched (monkey patch) during the second render of Development Mode and strict Mode, which caused the console to not output. The code for the timeout callback is not in the React Render context, so it is not affected.

In my opinion, this log-suppression is a really bad design choice.

if (__DEV__) {
  // ...
  if (
    debugRenderPhaseSideEffectsForStrictMode &&
    workInProgress.mode & StrictMode
  ) {
    disableLogs();       // <-- monkey patch fixes console
    try {                 
      // ...
    } finally {          
      reenableLogs();    // <-- restore console}}}Copy the code

In the pseudocode above, disableLogs modifies the console and reenableLogs restores the console.

React disableLogs = disableLogs = disableLogs = disableLogs = disableLogs Then change these methods to disableLogs with Object.defineProperties and increment the disabledDepth count by one. This variable is also used to restore the disableLogs pair with the reenableLogs method.

let disabledDepth = 0;
let prevLog;
let prevInfo;
let prevWarn;
let prevError;
let prevGroup;
let prevGroupCollapsed;
let prevGroupEnd;

function disabledLog() {}
disabledLog.__reactDisabledLog = true;

export function disableLogs() :void {
  if (__DEV__) {
    if (disabledDepth === 0) {
      /* eslint-disable react-internal/no-production-logging */
      prevLog = console.log;
      prevInfo = console.info;
      prevWarn = console.warn;
      prevError = console.error;
      prevGroup = console.group;
      prevGroupCollapsed = console.groupCollapsed;
      prevGroupEnd = console.groupEnd;
      // https://github.com/facebook/react/issues/19099
      const props = {
        configurable: true.enumerable: true.value: disabledLog,
        writable: true};// $FlowFixMe Flow thinks console is immutable.
      Object.defineProperties(console, {
        info: props,
        log: props,
        warn: props,
        error: props,
        group: props,
        groupCollapsed: props,
        groupEnd: props,
      });
      /* eslint-enable react-internal/no-production-logging */} disabledDepth++; }}Copy the code

So, go ahead and boldly verify that if you change console to alert in your APP, or use the native console reference, you should get the expected result:

const myPrint = console.log;

function App() {
  myPrint('render... ')
  setTimeout(() = >{
    console.log(1)},1)
  return 100
}
Copy the code

Experience proves, as expected also!

So, at this point, the problem is clear, two main points:

  1. Strict mode does render twice
  2. The console is modified and restored in the second rendering, which results in no output of the console in the second rendering;

Happy Mid-Autumn Festival everyone ~ 🥮 🌕 👬