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 元素來(lái)分組元素
在 React 中,每個(gè)組件必須返回單個(gè)元素。不要將多個(gè)元素包裝在 <div> 或 <span> 中,而是使用 <Fragment> 來(lái)保持 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 來(lái)使用。
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)來(lái)進(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 屬性來(lái)獲得更清晰的代碼(以及性能優(yōu)勢(shì))
利用子組件 props 來(lái)獲得更簡(jiǎn)潔的代碼(和性能優(yōu)勢(shì))。使用子組件 props 有幾個(gè)好處:
- 好處 1:你可以通過將 props 直接傳遞給子組件而不是通過父組件路由來(lái)避免 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ò)誤邊界來(lái):
- 即使發(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 屬性來(lái)觸發(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ǔ)的類組件來(lái)說,這不是問題。
一個(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)來(lá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)來(lá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 上下文來(lái)處理廣泛需要的靜態(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))。
來(lái)看一個(gè)示例的部分代碼:
context.ts
// UserInfo接口來(lái)自測(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}>
歡迎回來(lái){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)部有太多邏輯來(lá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)管理解決方案)來(lá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ù)來(lái)指示 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 來(lái)檢測(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è)參考。一起跟隨小編過來(lái)看看吧2017-11-11
React-Native中禁用Navigator手勢(shì)返回的示例代碼
本篇文章主要介紹了React-Native中禁用Navigator手勢(shì)返回的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-09-09
useEffect中return函數(shù)的作用和執(zhí)行時(shí)機(jī)解讀
這篇文章主要介紹了useEffect中return函數(shù)的作用和執(zhí)行時(shí)機(jī),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
react?事項(xiàng)懶加載的三種方法及使用場(chǎng)景
這篇文章主要介紹了react?事項(xiàng)懶加載的三種方法及使用場(chǎng)景,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
JavaScript的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

