描述UI
- React 組件是一段可以 使用標簽進行擴展 的 JavaScript 函數。
- React 組件是常規的 JavaScript 函數,但 組件的名稱必須以大寫字母開頭
- 同一文件中,有且僅有一個默認導出,但可以有多個具名導出!
- 為什么需要一個根標簽
- JSX 雖然看起來很像 HTML,但在底層其實被轉化為了 JavaScript 對象,你不能在一個函數中返回多個對象,除非用一個數組把他們包裝起來。這就是為什么多個 JSX 標簽必須要用一個父元素或者 Fragment 來包裹。
- 切勿將數字放在 && 左側.左側為0則會渲染為0
- key 需要滿足的條件
- key 值在兄弟節點之間必須是唯一的。 不過不要求全局唯一,在不同的數組中可以使用相同的 key。
- key 值不能改變,否則就失去了使用 key 的意義!所以千萬不要在渲染時動態地生成 key。
- 為什么需要key
- React 里需要 key 和文件夾里的文件需要有文件名的道理是類似的。一個精心選擇的 key 值所能提供的信息遠遠不止于這個元素在數組中的位置。即使元素的位置在渲染的過程中發生了改變,它提供的 key 值也能讓 React 在整個生命周期中一直認得它。
- 請不要在運行過程中動態地產生 key,像是 key={Math.random()} 這種方式。這會導致每次重新渲染后的 key 值都不一樣,從而使得所有的組件和 DOM 元素每次都要重新創建。這不僅會造成運行變慢的問題,更有可能導致用戶輸入的丟失。
- 純函數的基本定義:
- 只負責自己的任務。 它不會更改在該函數調用前就已存在的對象或變量。
- 輸入相同,輸出也相同。 在輸入相同的情況下,對純函數來說應總是返回相同的結果。
- React 假設你編寫的所有組件都是純函數。React 的渲染過程必須自始至終是純粹的。組件應該只返回它們的 JSX,而不改變在渲染前,就已存在的任何對象或變量 — 這將會使它們變得不純粹!
- React 提供了 “嚴格模式”,在嚴格模式下開發時,它將會調用每個組件函數兩次。通過重復調用組件函數,嚴格模式有助于找到違反這些規則的組件。
- 副作用:包括更新屏幕、啟動動畫、更改數據等,它們被稱為 副作用。它們是 “額外” 發生的事情,與渲染過程無關。
- 在 React 中,副作用通常屬于 事件處理程序。事件處理程序是 React 在你執行某些操作(如單擊按鈕)時運行的函數。即使事件處理程序是在你的組件 內部 定義的,它們也不會在渲染期間運行! 因此事件處理程序無需是純函數。
- 如果你用盡一切辦法,仍無法為副作用找到合適的事件處理程序,你還可以調用組件中的
useEffect
方法將其附加到返回的 JSX 中。這會告訴 React 在渲染結束后執行它。然而,這種方法應該是你最后的手段。
添加交互
-
e.stopPropagation()
阻止觸發綁定在外層標簽上的事件處理函數。 -
e.preventDefault()
阻止少數事件的默認瀏覽器行為。
事件處理函數是執行副作用的最佳位置,數據的改變使用state存儲
-
為什么需要useState
- 局部變量無法在多次渲染中持久保存。當 React 再次渲染這個組件時,它會從頭開始渲染——不會考慮之前對局部變量的任何更改。
- 更改局部變量不會觸發渲染。 React 沒有意識到它需要使用新數據再次渲染組件。
Hooks ——以
use
開頭的函數——只能在組件或自定義 Hook 的最頂層調用。如果你渲染同一個組件兩次,每個副本都會有完全隔離的 state
-
React 把更改提交到 DOM 上
-
對于初次渲染, React 會使用
appendChild()
DOM API 將其創建的所有 DOM 節點放在屏幕上。 - 對于重渲染, React 將應用最少的必要操作(在渲染時計算!),以使得 DOM 與最新的渲染輸出相互匹配。
-
對于初次渲染, React 會使用
React 僅在渲染之間存在差異時才會更改 DOM 節點
在渲染完成并且 React 更新 DOM 之后,瀏覽器就會重新繪制屏幕
-
在一個 React 應用中一次屏幕更新都會發生以下三個步驟:
- 觸發
- 組件的 初次渲染。
- 組件(或者其祖先之一)的 狀態發生了改變
- 渲染。在您觸發渲染后,React 會調用您的組件來確定要在屏幕上顯示的內容。“渲染中” 即 React 在調用您的組件。
- 在進行初次渲染時, React 會調用根組件。
- 對于后續的渲染, React 會調用內部狀態更新觸發了渲染的函數組件。
- 提交。在渲染(調用)您的組件之后,React 將會修改 DOM。
-
對于初次渲染, React 會使用
appendChild()
DOM API 將其創建的所有 DOM 節點放在屏幕上。 - 對于重渲染, React 將應用最少的必要操作(在渲染時計算!),以使得 DOM 與最新的渲染輸出相互匹配。
設置 state 會觸發渲染(當你調用 useState 時)
-
渲染會及時生成一張快照
- “正在渲染” 就意味著 React 正在調用你的組件——一個函數。你從該函數返回的 JSX 就像是 UI 的一張及時的快照。它的 props、事件處理函數和內部變量都是 根據當前渲染時的 state 被計算出來的。
- 返回的 UI “快照”是可交互的。它其中包括類似事件處理函數的邏輯,這些邏輯用于指定如何對輸入作出響應。React 隨后會更新屏幕來匹配這張快照,并綁定事件處理函數。因此,按下按鈕就會觸發你 JSX 中的點擊事件處理函數。
- state 實際上“活”在 React 本身中——就像被擺在一個架子上!——位于你的函數之外。當 React 調用你的組件時,它會為特定的那一次渲染提供一張 state 快照。你的組件會在其 JSX 中返回一張包含一整套新的 props 和事件處理函數的 UI 快照 ,其中所有的值都是 根據那一次渲染中 state 的值 被計算出來的!
設置 state 只會為下一次渲染變更 state 的值。
一個 state 變量的值永遠不會在一次渲染的內部發生變化, 即使其事件處理函數的代碼是異步的。
React 會使 state 的值始終”固定“在一次渲染的各個事件處理函數內部。 你無需擔心代碼運行時 state 是否發生了變化。
變量和事件處理函數不會在重渲染中“存活”。每個渲染都有自己的事件處理函數。
每個渲染(以及其中的函數)始終“看到”的是 React 提供給這個 渲染的 state 快照。
過去創建的事件處理函數擁有的是創建它們的那次渲染中的 state 值。
React 會等到事件處理函數中的 所有 代碼都運行完畢再處理你的 state 更新。
在下次渲染前多次更新同一個 state,需要傳入一個更新函數,如setNumber(n => n + 1),而不是像 setNumber(number + 1) 這樣傳入 下一個 state 值
設置 state 不會更改現有渲染中的變量,但會請求一次新的渲染。
React 會在事件處理函數執行完成之后處理 state 更新。這被稱為批處理。
要在一個事件中多次更新某些 state,你可以使用 setNumber(n => n + 1) 更新函數。
把所有存放在 state 中的 JavaScript 對象都視為只讀的,為了真正地 觸發一次重新渲染,你需要創建一個新對象并把它傳遞給 state 的設置函數
不要直接修改一個對象,而要為它創建一個 新 版本,并通過把 state 設置成這個新版本來觸發重新渲染。
你可以使用這樣的 {...obj, something: 'newValue'} 對象展開語法來創建對象的拷貝。
狀態管理
- 只要一個組件還被渲染在 UI 樹的相同位置,React 就會保留它的 state。 如果它被移除,或者一個不同的組件被渲染在相同的位置,那么 React 就會丟掉它的 state。
- 相同位置的相同組件會使得 state 被保留下來
- 對 React 來說重要的是組件在 UI 樹中的位置,而不是在 JSX 中的位置!
- 當你在相同位置渲染不同的組件時,組件的整個子樹都會被重置
- 永遠要將組件定義在最上層(函數組件外)并且不要把它們的定義嵌套起來,否則每次渲染出的都是不同的組件,狀態會丟失
- 在相同位置重置 state
- 將組件渲染在不同的位置。同一個組件在第一個位置與第二個位置交替展示時會重置state
- 使用 key 來重置 state。即使兩個 <Counter /> 會出現在 JSX 中的同一個位置,它們也不會共享 state
- 使用key后,React 將重新創建 DOM 元素,而不是復用它們。
- 為被移除的組件保留 state
- 用 CSS 把其他組件隱藏起來。這些組件就不會從樹中被移除了,所以它們的內部 state 會被保留下來。這種解決方法對于簡單 UI 非常有效。但如果要隱藏的樹形結構很大且包含了大量的 DOM 節點,那么性能就會變得很差。
- 你可以進行狀態提升并在父組件中保存每個收件人的草稿消息。這是最常見的解決方法
- 你也可以使用localStorage存儲草稿信息
- 為多個 不同位置的 相同組件 指定key可以保留state
- 把 useState 轉化為 useReducer:
- 通過事件處理函數 dispatch actions(派發action對象);
- 編寫一個 reducer 函數,它接受傳入的 state 和一個 action對象(通常包含type字段),并返回一個新的 state;
- 使用 useReducer 替換 useState;
- reducer 必須是一個純函數——它應該只計算下一個狀態。而不應該 “做” 其它事情,包括向用戶顯示消息。這應該在事件處理程序中處理。(為了便于捕獲這樣的錯誤,React 會在嚴格模式下多次調用你的 reducer。
- Context 可以讓父節點,甚至是很遠的父節點都可以為其內部的整個組件樹提供數據。
- 通過 export const MyContext = createContext(defaultValue) 創建并導出 context。
- 在無論層級多深的任何子組件中,把 context 傳遞給 useContext(MyContext) Hook 來讀取它。
- 在父組件中把 children 包在 <MyContext.Provider value={...}> 中來提供 context。
- 在 React 中,覆蓋來自上層的某些 context 的唯一方法是將子組件包裹到一個提供不同值的 context provider 中。
- 不同的 React context 不會覆蓋彼此。你通過 createContext() 創建的每個 context 都和其他 context 完全分離,只有使用和提供 那個特定的 context 的組件才會聯系在一起。一個組件可以輕松地使用或者提供許多不同的 context。
應急方案
- 當你希望組件“記住”某些信息,但又不想讓這些信息觸發新的渲染時,你可以使用 ref 。
- 當一條信息用于渲染時,將它保存在 state 中。當一條信息僅被事件處理器需要,并且更改它不需要重新渲染時,使用 ref 可能會更高效。
- 何時使用ref
- 存儲timeout ID
- 存儲和操作DOM 元素
- 存儲不需要被用來計算 JSX 的其他對象。
- 不要在渲染過程中讀取或寫入 ref.current,ref 本身是一個普通的 JavaScript 對象
- 給未知長度的列表中每項綁定ref
- 訪問另一個組件的 DOM 節點
- 一個組件可以指定將它的 ref “轉發”給一個子組件,子組件是使用 forwardRef 聲明的。 這讓從父組件接收的ref作為第二個參數傳入組件,第一個參數是 props
- 使用ref后,父組件可以調用子組件dom節點的所有方法,使用命令句柄(useImperativeHandle)可以只暴露一部分 API
- 通常,你將從事件處理器訪問 refs。
- 你可以強制 React 同步更新(“刷新”)DOM(添加一個元素后滾動至該元素)。 為此,從 react-dom 導入 flushSync 并將 state 更新包裹 到 flushSync 調用中
- useEffect 會把它包裹的代碼放到屏幕更新渲染之后執行
-為什么依賴數組中可以省略 ref 或者 set函數?- 這是因為
ref
具有 穩定 的標識:React 保證 每輪渲染中調用useRef
所產生的引用對象時,獲取到的對象引用總是相同的,即獲取到的對象引用永遠不會改變,所以它不會導致重新運行 Effect。因此,依賴數組中是否包含它并不重要。當然也可以包括它 -
useState
返回的set
函數 也有穩定的標識符,所以也可以把它從依賴數組中忽略掉
- 這是因為
- React 總是在執行下一輪渲染的 Effect 之前清理(執行effect中return出的清理函數)上一輪渲染的 Effect。
- 如果一個值可以基于現有的 props 或 state 計算得出,不要把它作為一個 state,而是在渲染期間直接計算這個值
- 你可以使用
useMemo
Hook 緩存(或者說 記憶(memoize))一個昂貴的計算(用該hook包裹一個耗時的函數,以避免每次渲染期間的重新計算)。 - 當 props 變化時重置所有 state,應當給該組件設置key屬性,key變化,react會自動重置該組件
- 當 props 變化時重置部分 state
- 在渲染期間更新組件時,React 會丟棄已經返回的 JSX 并立即嘗試重新渲染。為了避免非常緩慢的級聯重試,React 只允許在渲染期間更新 同一 組件的狀態。如果你在渲染期間更新另一個組件的狀態,你會看到一條報錯信息。
- 如果某些邏輯必須在 每次應用加載時執行一次,而不是在 每次組件掛載時執行一次,可以添加一個頂層變量(寫在組件函數之外)來記錄它是否已經執行過了
- 訂閱外部 store,useSyncExternalStore
- 數據請求中,為了修復競態條件下返回值set錯誤的問題,你需要添加一個 清理函數 來忽略較早的返回結果
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
- 每個 Effect 表示一個獨立的同步過程。抵制將與 Effect 無關的邏輯添加到已經編寫的 Effect 中(代碼中的每個 Effect 應該代表一個獨立的同步過程),僅僅因為 這些邏輯 需要與 Effect 同時運行(應重新寫一個effect函數來同步 這些邏輯,它的的好處是,刪除一個 Effect 不會影響另一個 Effect 的邏輯)。
- 組件內部的所有值(包括 props、state 和組件體內的變量)都是響應式的。任何響應式值都可以在重新渲染時發生變化,所以需要將響應式值包括在 Effect 的依賴項中。
- 避免將對象和函數作為依賴項。如果在渲染過程中創建對象和函數,然后在 Effect 中讀取它們,它們將在每次渲染時都不同。這將導致 Effect 每次都重新同步
- 避免禁用檢查工具。為了不違反規則,修復代碼總是值得的!
useEffect(() => {
// ...
// ?? 避免這樣禁用靜態代碼分析工具:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);
- 使用
useEffectEvent
(實驗性的功能)這個特殊的 Hook 從 Effect 中提取非響應式邏輯。Effect Event。它是 Effect 邏輯的一部分,但是其行為更像事件處理函數。它內部的邏輯不是響應式的,不需要添加到effect的依賴中,Effect Event 讓你在 Effect 響應性和不應是響應式的代碼間“打破鏈條”, Effect Event 讀取的是最新的 props 和 state 。 - 事件處理函數內部的邏輯是非響應式的(用戶的特定操作,才會執行)。
- Effect 內部的邏輯是響應式的(切換聊天室,需要自動斷開之前的,連接最新的)。
- 你可以將非響應式邏輯從 Effect 移到 Effect Event 中。
- 只在 Effect 內部調用 Effect Event。
- 不要將 Effect Event 傳給其他組件或者 Hook。
- 注意,你不能“選擇” Effect 的依賴。每個被 Effect 所使用的響應式值,必須在依賴中聲明。依賴是由 Effect 的代碼決定的。每個 Effect 應該代表一個獨立的同步過程,不應該同步兩個不同的、不相關的東西,應將他們拆分在不同的effect中
- 是否在讀取一些狀態來計算下一個狀態?使用state的更新函數寫法
- 你想讀取一個值而不對其變化做出“反應”嗎?或者來自 props 的事件處理程序的處理,將非響應式邏輯(props的事件處理程序)移至 Effect Event 中(非響應式邏輯應該在事件中)
- 盡量避免對象和函數依賴。將它們移到組件外(常量對象或者函數)、effect外(在effect外對對象或者函數解構,使effect依賴解構出來的基本類型的值)、 Effect 內(在effect中創建對象或者函數)
- 自定義 Hook 共享的只是狀態邏輯而不是狀態本身。對 Hook 的每個調用完全獨立于對同一個 Hook 的其他調用
- 不要創建像 useMount 這樣的自定義 Hook。保持目標具體化。
api
- 即使一個組件被記憶化(memo)了,當它自身的狀態發生變化時,它仍然會重新渲染
- 即使組件已被記憶化(memo),當其使用的 context 發生變化時,它仍將重新渲染