黃驊市第三中學(xué)關(guān)鍵詞優(yōu)化包年推廣
文章目錄
- 用 State 響應(yīng)輸入
- 聲明式地考慮 UI
- 步驟 1:定位組件中不同的視圖狀態(tài)
- 步驟 2:確定是什么觸發(fā)了這些狀態(tài)的改變
- 步驟 3:通過 useState 表示內(nèi)存中的 state
- 步驟 4:刪除任何不必要的 state 變量
- 步驟 5:連接事件處理函數(shù)以設(shè)置 state
- 選擇 State 結(jié)構(gòu)
- 構(gòu)建 state 的原則
- 在組件間共享狀態(tài)
- 舉例說明一下狀態(tài)提升
- 第 1 步: 從子組件中移除狀態(tài)
- 第 2 步: 從公共父組件傳遞硬編碼數(shù)據(jù)
- 第 3 步: 為公共父組件添加狀態(tài)
- 受控組件和非受控組件
- 對(duì) state 進(jìn)行保留和重置
- 狀態(tài)與渲染樹中的位置相關(guān)
- 相同位置的相同組件會(huì)使得 state 被保留下來
- 相同位置的不同組件會(huì)使 state 重置
- 在相同位置重置 state
- 方法一:將組件渲染在不同的位置
- 方法二:使用 key 來重置 state
- 為被移除的組件保留 state
- 遷移狀態(tài)邏輯至 Reducer 中
- 使用 reducer 整合狀態(tài)邏輯
- 第 1 步: 將設(shè)置狀態(tài)的邏輯修改成 dispatch 的一個(gè) action
- 第 2 步: 編寫一個(gè) reducer 函數(shù)
- 語法
- 示例
- 使用場景
- 注意事項(xiàng)
- 示例
- 第 3 步: 在組件中使用 reducer
- 對(duì)比 useState 和 useReducer
- 使用 Context 深層傳遞參數(shù)
- 傳遞 props 帶來的問題
- Context:傳遞 props 的另一種方法
- 寫在你使用 context 之前
- Context 的使用場景
- 使用 Reducer 和 Context 拓展你的應(yīng)用
- 應(yīng)急方案
- 使用 ref 引用值
- 給你的組件添加 ref
- ref 和 state 的不同之處
- useRef 內(nèi)部是如何運(yùn)行的?
- 何時(shí)使用 ref
- ref 的最佳實(shí)踐
- 使用 ref 操作 DOM
- 獲取指向節(jié)點(diǎn)的 ref
- 訪問另一個(gè)組件的 DOM 節(jié)點(diǎn)
- 使用 Effect 同步
- 如何編寫 Effect
- 第一步:聲明 Effect
- 第二步:指定 Effect 依賴
- 第三部:按需添加清理(cleanup)函數(shù)
- 訂閱事件
- 觸發(fā)動(dòng)畫
- 初始化應(yīng)用時(shí)不需要使用 Effect 的情形
- 你可能不需要 Effect
- 響應(yīng)式 Effect 的生命周期
- Effect 的生命周期
[]以下內(nèi)容來自官方文檔 https://zh-hans.react.dev/learn](https://zh-hans.react.dev/learn)
用 State 響應(yīng)輸入
React 控制 UI 的方式是聲明式的。你不必直接控制 UI 的各個(gè)部分,只需要聲明組件可以處于的不同狀態(tài),并根據(jù)用戶的輸入在它們之間切換
聲明式地考慮 UI
你已經(jīng)從上面的例子看到如何去實(shí)現(xiàn)一個(gè)表單了,為了更好地理解如何在 React 中思考,接下來你將會(huì)學(xué)到如何用 React 重新實(shí)現(xiàn)這個(gè) UI:
- 定位你的組件中不同的視圖狀態(tài)
- 確定是什么觸發(fā)了這些 state 的改變
- 表示內(nèi)存中的 state(需要使用 useState)
- 刪除任何不必要的 state 變量
- 連接事件處理函數(shù)去設(shè)置 state
步驟 1:定位組件中不同的視圖狀態(tài)
在計(jì)算機(jī)科學(xué)中,你或許聽過可處于多種“狀態(tài)”之一的 “狀態(tài)機(jī)”
首先,你需要去可視化 UI 界面中用戶可能看到的所有不同的“狀態(tài)”:
- 無數(shù)據(jù):表單有一個(gè)不可用狀態(tài)的“提交”按鈕。
- 輸入中:表單有一個(gè)可用狀態(tài)的“提交”按鈕。
- 提交中:表單完全處于不可用狀態(tài),加載動(dòng)畫出現(xiàn)。
- 成功時(shí):顯示“成功”的消息而非表單。
- 錯(cuò)誤時(shí):與輸入狀態(tài)類似,但會(huì)多錯(cuò)誤的消息。
步驟 2:確定是什么觸發(fā)了這些狀態(tài)的改變
你可以觸發(fā) state 的更新來響應(yīng)兩種輸入:
- 人為輸入。比如點(diǎn)擊按鈕、在表單中輸入內(nèi)容,或?qū)Ш降芥溄印?/li>
- 計(jì)算機(jī)輸入。比如網(wǎng)絡(luò)請(qǐng)求得到反饋、定時(shí)器被觸發(fā),或加載一張圖片
你需要改變 state 以響應(yīng)幾個(gè)不同的輸入:
- 改變輸入框中的文本時(shí)(人為)應(yīng)該根據(jù)輸入框的內(nèi)容是否是空值,從而決定將表單的狀態(tài)從空值狀態(tài)切換到輸入中或切換回原狀態(tài)。
- 點(diǎn)擊提交按鈕時(shí)(人為)應(yīng)該將表單的狀態(tài)切換到提交中的狀態(tài)。
- 網(wǎng)絡(luò)請(qǐng)求成功后(計(jì)算機(jī))應(yīng)該將表單的狀態(tài)切換到成功的狀態(tài)。
- 網(wǎng)絡(luò)請(qǐng)求失敗后(計(jì)算機(jī))應(yīng)該將表單的狀態(tài)切換到失敗的狀態(tài),與此同時(shí),顯示錯(cuò)誤信息
步驟 3:通過 useState 表示內(nèi)存中的 state
如果你很難立即想出最好的辦法,那就先從添加足夠多的 state 開始,確保所有可能的視圖狀態(tài)都囊括其中:
const [isEmpty, setIsEmpty] = useState(true);const [isTyping, setIsTyping] = useState(false);const [isSubmitting, setIsSubmitting] = useState(false);const [isSuccess, setIsSuccess] = useState(false);const [isError, setIsError] = useState(false);
你最初的想法或許不是最好的,但是沒關(guān)系,重構(gòu) state 也是步驟中的一部分!
步驟 4:刪除任何不必要的 state 變量
在清理之后,你只剩下 3 個(gè)(從原本的 7 個(gè)!)_必要_的 state 變量:
const [answer, setAnswer] = useState('');const [error, setError] = useState(null);const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
步驟 5:連接事件處理函數(shù)以設(shè)置 state
練習(xí)1
import {useState} from 'react'
export default function Picture() {const [imgcss,setImgcss] = useState(false)function handleBgd(){setImgcss(false)}function handleImg(e){e.stopPropagation()setImgcss(true)}let bgdClassName = 'background'let imgClassName = 'picture'if(imgcss){imgClassName += ' picture--active' //注意此處的空格}else{bgdClassName += ' background--active' //注意此處的空格}return (<div className={bgdClassName} onClick={()=>setImgcss(false)}><imgonClick={e => {e.stopPropagation();setImgcss(true)}}className={imgClassName}alt="Rainbow houses in Kampung Pelangi, Indonesia"src="https://i.imgur.com/5qwVYb1.jpeg"/></div>);
}
練習(xí)2
import {useState} from 'react'
export default function EditProfile() {const [edit,setEdit]= useState(false)const [firstName,setFirstName] = useState('')const [lastName,setLastName] = useState('')return (<form onSubmit={(e)=>{e.preventDefault()setEdit(!edit)}}><label>First name:{' '}{edit? <input onChange={(e)=>{setFirstName(e.target.value)}} value={firstName}/>:<b>{firstName}</b>}</label><label>Last name:{' '}{edit? <input onChange={(e)=>{setLastName(e.target.value)}} value={lastName}/>:<b>{lastName}</b>}</label><button type="submit">{edit? 'save ': 'edit '} Profile</button><p><i>Hello, {firstName} {lastName}!</i></p></form>);
}
選擇 State 結(jié)構(gòu)
構(gòu)建 state 的原則
當(dāng)你編寫一個(gè)存有 state 的組件時(shí),你需要選擇使用多少個(gè) state 變量以及它們都是怎樣的數(shù)據(jù)格式。盡管選擇次優(yōu)的 state 結(jié)構(gòu)下也可以編寫正確的程序,但有幾個(gè)原則可以指導(dǎo)您做出更好的決策:
- 合并關(guān)聯(lián)的 state。如果你總是同時(shí)更新兩個(gè)或更多的 state 變量,請(qǐng)考慮將它們合并為一個(gè)單獨(dú)的 state 變量。
- 避免互相矛盾的 state。當(dāng) state 結(jié)構(gòu)中存在多個(gè)相互矛盾或“不一致”的 state 時(shí),你就可能為此會(huì)留下隱患。應(yīng)盡量避免這種情況。
- 避免冗余的 state。如果你能在渲染期間從組件的 props 或其現(xiàn)有的 state 變量中計(jì)算出一些信息,則不應(yīng)將這些信息放入該組件的 state 中。
- 避免重復(fù)的 state。當(dāng)同一數(shù)據(jù)在多個(gè) state 變量之間或在多個(gè)嵌套對(duì)象中重復(fù)時(shí),這會(huì)很難保持它們同步。應(yīng)盡可能減少重復(fù)。
- 避免深度嵌套的 state。深度分層的 state 更新起來不是很方便。如果可能的話,最好以扁平化方式構(gòu)建 state。
在組件間共享狀態(tài)
有時(shí)候,你希望兩個(gè)組件的狀態(tài)始終同步更改。要實(shí)現(xiàn)這一點(diǎn),可以將相關(guān) state 從這兩個(gè)組件上移除,并把 state 放到它們的公共父級(jí),再通過 props 將 state 傳遞給這兩個(gè)組件。這被稱為“狀態(tài)提升”,這是編寫 React 代碼時(shí)常做的事
舉例說明一下狀態(tài)提升
在這個(gè)例子中,父組件 Accordion 渲染了 2 個(gè)獨(dú)立的 Panel 組件。
- Accordion
- Panel
- Panel
每個(gè) Panel 組件都有一個(gè)布爾值 isActive,用于控制其內(nèi)容是否可見
import { useState } from 'react';function Panel({ title, children }) {const [isActive, setIsActive] = useState(false);return (<section className="panel"><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<button onClick={() => setIsActive(true)}>顯示</button>)}</section>);
}export default function Accordion() {return (<><h2>哈薩克斯坦,阿拉木圖</h2><Panel title="關(guān)于">阿拉木圖人口約200萬,是哈薩克斯坦最大的城市。它在 1929 年到 1997 年間都是首都。</Panel><Panel title="詞源">這個(gè)名字來自于 <span lang="kk-KZ">алма</span>,哈薩克語中“蘋果”的意思,經(jīng)常被翻譯成“蘋果之鄉(xiāng)”。事實(shí)上,阿拉木圖的周邊地區(qū)被認(rèn)為是蘋果的發(fā)源地,<i lang="la">Malus sieversii</i> 被認(rèn)為是現(xiàn)今蘋果的祖先。</Panel></>);
}
假設(shè)現(xiàn)在您想改變這種行為,以便在任何時(shí)候只展開一個(gè)面板。在這種設(shè)計(jì)下,展開第 2 個(gè)面板應(yīng)會(huì)折疊第 1 個(gè)面板。您該如何做到這一點(diǎn)呢?”
要協(xié)調(diào)好這兩個(gè)面板,我們需要分 3 步將狀態(tài)“提升”到他們的父組件中。
- 從子組件中 移除 state 。
- 從父組件 傳遞 硬編碼數(shù)據(jù)。
- 為共同的父組件添加 state ,并將其與事件處理函數(shù)一起向下傳遞
第 1 步: 從子組件中移除狀態(tài)
你將把 Panel 組件對(duì) isActive 的控制權(quán)交給他們的父組件。這意味著,父組件會(huì)將 isActive 作為 prop 傳給子組件 Panel。我們先從 Panel 組件中 刪除下面這一行:
const [isActive, setIsActive] = useState(false);
然后,把 isActive 加入 Panel 組件的 props 中:
function Panel({ title, children, isActive }) {
第 2 步: 從公共父組件傳遞硬編碼數(shù)據(jù)
import { useState } from 'react';export default function Accordion() {return (<><h2>哈薩克斯坦,阿拉木圖</h2><Panel title="關(guān)于" isActive={true}>阿拉木圖人口約200萬,是哈薩克斯坦最大的城市。它在 1929 年到 1997 年間都是首都。</Panel><Panel title="詞源" isActive={true}>這個(gè)名字來自于 <span lang="kk-KZ">алма</span>,哈薩克語中“蘋果”的意思,經(jīng)常被翻譯成“蘋果之鄉(xiāng)”。事實(shí)上,阿拉木圖的周邊地區(qū)被認(rèn)為是蘋果的發(fā)源地,<i lang="la">Malus sieversii</i> 被認(rèn)為是現(xiàn)今蘋果的祖先。</Panel></>);
}function Panel({ title, children, isActive }) {return (<section className="panel"><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<button onClick={() => setIsActive(true)}>顯示</button>)}</section>);
}
第 3 步: 為公共父組件添加狀態(tài)
狀態(tài)提升通常會(huì)改變原狀態(tài)的數(shù)據(jù)存儲(chǔ)類型。
在這個(gè)例子中,一次只能激活一個(gè)面板。這意味著 Accordion 這個(gè)父組件需要記錄 哪個(gè) 面板是被激活的面板。我們可以用數(shù)字作為當(dāng)前被激活 Panel 的索引,而不是 boolean 值:
const [activeIndex, setActiveIndex] = useState(0);
import { useState } from 'react';export default function Accordion() {const [activeIndex, setActiveIndex] = useState(0);return (<><h2>哈薩克斯坦,阿拉木圖</h2><Paneltitle="關(guān)于"isActive={activeIndex === 0}onShow={() => setActiveIndex(0)}>阿拉木圖人口約200萬,是哈薩克斯坦最大的城市。它在 1929 年到 1997 年間都是首都。</Panel><Paneltitle="詞源"isActive={activeIndex === 1}onShow={() => setActiveIndex(1)}>這個(gè)名字來自于 <span lang="kk-KZ">алма</span>,哈薩克語中“蘋果”的意思,經(jīng)常被翻譯成“蘋果之鄉(xiāng)”。事實(shí)上,阿拉木圖的周邊地區(qū)被認(rèn)為是蘋果的發(fā)源地,<i lang="la">Malus sieversii</i> 被認(rèn)為是現(xiàn)今蘋果的祖先。</Panel></>);
}function Panel({title,children,isActive,onShow
}) {return (<section className="panel"><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<button onClick={onShow}>顯示</button>)}</section>);
}
受控組件和非受控組件
通常我們把包含“不受控制”狀態(tài)的組件稱為“非受控組件”。例如,最開始帶有 isActive 狀態(tài)變量的 Panel 組件就是不受控制的,因?yàn)槠涓附M件無法控制面板的激活狀態(tài)。
相反,當(dāng)組件中的重要信息是由 props 而不是其自身狀態(tài)驅(qū)動(dòng)時(shí),就可以認(rèn)為該組件是“受控組件”。這就允許父組件完全指定其行為。最后帶有 isActive 屬性的 Panel 組件是由 Accordion 組件控制的
練習(xí)1
import { useState } from 'react';export default function SyncedInputs() {const [text,setText]= useState('')function handleChange(e){setText(e.target.value)}return (<><Input label="第一個(gè)輸入框" onChange={handleChange} text={text}/><Input label="第二個(gè)輸入框" onChange={handleChange} text={text}/></>);
}function Input({ label, onChange, text }) {//const [text, setText] = useState('');/*function handleChange(e) {setText(e.target.value);}*/return (<label>{label}{' '}<inputvalue={text}onChange={onChange}/></label>);
}
練習(xí)2
import { useState } from 'react';
import { foods, filterItems } from './data.js';export default function FilterableList() {const [query,setQuery] = useState('')function handleQuery(e){setQuery(e.target.value)}return (<><SearchBar text={query} onChange={handleQuery}/><hr /><List items={foods} text={query}/></>);
}function SearchBar({text,onChange}) {/*const [query, setQuery] = useState('');function handleChange(e) {setQuery(e.target.value);}*/return (<label>搜索:{' '}<inputvalue={text}onChange={onChange}/></label>);
}function List({ items,text }) {return (<table><tbody>{filterItems(items,text).map(food => (<tr key={food.id}><td>{food.name}</td><td>{food.description}</td></tr>))}</tbody></table>);
}
對(duì) state 進(jìn)行保留和重置
狀態(tài)與渲染樹中的位置相關(guān)
只有當(dāng)在樹中相同的位置渲染相同的組件時(shí),React 才會(huì)一直保留著組件的 state
import { useState } from 'react';export default function App() {const [showB, setShowB] = useState(true);return (<div><Counter />{showB && <Counter />} <label><inputtype="checkbox"checked={showB}onChange={e => {setShowB(e.target.checked)}}/>渲染第二個(gè)計(jì)數(shù)器</label></div>);
}function Counter() {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}
注意,當(dāng)你停止渲染第二個(gè)計(jì)數(shù)器的那一刻,它的 state 完全消失了。這是因?yàn)?React 在移除一個(gè)組件時(shí),也會(huì)銷毀它的 state
當(dāng)你重新勾選“渲染第二個(gè)計(jì)數(shù)器”復(fù)選框時(shí),另一個(gè)計(jì)數(shù)器及其 state 將從頭開始初始化(score = 0)并被添加到 DOM 中。
相同位置的相同組件會(huì)使得 state 被保留下來
import { useState } from 'react';export default function App() {const [isFancy, setIsFancy] = useState(false);return (<div>{isFancy ? (<Counter isFancy={true} /> ) : (<Counter isFancy={false} /> )}<label><inputtype="checkbox"checked={isFancy}onChange={e => {setIsFancy(e.target.checked)}}/>使用好看的樣式</label></div>);
}function Counter({ isFancy }) {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}if (isFancy) {className += ' fancy';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}
相同位置的不同組件會(huì)使 state 重置
在這個(gè)例子中,勾選復(fù)選框會(huì)將 替換為一個(gè)
import { useState } from 'react';export default function App() {const [isPaused, setIsPaused] = useState(false);return (<div>{isPaused ? (<p>待會(huì)見!</p> ) : (<Counter /> )}<label><inputtype="checkbox"checked={isPaused}onChange={e => {setIsPaused(e.target.checked)}}/>休息一下</label></div>);
}function Counter() {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}
并且,當(dāng)你在相同位置渲染不同的組件時(shí),組件的整個(gè)子樹都會(huì)被重置。要驗(yàn)證這一點(diǎn),可以增加計(jì)數(shù)器的值然后勾選復(fù)選框
import { useState } from 'react';export default function App() {const [isFancy, setIsFancy] = useState(false);return (<div>{isFancy ? (<div><Counter isFancy={true} /> </div>) : (<section><Counter isFancy={false} /></section>)}<label><inputtype="checkbox"checked={isFancy}onChange={e => {setIsFancy(e.target.checked)}}/>使用好看的樣式</label></div>);
}function Counter({ isFancy }) {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}if (isFancy) {className += ' fancy';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}
在相同位置重置 state
方法一:將組件渲染在不同的位置
import { useState } from 'react';export default function Scoreboard() {const [isPlayerA, setIsPlayerA] = useState(true);return (<div>{isPlayerA &&<Counter person="Taylor" />}{!isPlayerA &&<Counter person="Sarah" />}<button onClick={() => {setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);
}function Counter({ person }) {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{person} 的分?jǐn)?shù):{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}
方法二:使用 key 來重置 state
key 不只可以用于列表!你可以使用 key 來讓 React 區(qū)分任何組件。默認(rèn)情況下,React 使用父組件內(nèi)部的順序(“第一個(gè)計(jì)數(shù)器”、“第二個(gè)計(jì)數(shù)器”)來區(qū)分組件。但是 key 可以讓你告訴 React 這不僅僅是 第一個(gè) 或者 第二個(gè) 計(jì)數(shù)器,而且還是一個(gè)特定的計(jì)數(shù)器——例如,Taylor 的 計(jì)數(shù)器。這樣無論它出現(xiàn)在樹的任何位置, React 都會(huì)知道它是 Taylor 的 計(jì)數(shù)器!
import { useState } from 'react';export default function Scoreboard() {const [isPlayerA, setIsPlayerA] = useState(true);return (<div>{isPlayerA ? (<Counter key="Taylor" person="Taylor" />) : (<Counter key="Sarah" person="Sarah" />)}<button onClick={() => {setIsPlayerA(!isPlayerA);}}>下一位玩家!</button></div>);
}function Counter({ person }) {const [score, setScore] = useState(0);const [hover, setHover] = useState(false);let className = 'counter';if (hover) {className += ' hover';}return (<divclassName={className}onPointerEnter={() => setHover(true)}onPointerLeave={() => setHover(false)}><h1>{person} 的分?jǐn)?shù):{score}</h1><button onClick={() => setScore(score + 1)}>加一</button></div>);
}
為被移除的組件保留 state
在真正的聊天應(yīng)用中,你可能會(huì)想在用戶再次選擇前一個(gè)收件人時(shí)恢復(fù)輸入 state。對(duì)于一個(gè)不可見的組件,有幾種方法可以讓它的 state “活下去”:
- 與其只渲染現(xiàn)在這一個(gè)聊天,你可以把 所有 聊天都渲染出來,但用 CSS 把其他聊天隱藏起來。這些聊天就不會(huì)從樹中被移除了,所以它們的內(nèi)部 state 會(huì)被保留下來。這種解決方法對(duì)于簡單 UI 非常有效。但如果要隱藏的樹形結(jié)構(gòu)很大且包含了大量的 DOM 節(jié)點(diǎn),那么性能就會(huì)變得很差。
- 你可以進(jìn)行 狀態(tài)提升 并在父組件中保存每個(gè)收件人的草稿消息。這樣即使子組件被移除了也無所謂,因?yàn)楸A糁匾畔⒌氖歉附M件。這是最常見的解決方法。
- 除了 React 的 state,你也可以使用其他數(shù)據(jù)源。例如,也許你希望即使用戶不小心關(guān)閉頁面也可以保存一份信息草稿。要實(shí)現(xiàn)這一點(diǎn),你可以讓 Chat 組件通過讀取 localStorage 對(duì)其 state 進(jìn)行初始化,并把草稿保存在那里。
練習(xí)1
import { useState } from 'react';export default function App() {const [showHint, setShowHint] = useState(false);if (showHint) {return (<div><p><i>提示:你最喜歡的城市?</i></p><Form /><button onClick={() => {setShowHint(false);}}>隱藏提示</button></div>);}return (<div><p></p><Form /><button onClick={() => {setShowHint(true);}}>顯示提示</button></div>);
}function Form() {const [text, setText] = useState('');return (<textareavalue={text}onChange={e => setText(e.target.value)}/>);
}
練習(xí)2
import { useState } from 'react';export default function App() {const [reverse, setReverse] = useState(false);let checkbox = (<label><inputtype="checkbox"checked={reverse}onChange={e => setReverse(e.target.checked)}/>調(diào)換順序</label>);if (reverse) {return (<><Field label="姓氏" key='姓氏' /> <Field label="名字" key='名字'/>{checkbox}</>);} else {return (<><Field label="名字" key='名字'/> <Field label="姓氏" key='姓氏'/>{checkbox}</>); }
}function Field({ label }) {const [text, setText] = useState('');return (<label>{label}:<inputtype="text"value={text}placeholder={label}onChange={e => setText(e.target.value)}/></label>);
}
練習(xí)3,4,5
遷移狀態(tài)邏輯至 Reducer 中
使用 reducer 整合狀態(tài)邏輯
隨著組件復(fù)雜度的增加,你將很難一眼看清所有的組件狀態(tài)更新邏輯。例如,下面的 TaskApp 組件有一個(gè)數(shù)組類型的狀態(tài) tasks,并通過三個(gè)不同的事件處理程序來實(shí)現(xiàn)任務(wù)的添加、刪除和修改
import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';export default function TaskApp() {const [tasks, setTasks] = useState(initialTasks);function handleAddTask(text) {setTasks([...tasks,{id: nextId++,text: text,done: false,},]);}function handleChangeTask(task) {setTasks(tasks.map((t) => {if (t.id === task.id) {return task;} else {return t;}}));}function handleDeleteTask(taskId) {setTasks(tasks.filter((t) => t.id !== taskId));}return (<><h1>布拉格的行程安排</h1><AddTask onAddTask={handleAddTask} /><TaskListtasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></>);
}let nextId = 3;
const initialTasks = [{id: 0, text: '參觀卡夫卡博物館', done: true},{id: 1, text: '看木偶戲', done: false},{id: 2, text: '打卡列儂墻', done: false},
];
你可以通過三個(gè)步驟將 useState 遷移到 useReducer
第 1 步: 將設(shè)置狀態(tài)的邏輯修改成 dispatch 的一個(gè) action
移除所有的狀態(tài)設(shè)置邏輯。只留下三個(gè)事件處理函數(shù):
- handleAddTask(text) 在用戶點(diǎn)擊 “添加” 時(shí)被調(diào)用。
- handleChangeTask(task) 在用戶切換任務(wù)或點(diǎn)擊 “保存” 時(shí)被調(diào)用。
- handleDeleteTask(taskId) 在用戶點(diǎn)擊 “刪除” 時(shí)被調(diào)用。
使用 reducers 管理狀態(tài)與直接設(shè)置狀態(tài)略有不同。它不是通過設(shè)置狀態(tài)來告訴 React “要做什么”,而是通過事件處理程序 dispatch 一個(gè) “action” 來指明 “用戶剛剛做了什么”。(而狀態(tài)更新邏輯則保存在其他地方!)因此,我們不再通過事件處理器直接 “設(shè)置 task”,而是 dispatch 一個(gè) “添加/修改/刪除任務(wù)” 的 action。這更加符合用戶的思維。
function handleAddTask(text) {dispatch({type: 'added',id: nextId++,text: text,});
}function handleChangeTask(task) {dispatch({type: 'changed',task: task,});
}function handleDeleteTask(taskId) {dispatch({type: 'deleted',id: taskId,});
}
第 2 步: 編寫一個(gè) reducer 函數(shù)
reducer 函數(shù)就是你放置狀態(tài)邏輯的地方。它接受兩個(gè)參數(shù),分別為當(dāng)前 state 和 action 對(duì)象,并且返回的是更新后的 state
在這個(gè)例子中,要將狀態(tài)設(shè)置邏輯從事件處理程序移到 reducer 函數(shù)中,你需要:
- 聲明當(dāng)前狀態(tài)(tasks)作為第一個(gè)參數(shù);
- 聲明 action 對(duì)象作為第二個(gè)參數(shù);
- 從 reducer 返回 下一個(gè) 狀態(tài)(React 會(huì)將舊的狀態(tài)設(shè)置為這個(gè)最新的狀態(tài))
function tasksReducer(tasks, action) {if (action.type === 'added') {return [...tasks,{id: action.id,text: action.text,done: false,},];} else if (action.type === 'changed') {return tasks.map((t) => {if (t.id === action.task.id) {return action.task;} else {return t;}});} else if (action.type === 'deleted') {return tasks.filter((t) => t.id !== action.id);} else {throw Error('未知 action: ' + action.type);}
}
上面的代碼使用了 if/else 語句,但是在 reducers 中使用 switch 語句 是一種慣例。兩種方式結(jié)果是相同的,但 switch 語句讀起來一目了然
function tasksReducer(tasks, action) {switch (action.type) {case 'added': {return [...tasks,{id: action.id,text: action.text,done: false,},];}case 'changed': {return tasks.map((t) => {if (t.id === action.task.id) {return action.task;} else {return t;}});}case 'deleted': {return tasks.filter((t) => t.id !== action.id);}default: {throw Error('未知 action: ' + action.type);}}
}
Reduce()reduce()
是 JavaScript 中數(shù)組的高階函數(shù)之一,用于將數(shù)組中的元素歸納為單個(gè)值。它接受一個(gè)回調(diào)函數(shù)作為參數(shù),這個(gè)回調(diào)函數(shù)可以進(jìn)行累積操作。
語法
array.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue);
callback
:用于處理數(shù)組中每個(gè)元素的回調(diào)函數(shù),包含四個(gè)參數(shù):accumulator
:累積器,累積計(jì)算的結(jié)果。currentValue
:當(dāng)前處理的元素。currentIndex
(可選):當(dāng)前處理的元素的索引。array
(可選):調(diào)用reduce
的數(shù)組。
initialValue
(可選):作為第一次調(diào)用callback
時(shí)的第一個(gè)參數(shù)accumulator
的初始值。如果不提供,將使用數(shù)組的第一個(gè)元素作為初始值,且callback
不會(huì)在數(shù)組的第一個(gè)元素上調(diào)用。
示例
// 計(jì)算數(shù)組元素的和
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 輸出 15// 找出數(shù)組中的最大值
const max = numbers.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue));
console.log(max); // 輸出 5
使用場景
- 累加或累乘:計(jì)算數(shù)組中所有元素的和或積。
- 查找最值:找出數(shù)組中的最大值或最小值。
- 轉(zhuǎn)換數(shù)據(jù)格式:將數(shù)組轉(zhuǎn)換成其他數(shù)據(jù)結(jié)構(gòu)。
- 歸納操作:進(jìn)行復(fù)雜的歸納計(jì)算。
注意事項(xiàng)
- 在沒有提供
initialValue
的情況下,reduce
將從數(shù)組的第二個(gè)元素開始調(diào)用回調(diào)函數(shù)。 - 如果數(shù)組為空且沒有提供
initialValue
,reduce
將拋出TypeError
。 - 回調(diào)函數(shù)在數(shù)組有元素時(shí)至少會(huì)被調(diào)用一次。
reduce
不會(huì)改變原數(shù)組。
示例
// 使用 reduce 計(jì)算數(shù)組中的平均值
const numbers = [10, 20, 30, 40, 50];
const average = numbers.reduce((accumulator, currentValue, index, array) => {accumulator += currentValue;if (index === array.length - 1) {return accumulator / array.length;} else {return accumulator;}
}, 0);console.log(average); // 輸出 30
在這個(gè)示例中,reduce
被用于計(jì)算數(shù)組 numbers
中所有元素的平均值。
第 3 步: 在組件中使用 reducer
你需要將 tasksReducer 導(dǎo)入到組件中。記得先從 React 中導(dǎo)入 useReducer Hook:
import { useReducer } from 'react';
接下來,你就可以替換掉之前的 useState:
const [tasks, setTasks] = useState(initialTasks);
只需要像下面這樣使用 useReducer:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer 和 useState 很相似——你必須給它傳遞一個(gè)初始狀態(tài),它會(huì)返回一個(gè)有狀態(tài)的值和一個(gè)設(shè)置該狀態(tài)的函數(shù)(在這個(gè)例子中就是 dispatch 函數(shù))。但是,它們兩個(gè)之間還是有點(diǎn)差異的。
useReducer 鉤子接受 2 個(gè)參數(shù):
- 一個(gè) reducer 函數(shù)
- 一個(gè)初始的 state
它返回如下內(nèi)容:
- 一個(gè)有狀態(tài)的值
- 一個(gè) dispatch 函數(shù)(用來 “派發(fā)” 用戶操作給 reducer)
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';export default function TaskApp() {const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);function handleAddTask(text) {dispatch({type: 'added',id: nextId++,text: text,});}function handleChangeTask(task) {dispatch({type: 'changed',task: task,});}function handleDeleteTask(taskId) {dispatch({type: 'deleted',id: taskId,});}return (<><h1>布拉格的行程安排</h1><AddTask onAddTask={handleAddTask} /><TaskListtasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></>);
}function tasksReducer(tasks, action) {switch (action.type) {case 'added': {return [...tasks,{id: action.id,text: action.text,done: false,},];}case 'changed': {return tasks.map((t) => {if (t.id === action.task.id) {return action.task;} else {return t;}});}case 'deleted': {return tasks.filter((t) => t.id !== action.id);}default: {throw Error('未知 action: ' + action.type);}}
}let nextId = 3;
const initialTasks = [{id: 0, text: '參觀卡夫卡博物館', done: true},{id: 1, text: '看木偶戲', done: false},{id: 2, text: '打卡列儂墻', done: false}
];
對(duì)比 useState 和 useReducer
- 代碼體積: 通常,在使用 useState 時(shí),一開始只需要編寫少量代碼。而 useReducer 必須提前編寫 reducer 函數(shù)和需要調(diào)度的 actions。但是,當(dāng)多個(gè)事件處理程序以相似的方式修改 state 時(shí),useReducer 可以減少代碼量。
- 可讀性: 當(dāng)狀態(tài)更新邏輯足夠簡單時(shí),useState 的可讀性還行。但是,一旦邏輯變得復(fù)雜起來,它們會(huì)使組件變得臃腫且難以閱讀。在這種情況下,useReducer 允許你將狀態(tài)更新邏輯與事件處理程序分離開來。
- 可調(diào)試性: 當(dāng)使用 useState 出現(xiàn)問題時(shí), 你很難發(fā)現(xiàn)具體原因以及為什么。 而使用 useReducer 時(shí), 你可以在 reducer 函數(shù)中通過打印日志的方式來觀察每個(gè)狀態(tài)的更新,以及為什么要更新(來自哪個(gè) action)。 如果所有 action 都沒問題,你就知道問題出在了 reducer 本身的邏輯中。 然而,與使用 useState 相比,你必須單步執(zhí)行更多的代碼。
- 可測試性: reducer 是一個(gè)不依賴于組件的純函數(shù)。這就意味著你可以單獨(dú)對(duì)它進(jìn)行測試。一般來說,我們最好是在真實(shí)環(huán)境中測試組件,但對(duì)于復(fù)雜的狀態(tài)更新邏輯,針對(duì)特定的初始狀態(tài)和 action,斷言 reducer 返回的特定狀態(tài)會(huì)很有幫助。
- 個(gè)人偏好: 并不是所有人都喜歡用 reducer,沒關(guān)系,這是個(gè)人偏好問題。你可以隨時(shí)在 useState 和 useReducer 之間切換,它們能做的事情是一樣的
練習(xí)1,2,3,4 暫時(shí)擱置
使用 Context 深層傳遞參數(shù)
通常來說,你會(huì)通過 props 將信息從父組件傳遞到子組件。但是,如果你必須通過許多中間組件向下傳遞 props,或是在你應(yīng)用中的許多組件需要相同的信息,傳遞 props 會(huì)變的十分冗長和不便。Context 允許父組件向其下層無論多深的任何組件提供信息,而無需通過 props 顯式傳遞
傳遞 props 帶來的問題
Context:傳遞 props 的另一種方法
Context 讓父組件可以為它下面的整個(gè)組件樹提供數(shù)據(jù)
你可以通過以下三個(gè)步驟來實(shí)現(xiàn)它:
- 創(chuàng)建 一個(gè) context。(你可以將其命名為 LevelContext, 因?yàn)樗硎镜氖菢?biāo)題級(jí)別。)
- 在需要數(shù)據(jù)的組件內(nèi) 使用 剛剛創(chuàng)建的 context。(Heading 將會(huì)使用 LevelContext。)
- 在指定數(shù)據(jù)的組件中 提供 這個(gè) context。 (Section 將會(huì)提供 LevelContext。
寫在你使用 context 之前
使用 Context 看起來非常誘人!然而,這也意味著它也太容易被過度使用了。如果你只想把一些 props 傳遞到多個(gè)層級(jí)中,這并不意味著你需要把這些信息放到 context 里
在使用 context 之前,你可以考慮以下幾種替代方案:
- 從 傳遞 props 開始。 如果你的組件看起來不起眼,那么通過十幾個(gè)組件向下傳遞一堆 props 并不罕見。這有點(diǎn)像是在埋頭苦干,但是這樣做可以讓哪些組件用了哪些數(shù)據(jù)變得十分清晰!維護(hù)你代碼的人會(huì)很高興你用 props 讓數(shù)據(jù)流變得更加清晰。
- 抽象組件并 將 JSX 作為children傳遞 給它們。 如果你通過很多層不使用該數(shù)據(jù)的中間組件(并且只會(huì)向下傳遞)來傳遞數(shù)據(jù),這通常意味著你在此過程中忘記了抽象組件。舉個(gè)例子,你可能想傳遞一些像 posts 的數(shù)據(jù) props 到不會(huì)直接使用這個(gè)參數(shù)的組件,類似 。取而代之的是,讓 Layout 把 children 當(dāng)做一個(gè)參數(shù),然后渲染 。這樣就減少了定義數(shù)據(jù)的組件和使用數(shù)據(jù)的組件之間的層級(jí)
Context 的使用場景
- 主題: 如果你的應(yīng)用允許用戶更改其外觀(例如暗夜模式),你可以在應(yīng)用頂層放一個(gè) context provider,并在需要調(diào)整其外觀的組件中使用該 context。
- 當(dāng)前賬戶: 許多組件可能需要知道當(dāng)前登錄的用戶信息。將它放到 context 中可以方便地在樹中的任何位置讀取它。某些應(yīng)用還允許你同時(shí)操作多個(gè)賬戶(例如,以不同用戶的身份發(fā)表評(píng)論)。在這些情況下,將 UI 的一部分包裹到具有不同賬戶數(shù)據(jù)的 provider 中會(huì)很方便。
- 路由: 大多數(shù)路由解決方案在其內(nèi)部使用 context 來保存當(dāng)前路由。這就是每個(gè)鏈接“知道”它是否處于活動(dòng)狀態(tài)的方式。如果你創(chuàng)建自己的路由庫,你可能也會(huì)這么做。
- 狀態(tài)管理: 隨著你的應(yīng)用的增長,最終在靠近應(yīng)用頂部的位置可能會(huì)有很多 state。許多遙遠(yuǎn)的下層組件可能想要修改它們。通常 將 reducer 與 context 搭配使用來管理復(fù)雜的狀態(tài)并將其傳遞給深層的組件來避免過多的麻煩
傳遞 Context 的方法:
- 通過 export const MyContext = createContext(defaultValue) 創(chuàng)建并導(dǎo)出 context。
- 在無論層級(jí)多深的任何子組件中,把 context 傳遞給 useContext(MyContext) Hook 來讀取它。
- 在父組件中把 children 包在 <MyContext.Provider value={…}> 中來提供 context。
使用 Reducer 和 Context 拓展你的應(yīng)用
下面將介紹如何結(jié)合使用 reducer 和 context:
- 創(chuàng)建 context。
- 將 state 和 dispatch 放入 context。
- 在組件樹的任何地方 使用 context
應(yīng)急方案
使用 ref 引用值
當(dāng)你希望組件“記住”某些信息,但又不想讓這些信息 觸發(fā)新的渲染 時(shí),你可以使用 ref 。
給你的組件添加 ref
你可以通過從 React 導(dǎo)入 useRef Hook 來為你的組件添加一個(gè) ref:
import { useRef } from 'react';
在你的組件內(nèi),調(diào)用 useRef Hook 并傳入你想要引用的初始值作為唯一參數(shù)。例如,這里的 ref 引用的值是“0”:
const ref = useRef(0);
useRef 返回一個(gè)這樣的對(duì)象:
{ current: 0 // 你向 useRef 傳入的值}
import { useRef } from 'react';export default function Counter() {let ref = useRef(0);function handleClick() {ref.current = ref.current + 1;alert('你點(diǎn)擊了 ' + ref.current + ' 次!');}return (<button onClick={handleClick}>點(diǎn)擊我!</button>);
}
組件不會(huì)在每次遞增時(shí)重新渲染。 與 state 一樣,React 會(huì)在每次重新渲染之間保留 ref。但是,設(shè)置 state 會(huì)重新渲染組件,更改 ref 不會(huì)!
ref 和 state 的不同之處
useRef 內(nèi)部是如何運(yùn)行的?
盡管 useState 和 useRef 都是由 React 提供的,原則上 useRef 可以在 useState的基礎(chǔ)上 實(shí)現(xiàn)。 你可以想象在 React 內(nèi)部,useRef 是這樣實(shí)現(xiàn)的:
// React 內(nèi)部function useRef(initialValue) {const [ref, unused] = useState({ current: initialValue });return ref;}
何時(shí)使用 ref
- 存儲(chǔ) timeout ID
- 存儲(chǔ)和操作 DOM 元素,我們將在 下一頁 中介紹
- 存儲(chǔ)不需要被用來計(jì)算 JSX 的其他對(duì)象
ref 的最佳實(shí)踐
遵循這些原則將使你的組件更具可預(yù)測性:
- 將 ref 視為應(yīng)急方案。 當(dāng)你使用外部系統(tǒng)或?yàn)g覽器 API 時(shí),ref 很有用。如果你很大一部分應(yīng)用程序邏輯和數(shù)據(jù)流都依賴于 ref,你可能需要重新考慮你的方法。
- 不要在渲染過程中讀取或?qū)懭?ref.current。 如果渲染過程中需要某些信息,請(qǐng)使用 state 代替。由于 React 不知道 ref.current 何時(shí)發(fā)生變化,即使在渲染時(shí)讀取它也會(huì)使組件的行為難以預(yù)測。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 這樣的代碼,它只在第一次渲染期間設(shè)置一次 ref。)
使用 ref 操作 DOM
獲取指向節(jié)點(diǎn)的 ref
要訪問由 React 管理的 DOM 節(jié)點(diǎn),首先,引入 useRef Hook:
import { useRef } from 'react';
然后,在你的組件中使用它聲明一個(gè) ref:
const myRef = useRef(null);
Finally, pass your ref as the ref attribute to the JSX tag for which you want to get the DOM node:
<div ref={myRef}>
useRef Hook 返回一個(gè)對(duì)象,該對(duì)象有一個(gè)名為 current 的屬性。最初,myRef.current 是 null。當(dāng) React 為這個(gè)
// 你可以使用任意瀏覽器 API,例如:myRef.current.scrollIntoView();
在上面的例子中,ref 的數(shù)量是預(yù)先確定的。但有時(shí)候,你可能需要為列表中的每一項(xiàng)都綁定 ref ,而你又不知道會(huì)有多少項(xiàng)。像下面這樣做是行不通的:
<ul>{items.map((item) => {// 行不通!const ref = useRef(null);return <li ref={ref} />;})}</ul>
這是因?yàn)?Hook 只能在組件的頂層被調(diào)用。不能在循環(huán)語句、條件語句或 map() 函數(shù)中調(diào)用 useRef
訪問另一個(gè)組件的 DOM 節(jié)點(diǎn)
默認(rèn)情況下,React 不允許組件訪問其他組件的 DOM 節(jié)點(diǎn)。甚至自己的子組件也不行
想要 暴露其 DOM 節(jié)點(diǎn)的組件必須選擇該行為。一個(gè)組件可以指定將它的 ref “轉(zhuǎn)發(fā)”給一個(gè)子組件。下面是 MyInput 如何使用 forwardRef API:
const MyInput = forwardRef((props, ref) => {return <input {...props} ref={ref} />;});
使用 Effect 同步
有些組件需要與外部系統(tǒng)同步。例如,你可能希望根據(jù) React state 控制非 React 組件、設(shè)置服務(wù)器連接或在組件出現(xiàn)在屏幕上時(shí)發(fā)送分析日志。Effects 會(huì)在渲染后運(yùn)行一些代碼,以便可以將組件與 React 之外的某些系統(tǒng)同步
在本文和后續(xù)文本中,Effect 在 React 中是專有定義——由渲染引起的副作用。為了指代更廣泛的編程概念,也可以將其稱為“副作用(side effect)
不要隨意在你的組件中使用 Effect。記住,Effect 通常用于暫時(shí)“跳出” React 代碼并與一些 外部 系統(tǒng)進(jìn)行同步。這包括瀏覽器 API、第三方小部件,以及網(wǎng)絡(luò)等等。如果你想用 Effect 僅根據(jù)其他狀態(tài)調(diào)整某些狀態(tài),那么 你可能不需要 Effect。
如何編寫 Effect
第一步:聲明 Effect
首先在 React 中引入 useEffect Hook:
import { useEffect } from 'react';
然后,在組件頂部調(diào)用它,并傳入在每次渲染時(shí)都需要執(zhí)行的代碼:
function MyComponent() {useEffect(() => {// 每次渲染后都會(huì)執(zhí)行此處的代碼});return <div />;}
一般來說,Effect 會(huì)在 每次 渲染后執(zhí)行,而以下代碼會(huì)陷入死循環(huán)中:
const [count, setCount] = useState(0);useEffect(() => {setCount(count + 1);});
每次渲染結(jié)束都會(huì)執(zhí)行 Effect;而更新 state 會(huì)觸發(fā)重新渲染。但是新一輪渲染時(shí)又會(huì)再次執(zhí)行 Effect,然后 Effect 再次更新 state……如此周而復(fù)始,從而陷入死循環(huán)
第二步:指定 Effect 依賴
一般來說,Effect 會(huì)在 每次 渲染時(shí)執(zhí)行。但更多時(shí)候,并不需要每次渲染的時(shí)候都執(zhí)行 Effect。
- 有時(shí)這會(huì)拖慢運(yùn)行速度。因?yàn)榕c外部系統(tǒng)的同步操作總是有一定時(shí)耗,在非必要時(shí)可能希望跳過它。例如,沒有人會(huì)希望每次用鍵盤打字時(shí)都重新連接聊天服務(wù)器。
- 有時(shí)這會(huì)導(dǎo)致程序邏輯錯(cuò)誤。例如,組件的淡入動(dòng)畫只需要在第一輪渲染出現(xiàn)時(shí)播放一次,而不是每次觸發(fā)新一輪渲染后都播放。
useEffect(() => {if (isPlaying) { // isPlaying 在此處使用……// ...} else {// ...}}, [isPlaying]); // ……所以它必須在此處聲明!
指定 [isPlaying] 會(huì)告訴 React,如果 isPlaying 在上一次渲染時(shí)與當(dāng)前相同,它應(yīng)該跳過重新運(yùn)行 Effect
沒有依賴數(shù)組作為第二個(gè)參數(shù),與依賴數(shù)組位空數(shù)組 [] 的行為是不一致的:
useEffect(() => {// 這里的代碼會(huì)在每次渲染后執(zhí)行});useEffect(() => {// 這里的代碼只會(huì)在組件掛載后執(zhí)行}, []);useEffect(() => {//這里的代碼只會(huì)在每次渲染后,并且 a 或 b 的值與上次渲染不一致時(shí)執(zhí)行}, [a, b]);
第三部:按需添加清理(cleanup)函數(shù)
可以在 Effect 中返回一個(gè) 清理(cleanup) 函數(shù)。useEffect(() => {const connection = createConnection();connection.connect();return () => {connection.disconnect();};}, []);
每次重新執(zhí)行 Effect 之前,React 都會(huì)調(diào)用清理函數(shù);組件被卸載時(shí),也會(huì)調(diào)用清理函數(shù)
訂閱事件
如果 Effect 訂閱了某些事件,清理函數(shù)應(yīng)該退訂這些事件:
useEffect(() => {function handleScroll(e) {console.log(window.scrollX, window.scrollY);}window.addEventListener('scroll', handleScroll);return () => window.removeEventListener('scroll', handleScroll);}, []);
觸發(fā)動(dòng)畫
如果 Effect 對(duì)某些內(nèi)容加入了動(dòng)畫,清理函數(shù)應(yīng)將動(dòng)畫重置:
useEffect(() => {const node = ref.current;node.style.opacity = 1; // 觸發(fā)動(dòng)畫return () => {node.style.opacity = 0; // 重置為初始值};}, []);
初始化應(yīng)用時(shí)不需要使用 Effect 的情形
某些邏輯應(yīng)該只在應(yīng)用程序啟動(dòng)時(shí)運(yùn)行一次。比如,驗(yàn)證登陸狀態(tài)和加載本地程序數(shù)據(jù)。你可以將其放在組件之外:
if (typeof window !== 'undefined') { // 檢查是否在瀏覽器中運(yùn)行checkAuthToken();loadDataFromLocalStorage();}function App() {// ……}
你可能不需要 Effect
Effect 是 React 范式中的一個(gè)逃脫方案。它們讓你可以 “逃出” React 并使組件和一些外部系統(tǒng)同步,比如非 React 組件、網(wǎng)絡(luò)和瀏覽器 DOM。如果沒有涉及到外部系統(tǒng)(例如,你想根據(jù) props 或 state 的變化來更新一個(gè)組件的 state),你就不應(yīng)該使用 Effect
響應(yīng)式 Effect 的生命周期
Effect 的生命周期
每個(gè) React 組件都經(jīng)歷相同的生命周期:
- 當(dāng)組件被添加到屏幕上時(shí),它會(huì)進(jìn)行組件的 掛載。
- 當(dāng)組件接收到新的 props 或 state 時(shí),通常是作為對(duì)交互的響應(yīng),它會(huì)進(jìn)行組件的 更新。
- 當(dāng)組件從屏幕上移除時(shí),它會(huì)進(jìn)行組件的 卸載