React render twice on strict mode

Sean Hsieh-yoyo
6 min readMay 22, 2022

Question:

import { StrictMode } from "react"
import ReactDOM from "react-dom"
function Test(): React.ReactElement {
console.log('render')
Promise.resolve()
.then(() => console.log('then ' + Math.random()))
return <></>
}
ReactDOM.render(
<StrictMode>
<Test />
</StrictMode>,
document.getElementById("root")
)

output

render
then 0.5430663800781927
then 0.9667426372511254

react-github Concurrent mode renders components twice with different identities

https://github.com/facebook/react/issues/17786

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.

大概意思就是在 concurrent mode 和 strict mode 的開發階段,為了幫助開發者定位問題,react 會故意兩次調用render 階段

換句话说:setTimeout或是Promise 是一个Side Effect,但是卻没有寫在useEffect(放在提交階段)中。

react會在用這種行為(2次調用timeout)提醒開發者

在React strict mode 中,React 會多次調用render

在React 17 中有一段是這樣說的

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.

簡單來說,React 沒有更改Promise.then 所call 的console.log,但是如果是從render call的會魔改他

ref: https://stackoverflow.com/questions/68291908/why-is-promise-then-called-twice-in-a-react-component-but-not-the-console-log

總結:

組件的一次更新流程,在視圖真正刷新之前的部分都是可能被多次調用的,

因而這些部分中不能出現副作用,開發環境下會刻意觸發兩次以使得開發者能注意到誤用的副作用

提一下

Reconciliation Engine (調和引擎)

React的組件狀態更新( setState ) 是異步的。怎麼個異步呢?在調用 setState 以後,React 將會在未來的某個不確定的時間應用更新,再更新視圖。在新架構中,React會標記調用了 setState的組件為”待更新”, 然後在一個週期的分片中去處理這些更新,再更新結束後削除標記

在處理更新時,React 將流程分為兩個階段

  1. 渲染/調和階段 Render phase/ Reconciliation phase
  2. 提交階段 commit phase

在調和階段,React 將調用一些生命週期,處理需要更新的準備工作,最終得到視圖的映射; 而在提交階段,React 將把視圖映射更新到頁面「DOM」上,並調用其他生命週期函數

這裡需要注意的是,React會根據優先級調度每一個更新流程。React 通過定時確定時間片,並在每一個時間片週期執行調和階段的生命週期。但是這樣可能面臨一種現象:

時間片t0 中,進入了組建A的更新流程,觸發對應生命週期函數。但是當這一時間片結束時,即t1時刻,組件A的調和階段尚未執行結束,而出現了優先級很高的任務 — 組件B的更新流程。

這時,React 會終止組件A的更新流程,並從t1時刻起執行組件B的更新流程。如果沒有其他情況的話,假設組件B的更新流程在t2時間片內完成。組件A的更新流程將在t3時刻重啟

*注意:被打斷過的生命週期都將從頭開始順序執行

簡單翻譯一下,就是React 在執行你的組件更新流程中,可能遇到高優先級任務搶斷的情況,這樣的話等到組件更新被執行,相關的週期可能被二次,甚至更多次執行。因此,,處於調和階段的所有生命週期函數或鉤子必須具有冪等性,即為 沒有副作用(side effect),執行結果不隨調用次數改變。

因此,在開發模式下,React會讓組件更新的調和階段至少被觸發兩次,以方便開發者捕捉到不應該出現的副作用可能帶來的問題

最後,如何判斷一個生命週期函數或是鉤子是否屬於調和階段?

對於class 的更新週期,它包括:

  1. static getDerivedStateFromProps
  2. shouldComponentUpdate
  3. render

這裡順帶解釋有一個常見誤區,render方法的返回值並不直接導致視圖更新,而是僅作為”視圖更新的目標結果”, 被暫時存儲下來,在這一週期完整結束之後才由調和引擎計算需要進行的操作,再應用到視圖(真實DOM)上。因此, render 方法被調用後,視圖還沒有更新

4. getSnapshotBeforeUpdate

對於function ,即為可以出現副作用(=提交階段)的鉤子有

  1. useEffect
  2. useLayoutEffect

其他的鉤子,均屬於調和階段,不能出現副作用

ref https://juejin.cn/post/7009189602506309640

ref https://www.zhihu.com/question/387196401

--

--