- 組件名必須大寫(xiě)開(kāi)頭
- 組件應(yīng)該在頂層定義。不要在組件函數(shù)里再定義其他組件。
- 標(biāo)簽必須閉合若贮,例如使用
<br/>
而非<br>
- 組件返回只能返回一個(gè)標(biāo)簽戳吝,如需要多個(gè)可以包裹到一個(gè)空的父級(jí)里浩销,如
<>...</>
(但只有其完整形式<Fragment/>
可以接收key
屬性):
import { Fragment } from 'react';
const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);
組件渲染流程
- 組件初次渲染,或組件或其祖先的狀態(tài)發(fā)生了改變骨坑,觸發(fā)組件渲染
- 組件渲染:執(zhí)行組件函數(shù)(不包括Hook)撼嗓,并在虛擬DOM樹(shù)中進(jìn)行diff運(yùn)算
注意,組件函數(shù)必須都是沒(méi)有副作用的純函數(shù)欢唾。 - 瀏覽器繪制(DOM渲染):此時(shí)DOM才真正更新
-
useEffect
回調(diào)觸發(fā)
組件渲染的邏輯
每次組件渲染會(huì)重新執(zhí)行組件函數(shù)且警,但不包括其中的 Hook 內(nèi)容
純函數(shù)
組件應(yīng)當(dāng)都是純函數(shù),只負(fù)責(zé)自己的任務(wù)礁遣,不改變外部變量斑芜,且相同的輸入總得到相同的輸入。以此防止組件每次渲染時(shí)造成副作用祟霍。
Hook 內(nèi)容不會(huì)在組件再次渲染時(shí)重復(fù)執(zhí)行
父組件傳入的 props 如通過(guò)useState
鏡像杏头,則父組件稍后傳遞不同的props時(shí)盈包,該state不會(huì)更新:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
}
props
- 父組件可以通過(guò) prop 傳遞數(shù)據(jù)、方法給子組件醇王。
- 子組件通過(guò)解構(gòu)賦值來(lái)接收父組件傳遞進(jìn)來(lái)的數(shù)據(jù)和方法呢燥。
- 組件定義時(shí)本身嵌套的JSX內(nèi)容會(huì)作為該組件的
children
prop(作用類似Vue的slot):
function Card({children}) {
return <div>hello {children}</div>
}
export default function Profile() {
return <Card><b>world</b></Card>
}
//最后會(huì)渲染成<div>hello <b>world</b></div>
- 將本該子組件使用的state提升到父組件,再由父組件將state寓娩、setState通過(guò) props 傳入子組件供調(diào)用叛氨,這種操作稱為
狀態(tài)提升
。
function MyApp() {
function speak(name) {
console.log("name", name);
}
return (
<>
<Child name="VV" speak={(name) => speak(name)} />
</>
);
}
function Child({ name, speak }) {
return (
<>
<div>我的名字是{name}</div>
<button onClick={() => speak(name)}>說(shuō)話</button>
</>
);
}
- 組件也可以將自己從父組件獲得的props再傳遞給自己的子組件
可以通過(guò)擴(kuò)展運(yùn)算符全部傳遞:
export default function Father(props) {
return (
<Child
{...props}
/>
);
}
function Child({ savedContact, onSave }) {
...
}
事件
- 原生事件名在JSX中需要改為駝峰命名棘伴,如
onClick
寞埠。需要在捕獲階段觸發(fā)的事件在最后額外加上Capture
,如onClickCapture
焊夸。 - 事件處理程序通常命名為 handle 接事件名仁连,例如:
onClick={handleClick}
- 當(dāng)需要指定函數(shù)入?yún)r(shí),可以使用箭頭函數(shù):
onClick={() => handleClick(100)}
- 父組件可以把事件通過(guò) props 傳遞給子組件阱穗,通常在 props 中的命名為
on
開(kāi)頭的大駝峰 - 通過(guò)原生的
e.stopPropagation()
阻止冒泡饭冬,e.preventDefault()
阻止默認(rèn)行為。
Hook 與狀態(tài)管理
以 use 開(kāi)頭的函數(shù)被稱為 Hook颇象,用于管理狀態(tài)伍伤。
- Hook 只能在組件或其他 Hook(包括自定義Hook)的頂層調(diào)用,且不能出現(xiàn)在條件語(yǔ)句或循環(huán)中遣钳。
Hook 不能被包裹在非組件且非 Hook 的普通自定義函數(shù)內(nèi)扰魂。 - 可以將一些組件的公用邏輯抽出來(lái),作為自定義 Hook蕴茴。自定義 Hook 共享的是狀態(tài)邏輯劝评,而不是狀態(tài)本身(即被不同組件引用時(shí),內(nèi)容不互通)倦淀。
useState
const [state, setState] = useState(初始值)
- state 狀態(tài)變量蒋畜,獨(dú)立于組件保存,組件多次渲染不會(huì)重定義該值撞叽。
- setState 函數(shù)姻成,用于更新變量并觸發(fā)再次渲染
函數(shù)入?yún)⒖梢允且粋€(gè)更新函數(shù)(必須為純函數(shù),只用于計(jì)算下一個(gè)狀態(tài)愿棋。多個(gè)更新函數(shù)會(huì)在渲染期間依次調(diào)用)科展,或一個(gè)固定值(本質(zhì)上是更新函數(shù)返回固定值的簡(jiǎn)寫(xiě))
注意,state在每次渲染中是固定的(快照)糠雨,setState不會(huì)改變當(dāng)前渲染中的state才睹,只會(huì)改變下一次渲染的值!
如想要在當(dāng)前渲染中就改變,可使用普通變量或useRef
琅攘。
一個(gè)事件處理函數(shù)會(huì)全部執(zhí)行完再進(jìn)行渲染垮庐,稱為批處理。
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(10);
setNumber(number + 1);
setNumber(number + 1);
setNumber(n=>n + 1);
setNumber(n=>n + 1);
console.log(number);//0
}}>點(diǎn)擊后變成3</button>
</>
)
}
使用flushSync
函數(shù)同步更新DOM
傳入flushSync
執(zhí)行的setState
操作會(huì)提前觸發(fā)DOM渲染(但快照中的state保持不變)坞琴,而不會(huì)等待批處理完成:
const [number, setNumber] = useState(0);
return (
<>
<h1 id="h1">{number}</h1>
<button onClick={() => {
flushSync(() => {
setNumber(1);
console.log(number);//0
console.log(document.getElementById("h1").innerHTML);//0
});
console.log(number);//0
console.log(document.getElementById("h1").innerHTML);//1
}}>點(diǎn)我</button>
</>
);
state 不保存在 JSX 標(biāo)簽里
state 與樹(shù)中放置該 JSX 的位置相關(guān)聯(lián)哨查。因此在渲染樹(shù)中相同位置的相同組件,其內(nèi)部狀態(tài)會(huì)得到保留:
//以下<Counter/>組件內(nèi)部的state置济,在 isFancy 切換時(shí)會(huì)得到保留
{isFancy ? (
<Counter isFancy={true} />
) : (
<Counter isFancy={false} />
)}
//以下<Counter/>組件內(nèi)部的state解恰,在 isPlayerA 切換時(shí)會(huì)重置(因位置不同)
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
重置其內(nèi)部狀態(tài)的幾個(gè)方式:
- 渲染樹(shù)中不同位置
相同渲染樹(shù)需要:上級(jí)組件相同,且前置兄弟組件數(shù)量相同(<></>
是一個(gè)組件浙于,DOM元素也視為一種組件,一段JSX{}
也算一種組件) - 不同組件
- 銷毀該組件后不立刻重新渲染該組件
- 使用不同 key
突變 mutation
通過(guò) setState 以外的方式造成的 state 數(shù)據(jù)變化稱為突變(例如通過(guò)原生JS直接修改對(duì)象挟纱、數(shù)組的某個(gè)成員)羞酗。突變的結(jié)果會(huì)在下次渲染中顯示,但突變本身不會(huì)觸發(fā)渲染紊服。
應(yīng)當(dāng)避免突變的發(fā)生檀轨。
useReducer
當(dāng)需要在多處分別對(duì)同一state
做出不同修改時(shí),可以通過(guò)useReducer
代替useState
進(jìn)行統(tǒng)一管理欺嗤。
-
useReducer
可傳入兩個(gè)入?yún)⒉翁眩谝粋€(gè)是reducer
函數(shù),第二個(gè)是state
初始值煎饼。 -
reducer
函數(shù)必須為純函數(shù)讹挎,只用于計(jì)算下一個(gè)狀態(tài)。(嚴(yán)格模式下會(huì)調(diào)用兩次)吆玖。該函數(shù)的返回值用于更新?tīng)顟B(tài)筒溃。 - 返回的
dispatch
函數(shù)用來(lái) “派發(fā)” 用戶操作給reducer
函數(shù),調(diào)用時(shí)傳入一個(gè)自定義參數(shù)(通常約定會(huì)有 type 屬性)沾乘,傳給reducer
函數(shù)的第二個(gè)參數(shù):
import { useReducer } from 'react';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function tasksReducer(tasks, action) {
switch (action.type) {
case 1:
return{
...tasks,
age: action.age
}
default: {
throw Error('未知 action: ' + action.type);
}
}
}
useRef(脫圍)
類似useState
怜奖,返回一個(gè)ref
實(shí)例,用于在組件渲染間保留信息翅阵。
不要在渲染期間(即組件主體)讀取或?qū)懭?code>ref歪玲,應(yīng)該移到事件處理程序或者 Effect 中。
-
ref
實(shí)例是一個(gè)具有current
屬性的對(duì)象掷匠,其更新不會(huì)觸發(fā)組件渲染滥崩,也不是個(gè)快照,可以直接對(duì)current
屬性賦值以修改信息槐雾。 -
ref
實(shí)例通常用于儲(chǔ)存 timeout ID夭委、DOM元素、或其他不需要在JSX中渲染計(jì)算的內(nèi)容:
import { useRef } from 'react';
const ref = useRef(0);
ref.current = ref.current + 1;//1
- JSX中DOM節(jié)點(diǎn)的
ref
屬性,可直接賦值為ref
實(shí)例以綁定DOM元素株灸,也可以傳入一個(gè)方法崇摄,在方法內(nèi)手動(dòng)綁定DOM元素。
當(dāng)傳入方法時(shí)慌烧,每當(dāng)組件重新渲染逐抑,先前的函數(shù)將被調(diào)用并傳遞 null 作為參數(shù),并且下一個(gè)函數(shù)將被調(diào)用并傳遞對(duì)應(yīng) DOM 節(jié)點(diǎn)作為參數(shù):
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
const divRefs = useRef(new Map());
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>
聚焦輸入框
</button>
{
[0, 1, 2, 3, 4, 5].map(i => (
<div key={i} ref={node => {
if(node){
divRefs.current.set(i, node)
}else{
divRefs.current.delete(i)
}
}}
onClick={() => {
divRefs.current.get(i).style.color = "red";
}}>
我是第{i}個(gè)屹蚊,點(diǎn)我會(huì)變紅
</div>
))
}
</>
);
}
上述方法默認(rèn)只能用于瀏覽器原生元素厕氨,當(dāng)用于JSX中的自定義組件時(shí),該組件需要由forwardRef
方法創(chuàng)建(在此方法中還可以通過(guò)useImperativeHandle
限制暴露的內(nèi)容):
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
return (
<>
<MyInput ref={inputRef} />
</>
);
}
useEffect(脫圍汹粤,由渲染引起的副作用命斧,與React之外的系統(tǒng)同步)
- 處理 Effect 的函數(shù)(必傳)
- 該函數(shù)在嚴(yán)格模式+開(kāi)發(fā)環(huán)境下會(huì)調(diào)用兩次。
- 該函數(shù)可以返回一個(gè)清理函數(shù)嘱兼,會(huì)在組件卸載国葬,或
Effect
再次執(zhí)行前調(diào)用。
注意清理函數(shù)在Effect函數(shù)內(nèi)是個(gè)閉包芹壕,因此調(diào)用時(shí)起內(nèi)容只影響其聲明時(shí)的Effect內(nèi)變量:
//此處返回值內(nèi)的ignore可以讓同一次調(diào)用中的fetch回調(diào)失效汇四,但不影響下一次的Effect
//可用于防止競(jìng)態(tài)條件,保證是最后一次觸發(fā)的生效
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
}
}, [person]);
- 依賴數(shù)組(非必傳)
- 若不傳踢涌,則會(huì)在組件每次渲染并調(diào)用觸發(fā)DOM渲染后調(diào)用函數(shù)通孽。
- 若傳入依賴數(shù)組(可傳入
state
、props
睁壁、組件體內(nèi)聲明的利用前兩者計(jì)算得出的其他響應(yīng)式變量)背苦,且依賴內(nèi)容均和上次渲染時(shí)相同,則跳過(guò)本次Effect函數(shù)執(zhí)行堡僻。
傳入依賴數(shù)組后糠惫,函數(shù)只能使用依賴數(shù)組內(nèi)包含的響應(yīng)式變量。- 類似
location.pathname
這樣的外部可變值不是響應(yīng)式變量钉疫,應(yīng)改用useSyncExternalStore
來(lái)讀取和訂閱硼讽。 - 當(dāng)前組件的
ref
不是響應(yīng)式變量,因?yàn)槠涫怯幸饪勺兊纳蟆⒎€(wěn)定的固阁。其變化不會(huì)觸發(fā)重新渲染 -
useState
創(chuàng)建的setState
方法不是響應(yīng)式變量
- 類似
import { useEffect } from 'react';
useEffect(() => {
// 這里的代碼會(huì)在每次渲染后執(zhí)行
});
useEffect(() => {
// 這里的代碼只會(huì)在組件掛載后執(zhí)行
}, []);
useEffect(() => {
//這里的代碼只會(huì)在每次渲染后,并且 a 或 b 的值與上次渲染不一致時(shí)執(zhí)行
}, [a, b]);
注意:組件內(nèi)聲明的對(duì)象城菊、數(shù)組备燃、函數(shù)等(包括父組件傳遞下來(lái)的props中的以上內(nèi)容),因?yàn)槊看武秩径疾蝗攘杌#绻麄鬟f給useEffect
作為依賴則會(huì)導(dǎo)致每次渲染都觸發(fā)useEffect
并齐。此時(shí)需要使用useMemo
或 useCallback
,或?qū)⒃撀暶饕苿?dòng)到組件外或Effect
內(nèi)。
而當(dāng)某個(gè)響應(yīng)式變量既需要參與useEffect
內(nèi)容况褪,又不想因其變化而導(dǎo)致觸發(fā)Effect
撕贞,則可以使用useEffectEvent
。
useMemo 和 useCallback
useMemo 返回值测垛,useCallback 返回函數(shù)捏膨。兩者均用于避免組件每次渲染時(shí)的重復(fù)計(jì)算。
- 需傳入依賴數(shù)組食侮,若依賴內(nèi)容均和上次渲染時(shí)相同号涯,則返回和上一次全等的結(jié)果。
- 會(huì)在JSX渲染環(huán)節(jié)(即
DOM
更新前)生效锯七,因此都必須是純函數(shù)链快。
import { useMemo, useState, useCallback } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ? 除非 todos 或 filter 發(fā)生變化,否則不會(huì)重新執(zhí)行 getFilteredTodos()
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
useEffectEvent
用于提取Effect中部分邏輯起胰,以實(shí)現(xiàn)如下功能:存在某個(gè)響應(yīng)式變量久又,既要在Effect中使用,又不想充當(dāng)Effect的依賴項(xiàng)效五。
相比于useCallback
的區(qū)別:
- 不用顯式聲明依賴
- 即使依賴變了,fn的引用也不變(與原來(lái)全等)
- 只能在
useEffect
內(nèi)部使用炉峰,且不需要加入useEffect
依賴
//在這里畏妖,onVisit 內(nèi)的 url 對(duì)應(yīng) 最新的 url(可能已經(jīng)變化了),但是 visitedUrl 對(duì)應(yīng)的是最開(kāi)始引起這個(gè) Effect(并且是本次 onVisit 調(diào)用)運(yùn)行的 url 疼阔。
import { experimental_useEffectEvent as useEffectEvent } from 'react';
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // 延遲記錄訪問(wèn)
}, [url]);
使用 Immer 庫(kù)簡(jiǎn)化 useState 和 useReducer
Immer 庫(kù)可代替useState
和 useReducer
戒劫,以避免突變,并簡(jiǎn)化對(duì)象婆廊、數(shù)組的更新操作迅细。
npm install use-immer
useImmer
提供的修改方法,其參數(shù)同樣可以是一個(gè)更新函數(shù)或一個(gè)固定值淘邻。
- 為固定值時(shí)效果同
useState
- 為更新函數(shù)時(shí)(通常以
update
開(kāi)頭命名)茵典,其參數(shù)(通常命名為draft
)是一個(gè)Proxy而不是原始的state(類似 Vue3 實(shí)現(xiàn)了數(shù)據(jù)劫持),因此可以直接修改其中屬性:
import { useImmer } from 'use-immer';
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
});
updatePerson(draft => {
draft.name = e.target.value;
});
useImmerReducer
可以返回宾舅,也可以直接修改 draft
對(duì)象屬性统阿。
注意switch
中如果不使用return
,要使用break
分隔case
:
import { useImmerReducer } from 'use-immer';
const [tasks, dispatch] = useImmerReducer(tasksReducer, [{id:1},{id:2},{id:3}]);
function tasksReducer(draft, action) {
switch (action.type) {
case 'changed': {
const index = draft.findIndex((t) => t.id === action.task.id);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter((t) => t.id !== action.id);
}
default: {
throw Error('未知 action:' + action.type);
}
}
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
使用 Context 深層傳遞參數(shù)
用于祖先元素向其深層后代傳遞信息筹我,以代替 props 逐級(jí)下傳
// 單獨(dú)的文件 MyContext.js
import { createContext } from 'react';
export const MyContext = createContext("默認(rèn)值")
// 父組件提供Context扶平,Context.Provider的后代元素都可以獲得距離最近的值
import { MyContext } from './Context.js';
export default function App() {
return (
<>
<MyContext.Provider value={100}>
<List/>
</MyContext.Provider>
</>
)
}
// 子組件獲取Context
import { useContext } from 'react';
import { MyContext } from './MyContext.js';
function List() {
const data = useContext(MyContext)
return <div>{data}</div>
}