在過去的5 年裡,我一直在專業地使用 React。
在這篇文章中,我分享了我多年來學到的 101 個最佳提示和技巧。
準備好?讓我們潛入吧💪!
ℹ️注意事項:
本指南假設您基本上熟悉 React 並理解術語
props
、state
、context
等。為了讓事情變得簡單,我嘗試在大多數範例中使用 Vanilla JS。如果您使用 TypeScript,則可以輕鬆調整程式碼。
該程式碼尚未準備好用於生產。請自行決定使用。
7. 使用value && <Component {...props}/>
之前確保value
是布林值,以防止在螢幕上顯示意外的值。
62. 在鉤子(例如useEffect
中偏好命名函數而不是箭頭函數,以便在 React Dev Tools 中輕鬆找到它們
82. 使用ReactNode
取代JSX.Element | null | undefined | ...
讓您的程式碼更加緊湊
84. 使用ComponentProps
、 ComponentPropsWithoutRef
等有效存取元素 props
95. 使用 Sentry 或 Grafana Cloud Frontend Observability 等工具記錄和監控您的應用程式。
100. 透過訂閱像 This Week In React 或 ui.dev 這樣的時事通訊,了解 React 生態系統的最新動態
{% 結束詳細資料 %}
// ❌ Bad: too verbose
<MyComponent></MyComponent>
// ✅ Good
<MyComponent/>
fragments
而不是 DOM 節點(例如 div、span 等)來對元素進行分組在 React 中,每個元件必須傳回一個元素。不要將多個元素包裝在<div>
或<span>
中,而是使用<Fragment>
來保持DOM整潔。
❌ 不好:使用div
會使您的 DOM 變得混亂,並且可能需要更多 CSS 程式碼。
function Dashboard() {
return (
<div>
<Header />
<Main />
</div>
);
}
✅ 好: <Fragment>
包裹元素而不影響 DOM 結構。
function Dashboard() {
return (
<Fragment>
<Header />
<Main />
</Fragment>
);
}
<></>
(除非你需要設定一個key)❌ 不好:下面的程式碼不必要地冗長。
<Fragment>
<FirstChild />
<SecondChild />
</Fragment>
✅ 好:除非,你需要一個key
, <></>
更簡潔。
<>
<FirstChild />
<SecondChild />
</>
// Using a `Fragment` here is required because of the key.
function List({ users }) {
return (
<div>
{users.map((user) => (
<Fragment key={user.id}>
<span>{user.name}</span>
<span>{user.occupation}</span>
</Fragment>
))}
</div>
);
}
❌ 不好:下面的程式碼更難閱讀(尤其是大規模時)。
// We do `props…` all over the code.
function TodoList(props) {
return (
<div>
{props.todos.map((todo) => (
<div key={todo}>
<button
onClick={() => props.onSelectTodo(todo)}
style={{
backgroundColor: todo === props.selectedTodo ? "gold" : undefined,
}}
>
<span>{todo}</span>
</button>
</div>
))}
</div>
);
}
✅ 好:下面的程式碼更簡潔。
function TodoList({ todos, selectedTodo, onSelectTodo }) {
return (
<div>
{todos.map((todo) => (
<div key={todo}>
<button
onClick={() => onSelectTodo(todo)}
style={{
backgroundColor: todo === selectedTodo ? "gold" : undefined,
}}
>
<span>{todo}</span>
</button>
</div>
))}
</div>
);
}
❌ 不好:您可能需要在多個位置定義預設值並引入新變數。
function Button({ onClick, text, small, colorScheme }) {
let scheme = colorScheme || "light";
let isSmall = small || false;
return (
<button
onClick={onClick}
style={{
color: scheme === "dark" ? "white" : "black",
fontSize: isSmall ? "12px" : "16px",
}}
>
{text ?? "Click here"}
</button>
);
}
✅ 好:您可以在頂部的一處設定所有預設值。這使得其他人很容易找到他們。
function Button({
onClick,
text = "Click here",
small = false,
colorScheme = "light",
}) {
return (
<button
onClick={onClick}
style={{
color: colorScheme === "dark" ? "white" : "black",
fontSize: small ? "12px" : "16px",
}}
>
{text}
</button>
);
}
string
型別 props 時去掉花括號。// ❌ Bad: curly braces are not needed
<Button text={"Click me"} colorScheme={"dark"} />
// ✅ Good
<Button text="Click me" colorScheme="dark" />
value && <Component {...props}/>
之前確保value
是布林值,以防止在螢幕上顯示意外的值。❌ 不好:當清單為空時,螢幕上會印出0
。
export function ListWrapper({ items, selectedItem, setSelectedItem }) {
return (
<div className="list">
{items.length && ( // `0` if the list is empty
<List
items={items}
onSelectItem={setSelectedItem}
selectedItem={selectedItem}
/>
)}
</div>
);
}
✅好:沒有物品時,螢幕上不會列印任何內容。
export function ListWrapper({ items, selectedItem, setSelectedItem }) {
return (
<div className="list">
{items.length > 0 && (
<List
items={items}
onSelectItem={setSelectedItem}
selectedItem={selectedItem}
/>
)}
</div>
);
}
❌壞:變數gradeSum
和gradeCount
擾亂了元件的範圍。
function Grade() {
let gradeSum = 0;
let gradeCount = 0;
grades.forEach((grade) => {
gradeCount++;
gradeSum += grade;
});
const averageGrade = gradeSum / gradeCount;
return <>{averageGrade}</>;
}
✅好:變數gradeSum
和gradeCount
範圍在IIFE 內。
function Grade() {
const averageGrade = (() => {
let gradeSum = 0;
let gradeCount = 0;
grades.forEach((grade) => {
gradeCount++;
gradeSum += grade;
});
return gradeSum / gradeCount;
})();
return <>{averageGrade}</>;
}
💡 注意:你也可以在元件外部定義一個函數
computeAverageGrade
,並在元件內部呼叫它。
❌ 不好:更新欄位的邏輯非常重複。
function Form() {
const [{ name, email }, setFormState] = useState({
name: "",
email: "",
});
return (
<>
<h1>Class Registration Form</h1>
<form>
<label>
Name:{" "}
<input
type="text"
value={name}
onChange={(evt) =>
setFormState((formState) => ({
...formState,
name: evt.target.value,
}))
}
/>
</label>
<label>
Email:{" "}
<input
type="email"
value={email}
onChange={(evt) =>
setFormState((formState) => ({
...formState,
email: evt.target.value,
}))
}
/>
</label>
</form>
</>
);
}
✅ 好:引入createFormValueChangeHandler
,為每個欄位傳回正確的處理程序。
注意:如果您開啟了 ESLint 規則jsx-no-bind,這個技巧尤其有用。您可以將柯里化函數包裝在
useCallback
和“Voilà!”中。
function Form() {
const [{ name, email }, setFormState] = useState({
name: "",
email: "",
});
const createFormValueChangeHandler = (field) => {
return (event) => {
setFormState((formState) => ({
...formState,
[field]: event.target.value,
}));
};
};
return (
<>
<h1>Class Registration Form</h1>
<form>
<label>
Name:{" "}
<input
type="text"
value={name}
onChange={createFormValueChangeHandler("name")}
/>
</label>
<label>
Email:{" "}
<input
type="email"
value={email}
onChange={createFormValueChangeHandler("email")}
/>
</label>
</form>
</>
);
}
❌ 不好: OPTIONS
和renderOption
不需要位於元件內部,因為它們不依賴任何 props 或 state。
此外,將它們保留在內部意味著每次元件渲染時我們都會獲得新的物件參考。如果我們將renderOption
傳遞給包裹在memo
中的子元件,它會破壞記憶。
function CoursesSelector() {
const OPTIONS = ["Maths", "Literature", "History"];
const renderOption = (option: string) => {
return <option>{option}</option>;
};
return (
<select>
{OPTIONS.map((opt) => (
<Fragment key={opt}>{renderOption(opt)}</Fragment>
))}
</select>
);
}
✅ 好:將它們移出元件以保持元件清潔和參考穩定。
const OPTIONS = ["Maths", "Literature", "History"];
const renderOption = (option: string) => {
return <option>{option}</option>;
};
function CoursesSelector() {
return (
<select>
{OPTIONS.map((opt) => (
<Fragment key={opt}>{renderOption(opt)}</Fragment>
))}
</select>
);
}
💡 注意:在這個例子中,您可以透過使用內聯選項元素來進一步簡化。
const OPTIONS = ["Maths", "Literature", "History"];
function CoursesSelector() {
return (
<select>
{OPTIONS.map((opt) => (
<option key={opt}>{opt}</option>
))}
</select>
);
}
❌ 不好:如果選擇了一個專案,但隨後它發生了變化(即,我們收到相同 ID 的全新物件參考),或者如果該專案不再出現在列表中,則selectedItem
將保留過時的值或變更得不正確。
function ListWrapper({ items }) {
// We are referencing the entire item
const [selectedItem, setSelectedItem] = useState<Item | undefined>();
return (
<>
{selectedItem != null && <div>{selectedItem.name}</div>}
<List
items={items}
selectedItem={selectedItem}
onSelectItem={setSelectedItem}
/>
</>
);
}
✅ 好:我們透過 ID 儲存所選專案(應該是穩定的)。這可以確保即使該專案從清單中刪除或其屬性之一發生更改,UI 也應該是正確的。
function ListWrapper({ items }) {
const [selectedItemId, setSelectedItemId] = useState<number | undefined>();
// We derive the selected item from the list
const selectedItem = items.find((item) => item.id === selectedItemId);
return (
<>
{selectedItem != null && <div>{selectedItem.name}</div>}
<List
items={items}
selectedItemId={selectedItemId}
onSelectItem={setSelectedItemId}
/>
</>
);
}
❌ 不好:因為所有user == null
檢查,程式碼變得混亂。
在這裡,由於hooks 的規則,我們不能提前返回。
function Posts({ user }) {
// Due to the rules of hooks, `posts` and `handlePostSelect` must be declared before the `if` statement.
const posts = useMemo(() => {
if (user == null) {
return [];
}
return getUserPosts(user.id);
}, [user]);
const handlePostSelect = useCallback(
(postId) => {
if (user == null) {
return;
}
// TODO: Do something
},
[user]
);
if (user == null) {
return null;
}
return (
<div>
{posts.map((post) => (
<button key={post.id} onClick={() => handlePostSelect(post.id)}>
{post.title}
</button>
))}
</div>
);
}
✅ 好:我們引入了一個新元件UserPosts
,它接受定義的使用者並且更加乾淨。
function Posts({ user }) {
if (user == null) {
return null;
}
return <UserPosts user={user} />;
}
function UserPosts({ user }) {
const posts = useMemo(() => getUserPosts(user.id), [user.id]);
const handlePostSelect = useCallback(
(postId) => {
// TODO: Do something
},
[user]
);
return (
<div>
{posts.map((post) => (
<button key={post.id} onClick={() => handlePostSelect(post.id)}>
{post.title}
</button>
))}
</div>
);
}
:empty
偽類別隱藏沒有子元素的元素在下面的範例中👇,包裝器接受子專案並在它們周圍加入紅色邊框。
function PostWrapper({ children }) {
return <div className="posts-wrapper">{children}</div>;
}
.posts-wrapper {
border: solid 1px red;
}
❌ 問題:即使子項為空(即等於null
、 undefined
等),邊框在螢幕上仍可見。
✅ 解決方案:使用:empty
CSS 偽類來確保包裝器為空時不顯示。
.posts-wrapper:empty {
display: none;
}
當所有狀態和上下文都位於頂部時,很容易發現什麼可以觸發元件重新渲染。
❌ 不好:狀態和上下文分散,難以追蹤。
function App() {
const [email, setEmail] = useState("");
const onEmailChange = (event) => {
setEmail(event.target.value);
};
const [password, setPassword] = useState("");
const onPasswordChange = (event) => {
setPassword(event.target.value);
};
const theme = useContext(ThemeContext);
return (
<div className={`App ${theme}`}>
<h1>Welcome</h1>
<p>
Email: <input type="email" value={email} onChange={onEmailChange} />
</p>
<p>
Password:{" "}
<input type="password" value={password} onChange={onPasswordChange} />
</p>
</div>
);
}
✅ 好:所有狀態和上下文都分組在頂部,使其易於發現。
function App() {
const theme = useContext(ThemeContext);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const onEmailChange = (event) => {
setEmail(event.target.value);
};
const onPasswordChange = (event) => {
setPassword(event.target.value);
};
return (
<div className={`App ${theme}`}>
<h1>Welcome</h1>
<p>
Email: <input type="email" value={email} onChange={onEmailChange} />
</p>
<p>
Password:{" "}
<input type="password" value={password} onChange={onPasswordChange} />
</p>
</div>
);
}
children
props 實現更簡潔的程式碼(以及效能優勢)使用children
道具有幾個好處:
好處#1:您可以透過將 prop 直接傳遞給子元件而不是透過父元件路由它們來避免 prop 鑽取。
好處#2:您的程式碼更具可擴展性,因為您可以輕鬆修改子元件而無需更改父元件。
好處#3:您可以使用此技巧來避免重新渲染「慢」元件(請參閱下面的範例👇)。
❌ 不好:每當Dashboard
渲染時, MyVerySlowComponent
就會渲染,每次當前時間更新時都會發生這種情況。您可以在下圖中看到它,我在其中使用了React Developer Tool 的分析器。
function App() {
// Some other logic…
return (
<Dashboard />
);
}
function Dashboard() {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(new Date());
}, 1_000);
return () => clearInterval(intervalId);
}, []);
return (
<>
<h1>{currentTime.toTimeString()}</h1>
<MyVerySlowComponent /> {/* Renders whenever `Dashboard` renders */}
</>
);
}
<
圖>
每當儀表板呈現時, MyVerySlowComponent都會呈現
✅ 好: Dashboard
渲染時MyVerySlowComponent
不會渲染。
function App() {
return (
<Dashboard >
<MyVerySlowComponent />
</Dashboard>
);
}
function Dashboard({ children }) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(new Date());
}, 1_000);
return () => clearInterval(intervalId);
}, []);
return (
<>
<h1>{currentTime.toTimeString()}</h1>
{children}
</>
);
}
<
圖>
MyVerySlowComponent不再渲染
compound components
建構可組合程式碼將複合元件視為樂高積木。
您將它們組合在一起以建立自訂的 UI。這些元件在建立庫時工作得非常好,從而產生富有表現力和高度可擴展的程式碼。
您可以在此處進一步探索此模式👉複合模式
來自reach.ui的範例( Menu、MenuButton、MenuList、MenuLink是複合元件)
<Menu>
<MenuButton>
Actions <span aria-hidden>▾</span>
</MenuButton>
<MenuList>
<MenuItem onSelect={() => alert("Download")}>Download</MenuItem>
<MenuItem onSelect={() => alert("Copy")}>Create a Copy</MenuItem>
<MenuLink as="a" href="https://reacttraining.com/workshops/">
Attend a Workshop
</MenuLink>
</MenuList>
</Menu>
render functions
或component functions
props 使程式碼更具可擴充性假設我們想要顯示各種列表,例如訊息、個人資料或帖子,並且每個列表都應該是可排序的。
為了實現這一點,我們引入了一個List
元件以供重複使用。我們可以透過兩種方法來解決這個問題:
❌ 不好:選項 1
List
處理每個專案的渲染以及它們的排序方式。這是有問題的,因為它違反了開閉原則。每當新增新的專案類型時,都會修改此程式碼。
✅ 好:選項 2
List
接受渲染函數或元件函數,僅在需要時呼叫它們。
您可以在下面的沙箱中找到一個範例:
value === case && <Component />
以避免保留舊狀態❌問題:在下面的沙箱中,在Posts
和Snippets
之間切換時計數器不會重置。發生這種情況是因為在渲染相同元件時,其狀態在類型變更時保持不變。
✅ 解決方案:根據selectedType
渲染一個元件或在type改變時使用按鍵強制重設。
function App() {
const [selectedType, setSelectedType] = useState<ResourceType>("posts");
return (
<>
<Navbar selectedType={selectedType} onSelectType={setSelectedType} />
{selectedType === "posts" && <Resource type="posts" />}
{selectedType === "snippets" && <Resource type="snippets" />}
</>
);
}
// We use the `selectedType` as a key
function App() {
const [selectedType, setSelectedType] = useState<ResourceType>("posts");
return (
<>
<Navbar selectedType={selectedType} onSelectType={setSelectedType} />
<Resource type={selectedType} key={selectedType} />
</>
);
}
預設情況下,如果你的應用程式在渲染過程中遇到錯誤,整個 UI 就會崩潰💥。
為了防止這種情況,請使用錯誤邊界來:
即使發生錯誤,也能保持應用程式的某些部分正常運作。
顯示使用者友善的錯誤訊息並可選擇追蹤錯誤。
💡提示:您可以使用react-error-boundary庫。
crypto.randomUUID
或Math.random
產生金鑰map()
呼叫中的 JSX 元素始終需要鍵。
假設您的元素還沒有鍵。在這種情況下,您可以使用crypto.randomUUID
、 Math.random
或uuid函式庫產生唯一 ID。
注意:請注意,舊版瀏覽器中未定義
crypto.randomUUID
。
function App({ fruits }) {
const fruitsWithIds = fruits.map((fruit) => ({
value: fruit,
id: crypto.randomUUID(), // or uuid.v4(), Math.random()
}));
return <List items={fruitsWithIds} />;
}
function List({ items }) {
return (
<ul>
{items.map(({ value, id }) => {
return <li key={id}>{value}</li>;
})}
</ul>
);
}
密鑰/ID 應盡可能穩定。
否則,React 可能會無用地重新渲染某些元件,或者選擇將不再有效,如下例所示。
❌ 不好: selectedQuoteId
會在App
渲染時發生變化,因此永遠不會有有效的選擇。
function App() {
const [quotes, setQuotes] = useState([]);
const [selectedQuoteId, setSelectedQuoteId] = useState(undefined);
// Fetch quotes
useEffect(() => {
const loadQuotes = () =>
fetchQuotes().then((result) => {
setQuotes(result);
});
loadQuotes();
}, []);
// Add ids: this is bad!!!
const quotesWithIds = quotes.map((quote) => ({
value: quote,
id: crypto.randomUUID(),
}));
return (
<List
items={quotesWithIds}
selectedItemId={selectedQuoteId}
onSelectItem={setSelectedQuoteId}
/>
);
}
✅ 好:當我們收到報價時會新增IDs
。
function App() {
const [quotes, setQuotes] = useState([]);
const [selectedQuoteId, setSelectedQuoteId] = useState(undefined);
// Fetch quotes and save with ID
useEffect(() => {
const loadQuotes = () =>
fetchQuotes().then((result) => {
// We add the `ids` as soon as we get the results
setQuotes(
result.map((quote) => ({
value: quote,
id: crypto.randomUUID(),
}))
);
});
loadQuotes();
}, []);
return (
<List
items={quotes}
selectedItemId={selectedQuoteId}
onSelectItem={setSelectedQuoteId}
/>
);
}
key
屬性來觸發元件重新渲染想要強制元件從頭開始重新渲染?只需更改其key
即可。
在下面的範例中,我們使用此技巧在切換到新分頁時重設錯誤邊界。
ref callback function
來執行監視 ref 大小變化和管理多個節點元素等任務。您是否知道可以將函數而不是 ref 物件傳遞給ref
屬性?
它的工作原理如下:
當 DOM 節點新增到螢幕上時,React 以 DOM 節點作為參數呼叫函數。
當 DOM 節點被刪除時,React 使用null
呼叫函數。
在下面的例子中,我們使用這個技巧來跳過useEffect
❌之前:使用useEffect
聚焦輸入
function App() {
const ref = useRef();
useEffect(() => {
ref.current?.focus();
}, []);
return <input ref={ref} type="text" />;
}
✅ 之後:一旦有輸入,我們就會立即關注。
function App() {
const ref = useCallback((inputNode) => {
inputNode?.focus();
}, []);
return <input ref={ref} type="text" />;
}
始終將每個 React 元件與相關資產一起保存,例如樣式和圖像。
這使得當不再需要該元件時可以更輕鬆地刪除它們。
它還簡化了程式碼導航,因為您需要的一切都集中在一個地方。
包含大量元件和匯出的大檔案可能會令人困惑。
另外,隨著更多東西的加入,它們往往會變得更大。
因此,以合理的檔案大小為目標,並在有意義時將元件拆分為單獨的檔案。
功能元件中的多個return
語句使得很難看到元件傳回的內容。
對於我們可以搜尋render
術語的類別元件來說,這不是問題。
一個方便的技巧是盡可能使用不含大括號的箭頭函數(VSCode 有一個針對此的運算😀)。
❌ 缺點:更難發現元件回傳語句
function Dashboard({ posts, searchTerm, onPostSelect }) {
const filteredPosts = posts.filter((post) => {
return post.title.includes(searchTerm);
});
const createPostSelectHandler = (post) => {
return () => {
onPostSelect(post.id);
};
};
return (
<>
<h1>Posts</h1>
<ul>
{filteredPosts.map((post) => {
return (
<li key={post.id} onClick={createPostSelectHandler(post)}>
{post.title}
</li>
);
})}
</ul>
</>
);
}
✅ 好:元件有一個回傳語句
function Dashboard({ posts, searchTerm, onPostSelect, selectedPostId }) {
const filteredPosts = posts.filter((post) => post.title.includes(searchTerm));
const createPostSelectHandler = (post) => () => {
onPostSelect(post.id);
};
return (
<>
<h1>Posts</h1>
<ul>
{filteredPosts.map((post) => (
<li
key={post.id}
onClick={createPostSelectHandler(post)}
style={{ color: post.id === selectedPostId ? "red" : "black" }}
>
{post.title}
</li>
))}
</ul>
</>
);
}
我到處都看到預設導出,這讓我很難過🥲。
讓我們比較一下這兩種方法:
/// `Dashboard` is exported as the default component
export function Dashboard(props) {
/// TODO
}
/// `Dashboard` export is named
export function Dashboard(props) {
/// TODO
}
我們現在像這樣導入元件:
/// Default export
import Dashboard from "/path/to/Dashboard"
/// Named export
import { Dashboard } from "/path/to/Dashboard"
這些都是預設導出的問題:
例如,如果Dashboard
重新命名為Console
,我們將得到以下結果:
/// In the default export case, the name is not changed
import Dashboard from "/path/to/Console"
/// In the named export case, the name is changed
import { Console } from "/path/to/Console"
例如,在命名導入的情況下,一旦我輸入import { } from "/path/to/file"
,當我將遊標放在括號內時,我就會得到自動補全。
例如,如果我想從索引index
重新匯出Dashboard
元件,我必須這樣做:
export { default as Dashboard } from "/path/to/Dashboard"
使用命名導出,解決方案更加簡單。
export { Dashboard } from "/path/to/Dashboard"
所以,請預設為命名導出🙏。
更多的狀態=更多的麻煩。
每個狀態都可以觸發重新渲染,並使重置狀態變得非常麻煩。
因此,如果可以從狀態或道具派生出值,請跳過新增狀態。
❌ 不好: filteredPosts
不需要處於狀態。
function App({ posts }) {
const [filters, setFilters] = useState();
const [filteredPosts, setFilteredPosts] = useState([]);
useEffect(
() => {
setFilteredPosts(filterPosts(posts, filters));
},
[posts, filters]
);
return (
<Dashboard>
<Filters filters={filters} onFiltersChange={setFilters} />
{filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
</Dashboard>
);
}
✅ 好: filteredPosts
源自於posts
和filters.
function App({ posts }) {
const [filters, setFilters] = useState({});
const filteredPosts = filterPosts(posts, filters)
return (
<Dashboard>
<Filters filters={filters} onFiltersChange={setFilters} />
{filteredPosts.length > 0 && <Posts posts={filteredPosts} />}
</Dashboard>
);
}
每當元件內部的狀態發生變化時,React 都會重新渲染該元件及其所有子元件(子元件包裹在memo中是一個例外)。
即使這些孩子不使用更改後的狀態,也會發生這種情況。為了最大程度地減少重新渲染,請盡可能將狀態在元件樹中向下移動。
❌ 不好:當sortOrder
改變時, LeftSidebar
和RightSidebar
都會重新渲染。
function App() {
const [sortOrder, setSortOrder] = useState("popular");
return (
<div className="App">
<LeftSidebar />
<Main sortOrder={sortOrder} setSortOrder={setSortOrder} />
<RightSidebar />
</div>
);
}
function Main({ sortOrder, setSortOrder }) {
return (
<div>
<Button
onClick={() => setSortOrder("popular")}
active={sortOrder === "popular"}
>
Popular
</Button>
<Button
onClick={() => setSortOrder("latest")}
active={sortOrder === "latest"}
>
Latest
</Button>
</div>
);
}
✅ 好: sortOrder
更改只會影響Main
。
function App() {
return (
<div className="App">
<LeftSidebar />
<Main />
<RightSidebar />
</div>
);
}
function Main() {
const [sortOrder, setSortOrder] = useState("popular");
return (
<div>
<Button
onClick={() => setSortOrder("popular")}
active={sortOrder === "popular"}
>
Popular
</Button>
<Button
onClick={() => setSortOrder("latest")}
active={sortOrder === "latest"}
>
Latest
</Button>
</div>
);
}
❌ 不好:不清楚 sortOrder 只是初始值,這可能會導致狀態管理的混亂或錯誤。
function Main({ sortOrder }) {
const [internalSortOrder, setInternalSortOrder] = useState(sortOrder);
return (
<div>
<Button
onClick={() => setInternalSortOrder("popular")}
active={internalSortOrder === "popular"}
>
Popular
</Button>
<Button
onClick={() => setInternalSortOrder("latest")}
active={internalSortOrder === "latest"}
>
Latest
</Button>
</div>
);
}
✅ 好:命名清楚地表示什麼是初始狀態,什麼是目前狀態。
function Main({ initialSortOrder }) {
const [sortOrder, setSortOrder] = useState(initialSortOrder);
return (
<div>
<Button
onClick={() => setSortOrder("popular")}
active={sortOrder === "popular"}
>
Popular
</Button>
<Button
onClick={() => setSortOrder("latest")}
active={sortOrder === "latest"}
>
Latest
</Button>
</div>
);
}
useCallback
進行記憶時React 允許您將 updater 函數從useState
傳遞給set
函數。
此更新器函數使用目前狀態來計算下一個狀態。
每當我需要根據先前的狀態更新狀態時,我都會使用此行為,尤其是在useCallback.
事實上,這種方法不需要將狀態作為鉤子依賴項之一。
❌ 不好:每當todos
改變時, handleAddTodo
和handleRemoveTodo
也會改變。
function App() {
const [todos, setToDos] = useState([]);
const handleAddTodo = useCallback(
(todo) => {
setToDos([...todos, todo]);
},
[todos]
);
const handleRemoveTodo = useCallback(
(id) => {
setToDos(todos.filter((todo) => todo.id !== id));
},
[todos]
);
return (
<div className="App">
<TodoInput onAddTodo={handleAddTodo} />
<TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
</div>
);
}
✅ 好:即使todos
發生變化, handleAddTodo
和handleRemoveTodo
也保持不變。
function App() {
const [todos, setToDos] = useState([]);
const handleAddTodo = useCallback((todo) => {
setToDos((prevTodos) => [...prevTodos, todo]);
}, []);
const handleRemoveTodo = useCallback((id) => {
setToDos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
}, []);
return (
<div className="App">
<TodoInput onAddTodo={handleAddTodo} />
<TodoList todos={todos} onRemoveTodo={handleRemoveTodo} />
</div>
);
}
useState
中的函數進行延遲初始化和效能提升,因為它們只被呼叫一次。在 useState 中使用函數可確保初始狀態僅計算一次。
這可以提高效能,特別是當初始狀態源自於「昂貴」操作(例如從本地儲存讀取)時。
❌ 不好:每次元件渲染時我們都會從本地儲存讀取主題
const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";
function PageWrapper({ children }) {
const [theme, setTheme] = useState(
localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
);
const handleThemeChange = (theme) => {
setTheme(theme);
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
};
return (
<div
className="page-wrapper"
style={{ background: theme === "dark" ? "black" : "white" }}
>
<div className="header">
<button onClick={() => handleThemeChange("dark")}>Dark</button>
<button onClick={() => handleThemeChange("light")}>Light</button>
</div>
<div>{children}</div>
</div>
);
}
✅ 好:我們僅在元件安裝時從本地存儲中讀取。
function PageWrapper({ children }) {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
);
const handleThemeChange = (theme) => {
setTheme(theme);
localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
};
return (
<div
className="page-wrapper"
style={{ background: theme === "dark" ? "black" : "white" }}
>
<div className="header">
<button onClick={() => handleThemeChange("dark")}>Dark</button>
<button onClick={() => handleThemeChange("light")}>Light</button>
</div>
<div>{children}</div>
</div>
);
}
每當我有一些資料時,我都會使用 React 上下文:
在多個地方都需要(例如,主題、當前使用者等)
大部分是靜態的或唯讀的(即使用者不能/不經常更改資料)
這種方法有助於避免道具鑽探(即,透過元件層次結構的多個層向下傳遞資料或狀態)。
請參閱下面沙箱中的範例👇。
React 上下文的一個挑戰是,每當上下文資料發生變化時,所有使用上下文的元件都會重新渲染,即使它們不使用上下文中發生變化的部分🤦♀️。
一個辦法?使用單獨的上下文。
在下面的範例中,我們建立了兩個上下文:一個用於操作(恆定),另一個用於狀態(可以更改)。
Provider
元件❌ 缺點: App
內有太多邏輯來管理主題。
export function App() {
const [theme, setTheme] = useState(() =>
window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
);
useEffect(() => {
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
const listener = (event) => {
setTheme(event.matches ? "dark" : "light");
};
darkThemeMq.addEventListener("change", listener);
return () => darkThemeMq.removeEventListener("change", listener);
}, []);
const [selectedPostId, setSelectedPostId] = useState(undefined);
const onPostSelect = (postId) => {
// TODO: some logging
setSelectedPostId(postId);
};
return (
<div className="App">
<ThemeContext.Provider value={theme}>
<Dashboard
posts={posts}
onPostSelect={onPostSelect}
selectedPostId={selectedPostId}
/>
</ThemeContext.Provider>
</div>
);
}
✅ 好:主題邏輯封裝在ThemeProvider
中
export function App() {
const [selectedPostId, setSelectedPostId] = useState(undefined);
const onPostSelect = (postId) => {
// TODO: some logging
setSelectedPostId(postId);
};
return (
<div className="App">
<ThemeProvider>
<Dashboard
posts={posts}
onPostSelect={onPostSelect}
selectedPostId={selectedPostId}
/>
</ThemeProvider>
</div>
);
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() =>
window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
);
useEffect(() => {
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
const listener = (event) => {
setTheme(event.matches ? "dark" : "light");
};
darkThemeMq.addEventListener("change", listener);
return () => darkThemeMq.removeEventListener("change", listener);
}, []);
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);
}
useReducer
鉤子作為輕量級狀態管理解決方案每當我的狀態或複雜狀態中有太多值並且不想依賴外部程式庫時,我就會使用useReducer
。
當與更廣泛的狀態管理需求的上下文相結合時,它尤其有效。
範例:請參閱#Tip 34 。
useImmer
或useImmerReducer
簡化狀態更新對於useState
和useReducer
這樣的鉤子,狀態必須是不可變的(即所有變更都需要建立一個新狀態而不是修改目前狀態)。
這通常很難實現。
這就是useImmer和useImmerReducer提供更簡單的替代方案的地方。它們允許您編寫自動轉換為不可變更新的“可變”程式碼。
❌ 不好:我們必須仔細確保我們正在建立一個新的狀態物件。
export function 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>Welcome</h1>
<p>
Email: <input type="email" value={email} onChange={onEmailChange} />
</p>
<p>
Password:{" "}
<input type="password" value={password} onChange={onPasswordChange} />
</p>
</div>
);
}
✅ 好:我們可以直接修改draftState
。
import { useImmer } from "use-immer";
export function 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;
});
};
/// Rest of logic
}
每當出現以下情況時我都會轉向Redux :
我有一個複雜的 FE 應用程式,其中包含許多共享的客戶端狀態(例如儀表板應用程式)
我希望用戶能夠回到過去並恢復更改
我不希望我的元件像 React 上下文那樣不必要地重新渲染
我有太多的情況開始失控
為了獲得簡化的體驗,我建議使用redux-tooltkit 。
Redux DevTools 瀏覽器擴充功能是除錯 Redux 專案的實用工具。
它允許您即時視覺化您的狀態和操作,在刷新過程中保持狀態持久性等等。
要了解其用途,請觀看這段精彩的YouTube 影片。
memo
防止不必要的重新渲染當處理渲染成本高昂且其父元件頻繁更新的元件時,將它們包裝在備忘錄中可能會改變遊戲規則。
memo
確保元件僅在其 props 變更時重新渲染,而不僅僅是因為其父元件重新渲染。
在下面的範例中,我透過useGetDashboardData
從伺服器取得一些資料。如果posts
沒有更改,則將ExpensiveList
包裝在memo
中將阻止它在資料其他部分更新時重新渲染。
export function App() {
const { profileInfo, posts } = useGetDashboardData();
return (
<div className="App">
<h1>Dashboard</h1>
<Profile data={profileInfo} />
<ExpensiveList posts={posts} />
</div>
);
}
const ExpensiveList = memo(
({ posts }) => {
/// Rest of implementation
}
);
💡:一旦React 編譯器變得穩定,這個技巧可能就不再重要了。
memo
的相等函數來指示 React 如何比較 props。預設情況下, memo
使用Object.is將每個 prop 與其先前的值進行比較。
但是,對於更複雜或特定的場景,指定自訂相等函數可能比預設比較或重新渲染更有效。
例👇
const ExpensiveList = memo(
({ posts }) => {
return <div>{JSON.stringify(posts)}</div>;
},
(prevProps, nextProps) => {
// Only re-render if the last post or the list size changes
const prevLastPost = prevProps.posts[prevProps.posts.length - 1];
const nextLastPost = nextProps.posts[nextProps.posts.length - 1];
return (
prevLastPost.id === nextLastPost.id &&
prevProps.posts.length === nextProps.posts.length
);
}
)
定義記憶元件時,使用命名函數而不是箭頭函數可以提高 React DevTools 中的清晰度。
箭頭函數通常會產生像_c2
這樣的通用名稱,這使得偵錯和分析變得更加困難。
❌ 不好:對記憶元件使用箭頭函數會導致 React DevTools 中的名稱資訊量較少。
const ExpensiveList = memo(
({ posts }) => {
/// Rest of implementation
}
);
<
圖>
ExpenseList名稱不可見
✅ 好:元件的名稱將在 DevTools 中可見。
const ExpensiveList = memo(
function ExpensiveListFn({ posts }) {
/// Rest of implementation
}
)