React組件、狀態(tài)管理、代碼優(yōu)化的技巧
文章總結(jié)了React組件設(shè)計(jì)、狀態(tài)管理、代碼組織和優(yōu)化的技巧,它涵蓋了使用Fragment、props解構(gòu)、defaultProps、key和ref的使用、渲染性能優(yōu)化等方面。
一. 組件相關(guān)
1. 使用自閉合組件
// 不好的寫法 <Component></Component> // 推薦寫法 <Component />
2. 推薦使用Fragment組件而不是 DOM 元素來分組元素
在 React 中,每個(gè)組件必須返回單個(gè)元素。不要將多個(gè)元素包裝在 <div> 或 <span> 中,而是使用 <Fragment> 來保持 DOM 整潔。
不好的寫法:使用 div 會(huì)使 DOM 變得雜亂,并且可能需要更多 CSS 代碼。
import Header from "./header"; import Content from "./content"; import Footer from "./footer"; const Test = () => { return ( <div> <Header /> <Content /> <Footer /> </div> ); };
推薦寫法: <Fragment> 包裝元素而不影響 DOM 結(jié)構(gòu)。
import Header from "./header"; import Content from "./content"; import Footer from "./footer"; const Test = () => { return ( // 如果元素不需要添加屬性,則可以使用簡(jiǎn)寫形式<></> <Fragment> <Header /> <Content /> <Footer /> </Fragment> ); };
3. 使用 React fragment 簡(jiǎn)寫 <></>(除非你需要設(shè)置一個(gè) key 屬性)
不好寫法:下面的代碼有點(diǎn)冗余。
const Test = () => { return ( <Fragment> <Header /> <Content /> <Footer /> </Fragment> ); };
推薦寫法:
const Test = () => { return ( <> <Header /> <Content /> <Footer /> </> ); };
除非你需要一個(gè) key 屬性。
const Tools = ({ tools }) => { return ( <Container> { tools?.map((item, index) => { <Fragment key={`${item.id}-${index}`}> <span>{ item.id }</span> <span>{ item.name }</span> <Fragment> }) } </Container> ) }
4. 優(yōu)先分散使用 props,而不是單獨(dú)訪問每個(gè) props
不好的寫法: 下面的代碼更難閱讀(特別是在項(xiàng)目較大時(shí))。
const TodoLists = (props) => ( <div className="todo-list"> {props.todoList?.map((todo, index) => ( <div className="todo-list-item" key={todo.uuid}> <p onClick={() => props.seeDetail?.(todo)}> {todo?.uuid}:{todo.text} </p> <div className="todo-list-item-btn-group"> <button type="button" onClick={() => props.handleEdit?.(todo, index)}> 編輯 </button> <button type="button" onClick={() => props.handleDelete?.(todo, index)} > 刪除 </button> </div> </div> ))} </div> ); export default TodoLists;
推薦寫法: 下面的代碼更加簡(jiǎn)潔。
const TodoLists = ({ todoList, seeDetail, handleEdit, handleDelete }) => ( <div className="todo-list"> {todoList?.map((todo, index) => ( <div className="todo-list-item" key={todo.uuid}> <p onClick={() => seeDetail?.(todo)}> {todo?.uuid}:{todo.text} </p> <div className="todo-list-item-btn-group"> <button type="button" onClick={() => handleEdit?.(todo, index)}> 編輯 </button> <button type="button" onClick={() => handleDelete?.(todo, index)}> 刪除 </button> </div> </div> ))} </div> ); export default TodoLists;
5. 設(shè)置 props 的默認(rèn)值時(shí),在解構(gòu)時(shí)進(jìn)行
不好的寫法: 你可能需要在多個(gè)地方定義默認(rèn)值并引入新變量。
const Text = ({ size, type }) => { const Component = type || "span"; const comSize = size || "mini"; return <Component size={comSize} />; };
推薦寫法,直接在對(duì)象解構(gòu)里給出默認(rèn)值。
const Text = ({ size = "mini", type: Component = "span" }) => { return <Component size={comSize} />; };
6. 傳遞字符串類型屬性時(shí)刪除花括號(hào)。
不好的寫法:帶花括號(hào)的寫法
<button type={"button"} className={"btn"}> 按鈕 </button>
推薦寫法: 不需要花括號(hào)
<button type="button" className="btn"> 按鈕 </button>
7. 在使用 value && <Component {...props}/> 之前確保 value 值是布爾值,以防止顯示意外的值。
不好的寫法: 當(dāng)列表的長(zhǎng)度為 0,則有可能顯示 0。
const DataList = ({ data }) => { return <Container>{data.length && <List data={data} />}</Container>; };
推薦寫法: 當(dāng)列表沒有數(shù)據(jù)時(shí),則不會(huì)渲染任何東西。
const DataList = ({ data }) => { return <Container>{data.length > 0 && <List data={data} />}</Container>; };
8. 使用函數(shù)(內(nèi)聯(lián)或非內(nèi)聯(lián))避免中間變量污染你的上下文
不好的寫法: 變量 totalCount 和 totalPrice 使組件的上下文變得混亂。
const GoodList = ({ goods }) => { if (goods.length === 0) { return <>暫無(wú)數(shù)據(jù)</>; } let totalCount = 0; let totalPrice = 0; goods.forEach((good) => { totalCount += good.count; totalPrice += good.price; }); return ( <> 總數(shù)量:{totalCount};總價(jià):{totalPrice} </> ); };
推薦寫法: 將變量 totalCount 和 totalPrice 控制在一個(gè)函數(shù)內(nèi)。
const GoodList = ({ goods }) => { if (goods.length === 0) { return <>暫無(wú)數(shù)據(jù)</>; } // 使用函數(shù) const { totalCount, totalPrice, } = () => { let totalCount = 0, totalPrice = 0; goods.forEach((good) => { totalCount += good.count; totalPrice += good.price; }); return { totalCount, totalPrice }; }; return ( <> 總數(shù)量:{totalCount};總價(jià):{totalPrice} </> ); };
個(gè)人更喜歡的寫法: 封裝成 hooks 來使用。
const useTotalGoods = ({ goods }) => { let totalCount = 0, totalPrice = 0; goods.forEach((good) => { totalCount += good.count; totalPrice += good.price; }); return { totalCount, totalPrice }; }; const GoodList = ({ goods }) => { if (goods.length === 0) { return <>暫無(wú)數(shù)據(jù)</>; } const { totalCount, totalPrice } = useTotalGoods(goods); return ( <> 總數(shù)量:{totalCount};總價(jià):{totalPrice} </> ); };
9. 使用柯里化函數(shù)重用邏輯(并正確緩存回調(diào)函數(shù))
不好的寫法: 表單更新字段重復(fù)。
const UserLoginForm = () => { const [{ username, password }, setFormUserState] = useState({ username: "", password: "", }); return ( <> <h1>登陸</h1> <form> <div class="form-item"> <label>用戶名:</label> <input placeholder="請(qǐng)輸入用戶名" value={username} onChange={(e) => setFormUserState((state) => ({ ...state, username: e.target.value, })) } /> </div> <div class="form-item"> <label>密碼:</label> <input placeholder="請(qǐng)輸入密碼" value={username} type="password" onChange={(e) => setFormUserState((state) => ({ ...state, password: e.target.value, })) } /> </div> </form> </> ); };
推薦寫法: 引入 createFormValueChangeHandler 方法,為每個(gè)字段返回正確的處理方法。
筆記: 如果你啟用了 ESLint 規(guī)則 jsx-no-bind,此技巧尤其有用。你只需將柯里化函數(shù)包裝在 useCallback 中。
const UserLoginForm = () => { const [{ username, password }, setFormUserState] = useState({ username: "", password: "", }); const createFormValueChangeHandler = (field: string) => { return (e) => { setFormUserState((state) => ({ ...state, [field]: e.target.value, })); }; }; return ( <> <h1>登陸</h1> <form> <div class="form-item"> <label>用戶名:</label> <input placeholder="請(qǐng)輸入用戶名" value={username} onChange={createFormValueChangeHandler("username")} /> </div> <div class="form-item"> <label>密碼:</label> <input placeholder="請(qǐng)輸入密碼" value={username} type="password" onChange={createFormValueChangeHandler("password")} /> </div> </form> </> ); };
10. 將不依賴組件 props/state 的數(shù)據(jù)移到組件外部,以獲得更干凈(和更高效)的代碼
不好的寫法: OPTIONS 和 renderOption 不需要位于組件內(nèi)部,因?yàn)樗鼈儾灰蕾嚾魏?props 或狀態(tài)。此外,將它們保留在內(nèi)部意味著每次組件渲染時(shí)我們都會(huì)獲得新的對(duì)象引用。如果我們將 renderOption 傳遞給包裹在 memo 中的子組件,則會(huì)破壞緩存功能。
const ToolSelector = () => { const options = [ { label: "html工具", value: "html-tool", }, { label: "css工具", value: "css-tool", }, { label: "js工具", value: "js-tool", }, ]; const renderOption = ({ label, value, }: { label?: string; value?: string; }) => <Option value={value}>{label}</Option>; return ( <Select placeholder="請(qǐng)選擇工具"> {options.map((item, index) => ( <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment> ))} </Select> ); };
推薦寫法: 將它們移出組件以保持組件干凈和引用穩(wěn)定。
const options = [ { label: "html工具", value: "html-tool", }, { label: "css工具", value: "css-tool", }, { label: "js工具", value: "js-tool", }, ]; const renderOption = ({ label, value }: { label?: string; value?: string }) => ( <Option value={value}>{label}</Option> ); const ToolSelector = () => { return ( <Select placeholder="請(qǐng)選擇工具"> {options.map((item, index) => ( <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment> ))} </Select> ); };
筆記: 在這個(gè)示例中,你可以通過使用選項(xiàng)元素內(nèi)聯(lián)來進(jìn)一步簡(jiǎn)化。
const options = [ { label: "html工具", value: "html-tool", }, { label: "css工具", value: "css-tool", }, { label: "js工具", value: "js-tool", }, ]; const ToolSelector = () => { return ( <Select placeholder="請(qǐng)選擇工具"> {options.map((item, index) => ( <Option value={item.value} key={`${item.value}-${index}`}> {item.label} </Option> ))} </Select> ); };
11. 存儲(chǔ)列表組件中選定的對(duì)象時(shí),存儲(chǔ)對(duì)象 ID,而不是整個(gè)對(duì)象
不好的寫法: 如果選擇了某個(gè)對(duì)象但隨后它發(fā)生了變化(即,我們收到了相同 ID 的全新對(duì)象引用),或者該對(duì)象不再存在于列表中,則 selectedItem 將保留過時(shí)的值或變得不正確。
const List = ({ data }) => { // 引用的是整個(gè)選中的是對(duì)象 const [selectedItem, setSelectedItem] = useState<Item | undefined>(); return ( <> {selectedItem && <div>{selectedItem.value}</div>} <List data={data} onSelect={setSelectedItem} selectedItem={selectedItem} /> </> ); };
推薦寫法: 我們通過 ID(應(yīng)該是穩(wěn)定的)存儲(chǔ)所選列表對(duì)象。這確保即使列表對(duì)象從列表中刪除或其某個(gè)屬性發(fā)生變化,UI 也應(yīng)該正確。
const List = ({ data }) => { const [selectedItemId, setSelectedItemId] = useState<string | number>(); // 我們從列表中根據(jù)選中id查找出選定的列表對(duì)象 const selectedItem = data.find((item) => item.id === selectedItemId); return ( <> {selectedItemId && <div>{selectedItem.value}</div>} <List data={data} onSelect={setSelectedItemId} selectedItemId={selectedItemId} /> </> ); };
12. 如果需要多次用到 prop 里面的值,那就引入一個(gè)新的組件
不好的寫法: 由于 type === null 的檢查使得代碼變得混亂。
注意: 由于hooks 規(guī)則,我們不能提前返回 null。
const CreatForm = ({ type }) => { const formList = useMemo(() => { if (type === null) { return []; } return getFormList({ type }); }, [type]); const onHandleChange = useCallback( (id) => { if (type === null) { return; } // do something }, [type] ); if (type === null) { return null; } return ( <> {formList.map(({ value, id, ...rest }, index) => ( <item.component value={value} onChange={onHandleChange} key={id} {...rest} /> ))} </> ); };
推薦寫法: 我們引入了一個(gè)新組件 FormLists,它采用定義的表單項(xiàng)組件并且更加簡(jiǎn)潔。
const FormList = ({ type }) => { const formList = useMemo(() => getFormList({ type }), [type]); const onHandleChange = useCallback( (id) => { // do something }, [type] ); return ( <> {formList.map(({ value, id, ...rest }, index) => ( <item.component value={value} onChange={onHandleChange} key={id} {...rest} /> ))} </> ); }; const CreateForm = ({ type }) => { if (type === null) { return null; } return <FormList type={type} />; };
13. 將所有狀態(tài)(state)和上下文(context)分組到組件頂部
當(dāng)所有狀態(tài)和上下文都位于頂部時(shí),很容易發(fā)現(xiàn)哪些因素會(huì)觸發(fā)組件重新渲染。
不好的寫法: 狀態(tài)和上下文分散,難以跟蹤。
const LoginForm = () => { const [username, setUsername] = useState(""); const onHandleChangeUsername = (e) => { setUserName(e.target.value); }; const [password, setPassword] = useState(""); const onHandleChangePassword = (e) => { setPassword(e.target.value); }; const theme = useContext(themeContext); return ( <div class={`login-form login-form-${theme}`}> <h1>login</h1> <form> <div class="login-form-item"> <label>用戶名:</label> <input value={username} onChange={onHandleChangeUsername} placeholder="請(qǐng)輸入用戶名" /> </div> <div class="login-form-item"> <label>密碼:</label> <input value={password} onChange={onHandleChangePassword} placeholder="請(qǐng)輸入密碼" type="password" /> </div> </form> </div> ); };
推薦寫法: 所有狀態(tài)和上下文都集中在頂部,以便于快速定位。
const LoginForm = () => { // context const theme = useContext(themeContext); // state const [password, setPassword] = useState(""); const [username, setUsername] = useState(""); // method const onHandleChangeUsername = (e) => { setUserName(e.target.value); }; const onHandleChangePassword = (e) => { setPassword(e.target.value); }; return ( <div class={`login-form login-form-${theme}`}> <h1>login</h1> <form> <div class="login-form-item"> <label>用戶名:</label> <input value={username} onChange={onHandleChangeUsername} placeholder="請(qǐng)輸入用戶名" /> </div> <div class="login-form-item"> <label>密碼:</label> <input value={password} onChange={onHandleChangePassword} placeholder="請(qǐng)輸入密碼" type="password" /> </div> </form> </div> ); };
二. 有效的設(shè)計(jì)模式與技巧
14. 利用 children 屬性來獲得更清晰的代碼(以及性能優(yōu)勢(shì))
利用子組件 props 來獲得更簡(jiǎn)潔的代碼(和性能優(yōu)勢(shì))。使用子組件 props 有幾個(gè)好處:
- 好處 1:你可以通過將 props 直接傳遞給子組件而不是通過父組件路由來避免 prop 混入。
- 好處 2:你的代碼更具可擴(kuò)展性,因?yàn)槟憧梢暂p松修改子組件而無(wú)需更改父組件。
- 好處 3:你可以使用此技巧避免重新渲染組件(參見下面的示例)。
不好的寫法: 每當(dāng) Timer 渲染時(shí),OtherSlowComponent 都會(huì)渲染,每次當(dāng)前時(shí)間更新時(shí)都會(huì)發(fā)生這種情況。
const Container = () => <Timer />; const Timer = () => { const [time, setTime] = useState(0); useEffect(() => { const intervalId = setInterval(() => setTime(new Date()), 1000); return () => { clearInterval(intervalId); }; }, []); return ( <> <h1>當(dāng)前時(shí)間:{dayjs(time).format("YYYY-MM-DD HH:mm:ss")}</h1> <OtherSlowComponent /> </> ); };
推薦寫法: Timer 呈現(xiàn)時(shí),OtherSlowComponent 不會(huì)呈現(xiàn)。
const Container = () => ( <Timer> <OtherSlowComponent /> </Timer> ); const Timer = ({ children }) => { const [time, setTime] = useState(0); useEffect(() => { const intervalId = setInterval(() => setTime(new Date()), 1000); return () => { clearInterval(intervalId); }; }, []); return ( <> <h1>當(dāng)前時(shí)間:{dayjs(time).formate("YYYY-MM-DD HH:mm:ss")}</h1> {children} </> ); };
15. 使用復(fù)合組件構(gòu)建可組合代碼
像搭積木一樣使用復(fù)合組件,將它們拼湊在一起以創(chuàng)建自定義 UI。這些組件在創(chuàng)建庫(kù)時(shí)效果極佳,可生成富有表現(xiàn)力且高度可擴(kuò)展的代碼。以下是一個(gè)以reach.ui為示例的代碼:
<Menu> <MenuButton> 操作吧 <span aria-hidden>?</span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert("下載")}>下載</MenuItem> <MenuItem onSelect={() => alert("復(fù)制")}>創(chuàng)建一個(gè)復(fù)制</MenuItem> <MenuLink as="a" rel="external nofollow" > 跳轉(zhuǎn)鏈接 </MenuLink> </MenuList> </Menu>
16. 使用渲染函數(shù)或組件函數(shù) props 使你的代碼更具可擴(kuò)展性
假設(shè)我們想要顯示各種列表,例如消息、個(gè)人資料或帖子,并且每個(gè)列表都應(yīng)該可排序。
為了實(shí)現(xiàn)這一點(diǎn),我們引入了一個(gè) List 組件以供重復(fù)使用。我們可以通過兩種方式解決這個(gè)問題:
不好的寫法:選項(xiàng) 1。
List 處理每個(gè)項(xiàng)目的渲染及其排序方式。這是有問題的,因?yàn)樗`反了開放封閉原則。每當(dāng)添加新的項(xiàng)目類型時(shí),此代碼都會(huì)被修改。
List.tsx:
export interface ListItem { id: string; } // 不好的列表組件寫法 // 我們還需要了解這些接口 type PostItem = ListItem & { title: string }; type UserItem = ListItem & { name: string; date: Date }; type ListNewItem = | { type: "post"; value: PostItem } | { type: "user"; value: UserItem }; interface BadListProps<T extends ListNewItem> { type: T["type"]; items: Array<T["value"]>; } const SortList = <T extends ListNewItem>({ type, items }: BadListProps<T>) => { const sortItems = [...items].sort((a, b) => { // 我們還需注意這里的比較邏輯,這里或者直接使用下方導(dǎo)出的比較函數(shù) return 0; }); return ( <> <h2>{type === "post" ? "帖子" : "用戶"}</h2> <ul className="sort-list"> {sortItems.map((item, index) => ( <li className="sort-list-item" key={`${item.id}-${index}`}> {(() => { switch (type) { case "post": return (item as PostItem).title; case "user": return ( <> <span>{(item as UserItem).name}</span> <span> - </span> <em> 加入時(shí)間: {(item as UserItem).date.toDateString()} </em> </> ); } })()} </li> ))} </ul> </> ); }; export function compareStrings(a: string, b: string): number { return a < b ? -1 : a == b ? 0 : 1; }
推薦寫法:選項(xiàng) 2。
List 采用渲染函數(shù)或組件函數(shù),僅在需要時(shí)調(diào)用它們。
List.tsx:
export interface ListItem { id: string; } interface ListProps<T extends ListItem> { items: T[]; // 列表數(shù)據(jù) header: React.ComponentType; // 頭部組件 itemRender: (item: T) => React.ReactNode; // 列表項(xiàng) itemCompare: (a: T, b: T) => number; // 列表項(xiàng)自定義排序函數(shù) } const SortList = <T extends ListItem>({ items, header: Header, itemRender, itemCompare, }: ListProps<T>) => { const sortedItems = [...items].sort(itemCompare); return ( <> <Header /> <ul className="sort-list"> {sortedItems.map((item, index) => ( <li className="sort-list-item" key={`${item.id}-${index}`}> {itemRender(item)} </li> ))} </ul> </> ); }; export default SortList;
完整示例代碼可前往這里查看。
17. 處理不同情況時(shí),使用 value === case && <Component /> 以避免保留舊狀態(tài)
不好的寫法: 在如下示例中,在切換時(shí)計(jì)數(shù)器 count 不會(huì)重置。發(fā)生這種情況的原因是,在渲染同一組件時(shí),其狀態(tài)在currentTab更改后保持不變。
tab.tsx:
const tabList = [ { label: "首頁(yè)", value: "tab-1", }, { label: "詳情頁(yè)", value: "tab-2", }, ]; export interface TabItem { label: string; value: string; } export interface TabProps { tabs: TabItem[]; currentTab: string | TabItem; onTab: (v: string | TabItem) => void; labelInValue?: boolean; } const Tab: React.FC<TabProps> = ({ tabs = tabList, currentTab, labelInValue, onTab, }) => { const currentTabValue = useMemo( () => (labelInValue ? (currentTab as TabItem)?.value : currentTab), [currentTab, labelInValue] ); return ( <div className="tab"> {tabs?.map((item, index) => ( <div className={`tab-item${ currentTabValue === item.value ? " active" : "" }`} key={`${item.value}-${index}`} onClick={() => onTab?.(labelInValue ? item : item.value)} > {item.label} </div> ))} </div> ); }; export default Tab;
Resource.tsx:
export interface ResourceProps { type: string; } const Resource: React.FC<ResourceProps> = ({ type }) => { const [count, setCount] = useState(0); const onHandleClick = () => { setCount((c) => c + 1); }; return ( <div className="tab-content"> 你當(dāng)前在{type === "tab-1" ? "首頁(yè)" : "詳情頁(yè)"}, <button onClick={onHandleClick} className="btn" type="button"> 點(diǎn)擊我 </button> 增加訪問{count}次數(shù) </div> ); };
推薦寫法: 根據(jù) currentTab 渲染組件或在類型改變時(shí)使用 key 強(qiáng)制重新渲染組件。
function App() { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> {currentTab === "tab-1" && <Resource type="tab-1" />} {currentTab === "tab-2" && <Resource type="tab-2" />} </> ); } // 使用key屬性 function App() { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> <Resource type={currentTab} key={currentTab} /> </> ); }
完整示例代碼可前往這里查看。
18. 始終使用錯(cuò)誤邊界處理組件渲染錯(cuò)誤
默認(rèn)情況下,如果你的應(yīng)用程序在渲染過程中遇到錯(cuò)誤,整個(gè) UI 都會(huì)崩潰。
為了防止這種情況,請(qǐng)使用錯(cuò)誤邊界來:
- 即使發(fā)生錯(cuò)誤,也要保持應(yīng)用程序的某些部分正常運(yùn)行。
- 顯示用戶友好的錯(cuò)誤消息并可選擇跟蹤錯(cuò)誤。
提示:你可以使用 react-error-boundary 庫(kù)。
三. key 與 ref
19. 使用 crypto.randomUUID 或 Math.random 生成 key
map 調(diào)用(也就是列表渲染)中的 JSX 元素始終需要 key。
假設(shè)你的元素還沒有 key。在這種情況下,你可以使用 crypto.randomUUID、Math.random 或 uuid 庫(kù)生成唯一 ID。
注意:請(qǐng)注意,舊版瀏覽器中未定義 crypto.randomUUID。
20. 確保你的列表項(xiàng) id 是穩(wěn)定的(即:它們?cè)阡秩局惺遣粫?huì)發(fā)生變化的)
盡可能的讓 id/key 可以穩(wěn)定。
否則,React 可能會(huì)無(wú)用地重新渲染某些組件,或者觸發(fā)一些功能異常,如下例所示。
不好的寫法: 每次 App 組件渲染時(shí) selectItemId 都會(huì)發(fā)生變化,因此設(shè)置 id 的值將永遠(yuǎn)不會(huì)正確。
const App = () => { const [items, setItems] = useState([]); const [selectItemId, setSelectItemId] = useState(undefined); const loadItems = () => { fetchItems().then((res) => setItems(res)); }; // 請(qǐng)求列表 useEffect(() => { loadItems(); }, []); // 添加列表id,這是一種很糟糕的做法 const newItems = items.map((item) => ({ ...item, id: crypto.randomUUID() })); return ( <List items={newItems} selectedItemId={selectItemId} onSelectItem={setSelectItemId} /> ); };
推薦寫法: 當(dāng)我們獲取列表項(xiàng)的時(shí)候添加 id。
const App = () => { const [items, setItems] = useState([]); const [selectItemId, setSelectItemId] = useState(undefined); const loadItems = () => { // 獲取列表數(shù)據(jù)并通過 id 保存 fetchItems().then((res) => // 一旦獲得結(jié)果,我們就會(huì)添加“id” setItems(res.map((item) => ({ ...item, id: crypto.randomUUID() }))) ); }; // 請(qǐng)求列表 useEffect(() => { loadItems(); }, []); return ( <List items={items} selectedItemId={selectItemId} onSelectItem={setSelectItemId} /> ); };
21. 策略性地使用 key 屬性來觸發(fā)組件重新渲染
想要強(qiáng)制組件從頭開始重新渲染?只需更改其 key 屬性即可。
在下面的示例中,我們使用此技巧在切換到新選項(xiàng)卡時(shí)重置錯(cuò)誤邊界。(該示例基于前面第 17 點(diǎn)所展示的示例改造)
Resource.tsx:
export interface ResourceProps { type: string; } const Resource: React.FC<ResourceProps> = ({ type }) => { const [count, setCount] = useState(0); const onHandleClick = () => { setCount((c) => c + 1); }; // 新增拋出異常的代碼 useEffect(() => { if (type === "tab-1") { throw new Error("該選項(xiàng)不可切換"); } }, []); return ( <div className="tab-content"> 你當(dāng)前在{type === "tab-1" ? "首頁(yè)" : "詳情頁(yè)"}, <button onClick={onHandleClick} className="btn" type="button"> 點(diǎn)擊我 </button> 增加訪問{count}次數(shù) </div> ); };
App.tsx:
import { ErrorBoundary } from "react-error-boundary"; const App = () => { const [currentTab, setCurrentTab] = useState("tab-1"); return ( <> <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} /> <ErrorBoundary fallback={<div className="error">組件渲染發(fā)生了一些錯(cuò)誤</div>} key={currentTab} // 如果沒有key屬性,當(dāng)currentTab值為“tab-2”時(shí)也會(huì)呈現(xiàn)錯(cuò)誤 > <Resource type={currentTab} /> </ErrorBoundary> </> ); };
完整示例代碼可前往這里查看。
22. 使用 ref 回調(diào)函數(shù)執(zhí)行諸如監(jiān)控大小變化和管理多個(gè)節(jié)點(diǎn)元素等任務(wù)。
你知道可以將函數(shù)傳遞給 ref 屬性而不是 ref 對(duì)象嗎?
它的工作原理如下:
- 當(dāng) DOM 節(jié)點(diǎn)添加到屏幕時(shí),React 會(huì)以 DOM 節(jié)點(diǎn)作為參數(shù)調(diào)用該函數(shù)。
- 當(dāng) DOM 節(jié)點(diǎn)被移除時(shí),React 會(huì)以 null 調(diào)用該函數(shù)。
在下面的示例中,我們使用此技巧跳過 useEffect。
不好的寫法: 使用 useEffect 關(guān)注輸入框焦點(diǎn)
const FocusInput = () => { const ref = useRef<HTMLInputElement>(); useEffect(() => { ref.current?.focus(); }, []); return <input ref={ref} type="text" />; };
推薦寫法: 我們?cè)谳斎肟捎脮r(shí)立即聚焦輸入。
const FocusInput = () => { const ref = useCallback((node) => node?.focus(), []); return <input ref={ref} type="text" />; };
四. 組織 react 代碼
23. 將 React 組件與其資源(例如樣式、圖像等)放在一起
始終將每個(gè) React 組件與相關(guān)資源(如樣式和圖像)放在一起。
這樣,當(dāng)不再需要組件時(shí),可以更輕松地刪除它們。
它還簡(jiǎn)化了代碼導(dǎo)航,因?yàn)槟阈枰囊磺卸技性谝粋€(gè)地方。
24. 限制組件文件大小
包含大量組件和導(dǎo)出內(nèi)容的大文件可能會(huì)令人困惑。
此外,隨著更多內(nèi)容的添加,它們往往會(huì)變得更大。
因此,請(qǐng)以合理的文件大小為目標(biāo),并在合理的情況下將組件拆分為單獨(dú)的文件。
25. 限制功能組件文件中的返回語(yǔ)句數(shù)量
功能組件中的多個(gè)返回語(yǔ)句使得很難看到組件返回的內(nèi)容。
對(duì)于我們可以搜索渲染術(shù)語(yǔ)的類組件來說,這不是問題。
一個(gè)方便的技巧是盡可能使用不帶括號(hào)的箭頭函數(shù)(VSCode 有一個(gè)針對(duì)此的操作)。
不好的寫法: 更難發(fā)現(xiàn)組件返回語(yǔ)句。
export interface UserInfo { id: string; name: string; age: number; } export interface UserListProps { users: UserInfo[]; searchUser: string; onSelectUser: (u: UserInfo) => void; } const UserList: React.FC<UserListProps> = ({ users, searchUser, onSelectUser, }) => { // 多余return語(yǔ)句 const filterUsers = users?.filter((user) => { return user.name.includes(searchUser); }); const onSelectUserHandler = (user) => { // 多余return語(yǔ)句 return () => { onSelectUser(user); }; }; return ( <> <h2>用戶列表</h2> <ul> {filterUsers.map((user, index) => { return ( <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}> <p> <span>用戶id</span> <span>{user.id}</span> </p> <p> <span>用戶名</span> <span>{user.name}</span> </p> <p> <span>用戶年齡</span> <span>{user.age}</span> </p> </li> ); })} </ul> </> ); };
推薦寫法: 組件僅有一個(gè)返回語(yǔ)句。
export interface UserInfo { id: string; name: string; age: number; } export interface UserListProps { users: UserInfo[]; searchUser: string; onSelectUser: (u: UserInfo) => void; } const UserList: React.FC<UserListProps> = ({ users, searchUser, onSelectUser, }) => { const filterUsers = users?.filter((user) => user.name.includes(searchUser)); const onSelectUserHandler = (user) => () => onSelectUser(user); return ( <> <h2>用戶列表</h2> <ul> {filterUsers.map((user, index) => ( <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}> <p> <span>用戶id</span> <span>{user.id}</span> </p> <p> <span>用戶名</span> <span>{user.name}</span> </p> <p> <span>用戶年齡</span> <span>{user.age}</span> </p> </li> ))} </ul> </> ); };
26. 優(yōu)先使用命名導(dǎo)出而不是默認(rèn)導(dǎo)出
讓我們比較一下這兩種方法:
//默認(rèn)導(dǎo)出 export default function App() { // 組件內(nèi)容 } // 命名導(dǎo)出 export function App() { // 組件內(nèi)容 }
我們現(xiàn)在就像如下這樣導(dǎo)入組件:
// 默認(rèn)導(dǎo)入 import App from "/path/to/App"; // 命名導(dǎo)入 import { App } from "/path/to/App";
默認(rèn)導(dǎo)出存在如下一些問題:
- 如果組件被重命名,編輯器將不會(huì)自動(dòng)重命名導(dǎo)出。
例如,如果將 App 重命名為 Index,我們將得到以下內(nèi)容:
// 默認(rèn)導(dǎo)入名字并未更改 import App from "/path/to/Index"; // 命名導(dǎo)入名字已更改 import { Index } from "/path/to/Index";
- 很難看出從具有默認(rèn)導(dǎo)出的文件中導(dǎo)出了什么。
例如,在命名導(dǎo)入的情況下,一旦我們輸入 import { } from "/path/to/file"
,當(dāng)我將光標(biāo)放在括號(hào)內(nèi)時(shí)就會(huì)獲得自動(dòng)完成功能。
- 默認(rèn)導(dǎo)出很難重新再導(dǎo)出。
例如,如果我想從 index 文件重新導(dǎo)出 App 組件,我必須執(zhí)行以下操作:
export { default as App } from "/path/to/App";
使用命名導(dǎo)出的解決方案更加直接。
export { App } from "/path/to/App";
因此,建議默認(rèn)使用命名導(dǎo)出。
注意:即使你使用的是 React lazy,你仍然可以使用命名導(dǎo)出。請(qǐng)參閱此處的介紹示例。
五. 高效的狀態(tài)管理
27. 永遠(yuǎn)不要為可以從其他 state 或 props 派生的值創(chuàng)建新的 state
state 越多 = 麻煩越多。
每個(gè) state 都可能觸發(fā)重新渲染,并使重置 state 變得麻煩。
因此,如果可以從 state 或 props 中派生出值,則跳過添加新的 state。
不好的做法:filteredUsers 不需要處于 state 中。
const FilterUserComponent = ({ users }) => { const [filters, setFilters] = useState([]); // 創(chuàng)建了新的state const [filteredUsers, setFilteredUsers] = useState([]); const filterUsersMethod = (filters, users) => { // 過濾邏輯方法 }; useEffect(() => { setFilteredUsers(filterUsersMethod(filters, users)); }, [users, filters]); return ( <Card> <Filters filters={filters} onChangeFilters={setFilters} /> {filteredUsers.length > 0 && <UserList users={filteredUsers} />} </Card> ); };
推薦做法: filteredUsers 由 users 和 filters 決定。
const FilterUserComponent = ({ users }) => { const [filters, setFilters] = useState([]); const filterUsersMethod = (filters, users) => { // 過濾邏輯方法 }; const filteredUsers = filterUsersMethod(filters, users); return ( <Card> <Filters filters={filters} onChangeFilters={setFilters} /> {filteredUsers.length > 0 && <UserList users={filteredUsers} />} </Card> ); };
28. 將 state 創(chuàng)建在僅需要更新的組件內(nèi)部,以減少組件的重新渲染
每當(dāng)組件內(nèi)部的狀態(tài)發(fā)生變化時(shí),React 都會(huì)重新渲染該組件及其所有子組件(包裹在 memo 中的子組件除外)。
即使這些子組件不使用已更改的狀態(tài),也會(huì)發(fā)生這種情況。為了最大限度地減少重新渲染,請(qǐng)盡可能將狀態(tài)移到組件樹的下方。
不好的做法: 當(dāng) type 發(fā)生改變時(shí),會(huì)使不依賴 type 狀態(tài)的 LeftList 和 RightList 組件也觸發(fā)重新渲染。
const App = () => { const [type, setType] = useState(""); return ( <Container> <LeftList /> <Main type={type} setType={setType} /> <RightList /> </Container> ); }; const mainBtnList = [ { label: "首頁(yè)", value: "home", }, { label: "詳情頁(yè)", value: "detail", }, ]; const Main = ({ type, setType }) => { return ( <> {mainBtnList.map((item, index) => ( <Button className={`${type.value === type ? "active" : ""}`} key={`${item.value}-${index}`} onClick={() => setType(item.value)} > {item.label} </Button> ))} </> ); };
推薦做法: 將狀態(tài)耦合到 Main 組件內(nèi)部,僅影響 Main 組件的重新渲染。
const App = () => { return ( <Container> <LeftList /> <Main /> <RightList /> </Container> ); }; const mainBtnList = [ { label: "首頁(yè)", value: "home", }, { label: "詳情頁(yè)", value: "detail", }, ]; const Main = () => { const [type, setType] = useState(""); return ( <> {mainBtnList.map((item, index) => ( <Button className={`${type.value === type ? "active" : ""}`} key={`${item.value}-${index}`} onClick={() => setType(item.value)} > {item.label} </Button> ))} </> ); };
29. 定義需要明確初始狀態(tài)和當(dāng)前狀態(tài)的區(qū)別
不好的做法: 不清楚 userInfo 只是初始值,這可能會(huì)導(dǎo)致狀態(tài)管理的混亂或錯(cuò)誤。
const UserInfo = ({ userInfo }) => { const [userInfo, setUserInfo] = useState(userInfo); return ( <Card> <Title>當(dāng)前用戶: {userInfo?.name}</Title> <UserInfoDetail detail={userInfo?.detail} /> </Card> ); };
推薦做法: 命名可以清楚地表明什么是初始狀態(tài),什么是當(dāng)前狀態(tài)。
const UserInfo = ({ initialUserInfo }) => { const [userInfo, setUserInfo] = useState(initialUserInfo); return ( <Card> <Title>當(dāng)前用戶: {userInfo?.name}</Title> <UserInfoDetail detail={userInfo?.detail} /> </Card> ); };
30. 根據(jù)之前的狀態(tài)更新狀態(tài),尤其是在使用 useCallback 進(jìn)行緩存時(shí)
React 允許你將更新函數(shù)從 useState 傳遞給 set 函數(shù)。
此更新函數(shù)使用當(dāng)前狀態(tài)來計(jì)算下一個(gè)狀態(tài)。
每當(dāng)需要根據(jù)之前狀態(tài)更新狀態(tài)時(shí),都可以使用此行為,尤其是在使用 useCallback 包裝的函數(shù)內(nèi)部。事實(shí)上,這種方法可以避免將狀態(tài)作為鉤子依賴項(xiàng)之一。
不好的做法: 無(wú)論什么時(shí)候,當(dāng) todoList 變化的時(shí)候,onHandleAddTodo 和 onHandleRemoveTodo 都會(huì)跟著改變。
const App = () => { const [todoList, setTodoList] = useState([]); const onHandleAddTodo = useCallback( (todo) => { setTodoList([...todoList, todo]); }, [todoList] ); const onHandleRemoveTodo = useCallback( (todo) => { setTodoList([...todoList].filter((item) => item.id !== todo.id)); }, [todoList] ); return ( <div className="App"> <TodoInput onAddTodo={onHandleAddTodo} /> <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} /> </div> ); };
推薦做法: 即使 todoList 發(fā)生變化,onHandleAddTodo 和 onHandleRemoveTodo 仍然保持不變。
const App = () => { const [todoList, setTodoList] = useState([]); const onHandleAddTodo = useCallback((todo) => { setTodoList((prevTodoList) => [...prevTodoList, todo]); }, []); const onHandleRemoveTodo = useCallback((todo) => { setTodoList((prevTodoList) => [...prevTodoList].filter((item) => item.id !== todo.id) ); }, []); return ( <div className="App"> <TodoInput onAddTodo={onHandleAddTodo} /> <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} /> </div> ); };
31. 使用 useState 中的函數(shù)進(jìn)行延遲初始化并提高性能,因?yàn)樗鼈冎槐徽{(diào)用一次。
在 useState 中使用函數(shù)可確保初始狀態(tài)僅計(jì)算一次。
這可以提高性能,尤其是當(dāng)初始狀態(tài)來自“昂貴”操作(例如從本地存儲(chǔ)讀?。r(shí)。
不好的做法:每次組件渲染時(shí),我們都會(huì)從本地存儲(chǔ)讀取主題。
const THEME_LOCAL_STORAGE_KEY = "page_theme_key"; const Theme = ({ theme, onChangeTheme }) => { // .... }; const App = ({ children }) => { const [theme, setTheme] = useState( localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark" ); const onChangeTheme = (theme: string) => { setTheme(theme); localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme); }; return ( <div className={`app${theme ? ` ${theme}` : ""}`}> <Theme onChange={onChangeTheme} theme={theme} /> <div>{children}</div> </div> ); };
推薦做法: 當(dāng)組件掛載時(shí),我們僅只會(huì)讀取本地存儲(chǔ)一次。
// ... const App = ({ children }) => { const [theme, setTheme] = useState( () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark" ); const onChangeTheme = (theme: string) => { setTheme(theme); localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme); }; return ( <div className={`app${theme ? ` ${theme}` : ""}`}> <Theme onChange={onChangeTheme} theme={theme} /> <div>{children}</div> </div> ); };
32. 使用 React 上下文來處理廣泛需要的靜態(tài)狀態(tài),以防止 prop 鉆取
每當(dāng)我有一些數(shù)據(jù)時(shí),我都會(huì)使用 React 上下文:
- 在多個(gè)地方都需要(例如,主題、當(dāng)前用戶等)
- 主要是靜態(tài)或只讀的(即,用戶不能/不會(huì)經(jīng)常更改數(shù)據(jù))
- 這種方法有助于避免 prop 鉆取(即,通過組件層次結(jié)構(gòu)的多個(gè)層傳遞數(shù)據(jù)或狀態(tài))。
來看一個(gè)示例的部分代碼:
context.ts
// UserInfo接口來自測(cè)試數(shù)據(jù) export const userInfoContext = createContext<string | UserInfoData>("loading"); export const useUserInfo = <T extends UserInfoData>() => { const value = useContext(userInfoContext); if (value == null) { throw new Error("Make sure to wrap the userInfoContext inside provider"); } return value as T; };
App.tsx
function App() { const [userInfoData, setUserInfoData] = useState<UserInfoData | string>( "loading" ); useEffect(() => { getCurrentUser().then(setUserInfoData); }, []); if (userInfoData === "loading") { return <Loading />; } return ( <div className="app"> <userInfoContext.Provider value={userInfoData}> <Header /> <Sidebar /> <Main /> </userInfoContext.Provider> </div> ); }
header.tsx:
const Header: React.FC<HeaderProps> = (props) => { // 使用context const userInfo = useUserInfo(); return ( <header className="header" {...props}> 歡迎回來{userInfo?.name} </header> ); };
main.tsx:
const Main: React.FC<MainProps> = ({ title }) => { const { posts } = useUserInfo(); return ( <div className="main"> <h2 className="title">{title}</h2> <ul className="list"> {posts?.map((post, index) => ( <li className="list-item" key={`${post.id}-${index}`}> {post.title} </li> ))} </ul> </div> ); };
33. React Context:將 react 上下文分為經(jīng)常變化的部分和不經(jīng)常變化的部分,以提高應(yīng)用程序性能
React 上下文的一個(gè)挑戰(zhàn)是,只要上下文數(shù)據(jù)發(fā)生變化,所有使用該上下文的組件都會(huì)重新渲染,即使它們不使用發(fā)生變化的上下文部分。
解決方案是什么?使用單獨(dú)的上下文。
在下面的示例中,我們創(chuàng)建了兩個(gè)上下文:一個(gè)用于操作(常量),另一個(gè)用于狀態(tài)(可以更改)。
export interface TodosInfoItem { id?: string; title?: string; completed?: boolean; } export interface TodosInfo { search?: string; todos: TodosInfoItem[]; } export const todosStateContext = createContext<TodosInfo>(void 0); export const todosActionContext = createContext<Dispatch<ReducerActionParams>>( void 0 ); export interface ReducerActionParams extends TodosInfoItem { type?: string; value?: string; } export const getTodosReducer = ( state: TodosInfo, action: ReducerActionParams ) => { switch (action.type) { case TodosActionType.ADD_TODO: return { ...state, todos: [ ...state.todos, { id: crypto.randomUUID(), title: action.title, completed: false, }, ], }; case TodosActionType.REMOVE_TODO: return { ...state, todos: [...state.todos].filter((item) => item.id !== action.id), }; case TodosActionType.TOGGLE_TODO_STATUS: return { ...state, todos: [...state.todos].map((item) => item.id === action.id ? { ...item, completed: !item.completed } : item ), }; case TodosActionType.SET_SEARCH_TERM: return { ...state, search: action.value, }; default: return state; } };
完整示例代碼前往這里查看。
34. React Context:當(dāng)值計(jì)算不直接時(shí),引入 Provider 組件
不好的做法:App 內(nèi)部有太多邏輯來管理 theme context。
const THEME_LOCAL_STORAGE_KEY = "current-project-theme"; const DEFAULT_THEME = "light"; const ThemeContext = createContext({ theme: DEFAULT_THEME, setTheme: () => null, }); const App = () => { const [theme, setTheme] = useState( () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME ); useEffect(() => { if (theme !== "system") { updateRootElementTheme(theme); return; } // 我們需要根據(jù)系統(tǒng)主題獲取要應(yīng)用的主題類 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") .matches ? "dark" : "light"; updateRootElementTheme(systemTheme); // 然后觀察系統(tǒng)主題的變化并相應(yīng)地更新根元素 const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); const listener = (event) => { updateRootElementTheme(event.matches ? "dark" : "light"); }; darkThemeMq.addEventListener("change", listener); return () => darkThemeMq.removeEventListener("change", listener); }, [theme]); const themeContextValue = { theme, setTheme: (theme) => { localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme); setTheme(theme); }, }; const [selectedUserId, setSelectedUserId] = useState(undefined); const onUserSelect = (id) => { // 待做:一些邏輯 setSelectedUserId(id); }; const users = useSWR("/api/users", fetcher); return ( <div className="App"> <ThemeContext.Provider value={themeContextValue}> <UserList users={users} onUserSelect={onUserSelect} selectedUserId={selectedUserId} /> </ThemeContext.Provider> </div> ); };
推薦:主題 context 相關(guān)的邏輯封裝在 ThemeProvider 中。
const THEME_LOCAL_STORAGE_KEY = "current-project-theme"; const DEFAULT_THEME = "light"; const ThemeContext = createContext({ theme: DEFAULT_THEME, setTheme: () => null, }); const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState( () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME ); useEffect(() => { if (theme !== "system") { updateRootElementTheme(theme); return; } // 我們需要根據(jù)系統(tǒng)主題獲取要應(yīng)用的主題類 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") .matches ? "dark" : "light"; updateRootElementTheme(systemTheme); // 然后觀察系統(tǒng)主題的變化并相應(yīng)地更新根元素 const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); const listener = (event) => { updateRootElementTheme(event.matches ? "dark" : "light"); }; darkThemeMq.addEventListener("change", listener); return () => darkThemeMq.removeEventListener("change", listener); }, [theme]); const themeContextValue = { theme, setTheme: (theme) => { localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme); setTheme(theme); }, }; return ( <div className="App"> <ThemeContext.Provider value={themeContextValue}> {children} </ThemeContext.Provider> </div> ); }; const App = () => { const [selectedUserId, setSelectedUserId] = useState(undefined); const onUserSelect = (id) => { // 待做:一些邏輯 setSelectedUserId(id); }; const users = useSWR("/api/users", fetcher); return ( <div className="App"> <ThemeProvider> <UserList users={users} onUserSelect={onUserSelect} selectedUserId={selectedUserId} /> </ThemeProvider> </div> ); };
35. 考慮使用 useReducer hook 作為輕量級(jí)狀態(tài)管理解決方案
每當(dāng)我的狀態(tài)或復(fù)雜狀態(tài)中的值太多并且不想依賴外部庫(kù)時(shí),我都會(huì)使用 useReducer。
當(dāng)與上下文結(jié)合使用時(shí),它對(duì)于更廣泛的狀態(tài)管理需求特別有效。
示例:這里。
36. 使用 useImmer 或 useImmerReducer 簡(jiǎn)化狀態(tài)更新
使用 useState 和 useReducer 等鉤子時(shí),狀態(tài)必須是不可變的(即,所有更改都需要?jiǎng)?chuàng)建新狀態(tài),而不是修改當(dāng)前狀態(tài))。
這通常很難實(shí)現(xiàn)。
這就是 useImmer 和 useImmerReducer 提供更簡(jiǎn)單的替代方案的地方。它們?cè)试S你編寫自動(dòng)轉(zhuǎn)換為不可變更新的“可變”代碼。
不好的做法: 我們必須小心確保我們正在創(chuàng)建一個(gè)新的狀態(tài)對(duì)象。
export const App = () => { const [{ email, password }, setState] = useState({ email: "", password: "", }); const onEmailChange = (event) => { setState((prevState) => ({ ...prevState, email: event.target.value })); }; const onPasswordChange = (event) => { setState((prevState) => ({ ...prevState, password: event.target.value })); }; return ( <div className="App"> <h1>歡迎登陸</h1> <div class="form-item"> <label>郵箱號(hào): </label> <input type="email" value={email} onChange={onEmailChange} /> </div> <div className="form-item"> <label>密碼:</label> <input type="password" value={password} onChange={onPasswordChange} /> </div> </div> ); };
推薦做法: 更直接一點(diǎn),我們可以直接修改 draftState。
import { useImmer } from "use-immer"; export const App = () => { const [{ email, password }, setState] = useImmer({ email: "", password: "", }); const onEmailChange = (event) => { setState((draftState) => { draftState.email = event.target.value; }); }; const onPasswordChange = (event) => { setState((draftState) => { draftState.password = event.target.value; }); }; // 剩余代碼 };
37. 使用 Redux(或其他狀態(tài)管理解決方案)來跨多個(gè)組件訪問復(fù)雜的客戶端狀態(tài)
每當(dāng)出現(xiàn)以下情況時(shí),我都會(huì)求助于 Redux:
我有一個(gè)復(fù)雜的 FE 應(yīng)用程序,其中包含大量共享的客戶端狀態(tài)(例如,儀表板應(yīng)用程序)
- 我希望用戶能夠回到過去并恢復(fù)更改。
- 我不希望我的組件像使用 React 上下文那樣不必要地重新渲染。
- 我有太多開始難以控制的上下文。
為了獲得簡(jiǎn)化的體驗(yàn),我建議使用 redux-tooltkit。
?? 注意:你還可以考慮 Redux 的其他替代方案,例如 Zustand 或 Recoil。
38. Redux:使用 Redux DevTools 調(diào)試你的狀態(tài)
Redux DevTools 瀏覽器擴(kuò)展是調(diào)試 Redux 項(xiàng)目的有用工具。
它允許你實(shí)時(shí)可視化你的狀態(tài)和操作,在刷新時(shí)保持狀態(tài)持久性等等。
要了解它的用途,請(qǐng)觀看這個(gè)精彩的視頻。
六. React 代碼優(yōu)化
39. 使用 memo 防止不必要的重新渲染
當(dāng)處理渲染成本高昂且父組件頻繁更新的組件時(shí),將它們包裝在 memo 中可能會(huì)改變渲染規(guī)則。
memo 確保組件僅在其 props 發(fā)生變化時(shí)重新渲染,而不僅僅是因?yàn)槠涓附M件重新渲染。
在以下示例中,我通過 useGetInfoData 從服務(wù)器獲取一些數(shù)據(jù)。如果數(shù)據(jù)沒有變化,將 UserInfoList 包裝在 memo 中將阻止它在數(shù)據(jù)的其他部分更新時(shí)重新渲染。
export const App = () => { const { currentUserInfo, users } = useGetInfoData(); return ( <div className="App"> <h1>信息面板</h1> <CurrentUserInfo data={currentUserInfo} /> <UserInfoList users={users} /> </div> ); }; const UserInfoList = memo(({ users }) => { // 剩余實(shí)現(xiàn) });
一旦 React 編譯器變得穩(wěn)定,這個(gè)小技巧可能就不再有用了。
40. 用 memo 指定一個(gè)相等函數(shù)來指示 React 如何比較 props。
默認(rèn)情況下,memo 使用Object.is將每個(gè) prop 與其先前的值進(jìn)行比較。
但是,對(duì)于更復(fù)雜或特定的場(chǎng)景,指定自定義相等函數(shù)可能比默認(rèn)比較或重新渲染更有效。
示例如下:
const UserList = memo( ({ users }) => { return <div>{JSON.stringify(users)}</div>; }, (prevProps, nextProps) => { // 僅當(dāng)最后一個(gè)用戶或列表大小發(fā)生變化時(shí)才重新渲染 const prevLastUser = prevProps.users[prevProps.users.length - 1]; const nextLastUser = nextProps.users[nextProps.users.length - 1]; return ( prevLastUser.id === nextLastUser.id && prevProps.users.length === nextProps.users.length ); } );
41.聲明緩存組件時(shí),優(yōu)先使用命名函數(shù)而不是箭頭函數(shù)
定義緩存組件時(shí),使用命名函數(shù)而不是箭頭函數(shù)可以提高 React DevTools 中的清晰度。
箭頭函數(shù)通常會(huì)導(dǎo)致像 _c2
這樣的通用名稱,這會(huì)使調(diào)試和分析更加困難。
不好的做法:對(duì)緩存組件使用箭頭函數(shù)會(huì)導(dǎo)致 React DevTools 中的名稱信息量較少。
const UserInfoList = memo(({ users }) => { // 剩余實(shí)現(xiàn)邏輯 });
推薦做法: 該組件的名稱將在 DevTools 中可見。
const UserInfoList = memo(function UserInfoList({ users }) { // 剩余實(shí)現(xiàn)邏輯 });
42. 使用 useMemo 緩存昂貴的計(jì)算或保留引用
我通常會(huì)使用 useMemo:
- 當(dāng)我有昂貴的計(jì)算,不應(yīng)該在每次渲染時(shí)重復(fù)這些計(jì)算時(shí)。
- 如果計(jì)算值是非原始值,用作 useEffect 等鉤子中的依賴項(xiàng)。
- 計(jì)算出的非原始值將作為 prop 傳遞給包裹在 memo 中的組件;否則,這將破壞緩存,因?yàn)?React 使用 Object.is 來檢測(cè) props 是否發(fā)生變化。
不好的做法:UserInfoList 的 memo 不會(huì)阻止重新渲染,因?yàn)槊看武秩緯r(shí)都會(huì)重新創(chuàng)建樣式。
export const UserInfo = () => { const { profileInfo, users, baseStyles } = useGetUserInfoData(); // 每次重新渲染我們都會(huì)得到一個(gè)樣式對(duì)象 const styles = { ...baseStyles, margin: 10 }; return ( <div className="App"> <h1>用戶頁(yè)</h1> <Profile data={profileInfo} /> <UserInfoList users={users} styles={styles} /> </div> ); }; const UserInfoList = memo(function UserInfoListFn({ users, styles }) { /// 剩余實(shí)現(xiàn) });
推薦做法: useMemo 的使用確保只有當(dāng) baseStyles 發(fā)生變化時(shí),styles 才會(huì)發(fā)生變化,從而使 memo 能夠有效防止不必要的重新渲染。
export const UserInfo = () => { const { profileInfo, users, baseStyles } = useGetUserInfoData(); // 每次重新渲染我們都會(huì)得到一個(gè)樣式對(duì)象 const styles = useMemo(() => ({ ...baseStyles, margin: 10 }), [baseStyles]); return ( <div className="App"> <h1>用戶頁(yè)</h1> <Profile data={profileInfo} /> <UserInfoList users={users} styles={styles} /> </div> ); }; const UserInfoList = memo(function UserInfoListFn({ users, styles }) { /// 剩余實(shí)現(xiàn) });
43. 使用 useCallback 緩存函數(shù)
useCallback 與 useMemo 類似,但專為緩存函數(shù)而設(shè)計(jì)。
不好的做法:每當(dāng) theme 發(fā)生變化時(shí),handleThemeChange 都會(huì)被調(diào)用兩次,并且我們會(huì)將日志推送到服務(wù)器兩次。
const useTheme = () => { const [theme, setTheme] = useState("light"); // 每次渲染`handleThemeChange`都會(huì)改變 // 因此,每次渲染后都會(huì)觸發(fā)該效果 const handleThemeChange = (newTheme) => { sendLog(["Theme changed"], { context: { theme: newTheme, }, }); setTheme(newTheme); }; useEffect(() => { const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); handleThemeChange(dqMediaQuery.matches ? "dark" : "light"); const listener = (event) => { handleThemeChange(event.matches ? "dark" : "light"); }; dqMediaQuery.addEventListener("change", listener); return () => { dqMediaQuery.removeEventListener("change", listener); }; }, [handleThemeChange]); return theme; };
推薦做法:將 handleThemeChange 包裝在 useCallback 中可確保僅在必要時(shí)重新創(chuàng)建它,從而減少不必要的執(zhí)行。
const handleThemeChange = useCallback((newTheme) => { sendLog(["Theme changed"], { context: { theme: newTheme, }, }); setTheme(newTheme); }, []);
44. 緩存回調(diào)函數(shù)或使用程序鉤子返回的值以避免性能問題
當(dāng)你創(chuàng)建自定義鉤子與他人共享時(shí),記住返回的值和函數(shù)至關(guān)重要。
這種做法可以使你的鉤子更高效,并防止任何使用它的人出現(xiàn)不必要的性能問題。
不好的做法:loadData 沒有被緩存并產(chǎn)生了性能問題。
const useLoadData = (fetchData) => { const [result, setResult] = useState({ type: "pending", }); const loadData = async () => { setResult({ type: "loading" }); try { const data = await fetchData(); setResult({ type: "loaded", data }); } catch (err) { setResult({ type: "error", error: err }); } }; return { result, loadData }; };
推薦做法: 我們緩存所有內(nèi)容,因此不會(huì)出現(xiàn)意外的性能問題。
const useLoadData = (fetchData) => { const [result, setResult] = useState({ type: "pending", }); // 包裹在 `useRef` 中并使用 `ref` 值,這樣函數(shù)就不會(huì)改變 const fetchDataRef = useRef(fetchData); useEffect(() => { fetchDataRef.current = fetchData; }, [fetchData]); // 包裹在 `useCallback` 中并使用 `ref` 值,這樣函數(shù)就不會(huì)改變 const loadData = useCallback(async () => { setResult({ type: "loading" }); try { const data = await fetchDataRef.current(); setResult({ type: "loaded", data }); } catch (err) { setResult({ type: "error", error: err }); } }, []); // 使用useMemo緩存值 return useMemo(() => ({ result, loadData }), [result, loadData]); };
45. 利用懶加載和 Suspense 讓你的應(yīng)用加載更快
構(gòu)建應(yīng)用時(shí),請(qǐng)考慮對(duì)以下代碼使用懶加載和 Suspense:
- 加載成本高。
- 僅與某些用戶相關(guān)(如高級(jí)功能)。
- 對(duì)于初始用戶交互而言并非立即需要。
在下面的示例,Slider 資源(JS + CSS)僅在你單擊卡片后加載。
//... const LazyLoadedSlider = lazy(() => import("./Slider")); //... const App = () => { // .... return ( <div className="container"> {/* .... */} {selectedUser != null && ( <Suspense fallback={<div>Loading...</div>}> <LazyLoadedSlider avatar={selectedUser.avatar} name={selectedUser.name} address={selectedUser.address} onClose={closeSlider} /> </Suspense> )} </div> ); };
46. 限制網(wǎng)絡(luò)以模擬慢速網(wǎng)絡(luò)
你知道可以直接在 Chrome 中模擬慢速互聯(lián)網(wǎng)連接嗎?
這在以下情況下尤其有用:
- 用戶報(bào)告加載時(shí)間緩慢,而你無(wú)法在更快的網(wǎng)絡(luò)上復(fù)制。
- 你正在實(shí)施懶加載,并希望觀察文件在較慢條件下的加載方式,以確保適當(dāng)?shù)募虞d狀態(tài)。
47. 使用 react-window 或 react-virtuoso 高效渲染列表
切勿一次性渲染一長(zhǎng)串項(xiàng)目,例如聊天消息、日志或無(wú)限列表。
這樣做可能會(huì)導(dǎo)致瀏覽器卡死崩潰。相反,可以使用虛擬化列表,這意味著僅渲染可能對(duì)用戶可見的項(xiàng)目子集。
react-window、react-virtuoso 或 @tanstack/react-virtual 等庫(kù)就是為此目的而設(shè)計(jì)的。
不好的做法:NonVirtualList 會(huì)同時(shí)呈現(xiàn)所有 50,000 條日志行,即使它們不可見。
const NonVirtualList = ({ items }: { items: LogLineItem[] }) => { return ( <div style={{ height: "100%" }}> {items?.map((log, index) => ( <div key={log.id} style={{ padding: "5px", borderBottom: index === items.length - 1 ? "none" : "1px solid #535455", }} > <LogLine log={log} index={index} /> </div> ))} </div> ); };
推薦做法: VirtualList
僅渲染可能可見的項(xiàng)目。
const VirtualList = ({ items }: { items: LogLineItem[] }) => { return ( <Virtuoso style={{ height: "100%" }} data={items} itemContent={(index, log) => ( <div key={log.id} style={{ padding: "5px", borderBottom: index === items.length - 1 ? "none" : "1px solid #535455", }} > <LogLine log={log} index={index} /> </div> )} /> ); };
你可以在這個(gè)完整的示例中在兩個(gè)選項(xiàng)之間切換,并注意使用 NonVirtualList
時(shí)應(yīng)用程序的性能有多糟糕。
七. 總結(jié)
到此這篇關(guān)于React組件、狀態(tài)管理、代碼優(yōu)化的技巧的文章就介紹到這了,更多相關(guān)React實(shí)踐小技巧內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React Hooks獲取數(shù)據(jù)實(shí)現(xiàn)方法介紹
這篇文章主要介紹了react hooks獲取數(shù)據(jù),文中給大家介紹了useState dispatch函數(shù)如何與其使用的Function Component進(jìn)行綁定,實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10詳解使用React全家桶搭建一個(gè)后臺(tái)管理系統(tǒng)
本篇文章主要介紹了使用React全家桶搭建一個(gè)后臺(tái)管理系統(tǒng),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11React-Native中禁用Navigator手勢(shì)返回的示例代碼
本篇文章主要介紹了React-Native中禁用Navigator手勢(shì)返回的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-09-09useEffect中return函數(shù)的作用和執(zhí)行時(shí)機(jī)解讀
這篇文章主要介紹了useEffect中return函數(shù)的作用和執(zhí)行時(shí)機(jī),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01react?事項(xiàng)懶加載的三種方法及使用場(chǎng)景
這篇文章主要介紹了react?事項(xiàng)懶加載的三種方法及使用場(chǎng)景,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07JavaScript的React Web庫(kù)的理念剖析及基礎(chǔ)上手指南
這篇文章主要介紹了JavaScript的React Web庫(kù)的理念剖析及基礎(chǔ)上手指南,React Web的目的即是把本地的React Native應(yīng)用程序項(xiàng)目變?yōu)閃eb應(yīng)用程序,需要的朋友可以參考下2016-05-05