在这篇文章中,我分享了我多年来学到的101个最佳提示和技巧。准备好了吗?让我们开始吧💪!
注意:本指南假定你对 React 有基本的了解,并了解术语
props
、state
、context
等。
类别 #1:组件组织
1. 使用自闭合标签保持代码紧凑
// ❌ Bad: too verbose
<MyComponent></MyComponent>
// ✅ Good
<MyComponent/>
2. 优先使用 fragment
而不是 DOM 节点(例如 div、span 等)来对元素进行分组
在 React 中,每个组件必须返回一个元素。不要将多个元素包装在 <div>
或 <span>
中,而是使用 <Fragment>
来保持 DOM 整洁。
❌ Bad: 使用 div
会使您的 DOM 混乱,并且可能需要更多的 CSS 代码。
function Dashboard() {
return (
<div>
<Header />
<Main />
</div>
);
}
✅ Good: <Fragment>
在不影响 DOM 结构的情况下包装元素。
function Dashboard() {
return (
<Fragment>
<Header />
<Main />
</Fragment>
);
}
3. 使用 React 片段简写 <></>
(除非需要设置key)
❌ Bad: 下面的代码过于冗长。
<Fragment>
<FirstChild />
<SecondChild />
</Fragment>
✅ Good: <></>
更简洁。
<>
<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>
);
}
4. 更喜欢展开 props 而不是单独访问每个 prop
❌ Bad: 下面的代码更难阅读(尤其是在大规模时)。
// 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>
);
}
✅ Good: 下面的代码更简洁。
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>
);
}
5. 设置 props 的默认值时,请在解构时进行
❌ Bad: 您可能需要在多个位置定义默认值并引入新变量。
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>
);
}
✅ Good:可以在顶部的一个位置设置所有默认值。这使其他人很容易找到它们。
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>
);
}
6. 传递字符串
类型 props 时去掉大括号。
// ❌ Bad: curly braces are not needed
<Button text={"Click me"} colorScheme={"dark"} />
// ✅ Good
<Button text="Click me" colorScheme="dark" />
7. 在使用 value&&<Component {...props}/>
时以防止在屏幕上显示意外值。
❌ Bad: 当列表为空时,屏幕上将显示 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>
);
}
✅ Good:当没有项目时,屏幕上不会打印任何内容。
export function ListWrapper({ items, selectedItem, setSelectedItem }) {
return (
<div className="list">
{items.length > 0 && (
<List
items={items}
onSelectItem={setSelectedItem}
selectedItem={selectedItem}
/>
)}
</div>
);
}
8. 使用函数(内联或非内联)以避免中间变量污染您的作用域
❌ Bad: 变量 gradeSum
和 gradeCount
使组件的作用域变得混乱。
function Grade({ grades }) {
if (grades.length === 0) {
return <>No grades available.</>;
}
let gradeSum = 0;
let gradeCount = 0;
grades.forEach((grade) => {
gradeCount++;
gradeSum += grade;
});
const averageGrade = gradeSum / gradeCount;
return <>Average Grade: {averageGrade}</>;
}
✅ Good: 变量 gradeSum
和 gradeCount
的范围限定在 computeAverageGrade
函数中。
function Grade({ grades }) {
if (grades.length === 0) {
return <>No grades available.</>;
}
// 这个地方优先考虑使用 useMemo
const computeAverageGrade = () => {
let gradeSum = 0;
let gradeCount = 0;
grades.forEach((grade) => {
gradeCount++;
gradeSum += grade;
});
return gradeSum / gradeCount;
};
return <>Average Grade: {computeAverageGrade()}</>;
}
9. 使用柯里化函数重用逻辑(并正确记住回调函数)
❌ Bad: 更新字段的逻辑非常重复。
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>
</>
);
}
✅ Good: 引入 createFormValueChangeHandler
,它为每个字段返回正确的处理程序。
注意: 如果你打开了 ESLint 规则 jsx-no-bind,这个技巧就特别好了。你可以把柯里化的函数包装在
useCallback
中。
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>
</>
);
}
10. 将不依赖于组件 props/state 的数据移到它之外,以获得更简洁高效的代码。
❌ Bad: OPTIONS
和 renderOption
不需要在组件内部,因为它们不依赖于任何 props 或 state。
此外,将它们保留在内部意味着每次组件渲染时我们都会获得新的对象引用。如果我们要将 renderOption
传递给包装在 memo
中的子组件,它会破坏 memoization。
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>
);
}
✅ Good: 将它们移出组件,以保持组件干净和引用稳定。
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>
);
}
11. 存储列表中的选定项时,请存储项 ID 而不是整个项
❌ Bad: 如果选择了一个项目,但随后它发生了变化(即我们收到相同 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}
/>
</>
);
}
✅ Good: 我们按其 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}
/>
</>
);
}
12. 如果你经常在做某事之前检查 prop 的值,请引入一个新组件
❌ Bad: 由于所有用户 == null
检查,代码变得混乱。
在这里,由于 hook 的规则,我们不能提前返回。
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>
);
}
✅ Good: 我们引入了一个新组件 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>
);
}
13. 使用 CSS :empty
伪类隐藏没有子元素的元素
在下面的示例中👇,包装器采用 children 并在其周围添加红色边框。
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;
}
14. 将所有 state 和 context 分组到组件的顶部
当所有 state 和 context 都位于顶部时,很容易发现什么可以触发组件重新渲染。
❌ Bad: 状态和上下文是分散的,因此难以跟踪。
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>
);
}
✅ Good: 所有状态和上下文都集中在顶部,以便于查找。
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>
);
}
类别#2:有效的设计模式和技术
15. 利用 children
属性实现更简洁的代码(和性能优势)
使用 children
props 有几个好处:
好处 #1:你可以通过将 props 直接传递给子组件而不是通过父组件路由它们来避免 prop 钻取。
好处 #2:您的代码更具可扩展性,因为您可以在不更改父组件的情况下轻松修改 children。
好处 #3:您可以使用此技巧来避免重新渲染 “慢速” 组件(请参阅下面的👇示例)。
❌ Bad: 每当 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());
}, 1000);
return () => clearInterval(intervalId);
}, []);
return (
<>
<h1>{currentTime.toTimeString()}</h1>
<MyVerySlowComponent /> {/* Renders whenever `Dashboard` renders */}
</>
);
}
✅ Good: MyVerySlowComponent
在 Dashboard
渲染时不渲染。
function App() {
return (
<Dashboard >
<MyVerySlowComponent />
</Dashboard>
);
}
function Dashboard({ children }) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(intervalId);
}, []);
return (
<>
<h1>{currentTime.toTimeString()}</h1>
{children}
</>
);
}
16. 使用复合组件
构建可组合代码
将复合组件想象成乐高积木。将它们拼凑在一起以创建自定义 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>
17. 使用 render 函数
或组件函数
props 使您的代码更具可扩展性
假设我们想要显示各种列表,例如消息、个人资料或帖子,并且每个列表都应该是可排序的。为了实现这一点,我们引入了一个 List
组件以供重用。我们有两种方法可以解决这个问题:
❌ Bad: 选项 1
List
处理呈现每个项及其排序方式。这是有问题的,因为它违反了开闭原则。每当添加新的监控项类型时,此代码都会被修改。
✅ Good: 选项 2
List
接受 render 函数或组件函数,仅在需要时调用它们。
您可以在下面的 👇 Sandbox 中找到一个示例:
18. 在处理不同的情况时,使用 value === case & <Component />
以避免保持旧状态
❌ 问题:在下面的 Sandbox 中,在 Posts
和 Snippets
之间切换时,计数器不会重置。发生这种情况是因为在渲染同一个组件时,它的状态在类型更改后保持不变。
✅ 解决:根据 selectedType
渲染组件,或者在类型更改时使用键强制重置。
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} />
</>
);
}
19. 始终使用误差边界
默认情况下,如果应用程序在渲染过程中遇到错误,则整个 UI 会崩溃💥。若要防止这种情况,请使用错误边界:
- 即使发生错误,也能保持应用程序的某些部分正常运行。
- 显示用户友好的错误消息,并可选择跟踪错误。
💡 提示: 你可以使用 react-error-boundary 库。
类别 #3:Key和Refs
20. 使用 crypto.randomUUID
或 Math.random
生成密钥
map()
调用中的 JSX 元素总是需要键。假设你的元素还没有键。在这种情况下,您可以使用 crypto.randomUUID
、Math.random
或 uuid 库生成唯一 ID。
注意:
crypto.randomUUID
在旧版浏览器中未定义。
21. 确保您的列表项 ID 稳定(即它们在渲染之间不会更改)
键/ID 应尽可能稳定。否则,React 可能会无用地重新渲染一些组件,或者选择将不再有效,就像下面的例子一样。
❌ Bad: 每当 App
呈现时,selectedQuoteId
都会更改,因此永远不会有有效的选择。
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}
/>
);
}
✅ Good: 当我们获取报价时,将添加 ID
。
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}
/>
);
}
22. 策略性地使用 key
属性来触发组件重新渲染
想要强制组件从头开始重新渲染?只需更改其key。在下面的示例中,我们使用此技巧在切换到新选项卡时重置错误边界。
23. 将 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" />;
}
类别 #4:组织 React 代码
24. 将 React 组件与其资源(例如样式、图片等)放在一起
始终保持每个 React 组件包含相关资源,例如样式和图片。
- 这样可以在不再需要组件时更轻松地删除它们。
- 它还简化了代码导航,因为您需要的一切都在一个地方。
25. 限制组件文件大小
包含大量组件和导出的大文件可能会令人困惑。此外,随着添加更多内容,它们往往会变得更大。因此,请以合理的文件大小为目标,并合理地将组件拆分为单独的文件。
26. 限制函数组件文件中的 return 语句数量
函数式组件中的多个 return
语句使得很难看到组件返回的内容。对于我们可以搜索 render
词的类组件来说,这不是问题。一个方便的技巧是尽可能使用不带大括号的箭头函数(VSCode有一个可以实现这个的功能 😀)。
❌ Bad: 更难发现组件 return 语句
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>
</>
);
}
✅ Good: 组件有一个 return 语句
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>
</>
);
}
27. 首选命名导出而不是默认导出
我到处都能看到默认导出,这让我很难过🥲。让我们比较一下这两种方法:
/// `Dashboard` is exported as the default component
export default 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"
以下是默认导出的问题:
- 如果组件已重命名,IDE 不会自动重命名导出。
例如,如果 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"
- 很难查看从具有默认导出的文件中导出的内容。
例如,在命名导入的情况下,一旦我从 “/path/to/file” 键入 import { }
,当我将光标放在括号内时,我会得到自动补全。
- 默认导出更难重新导出。
例如,如果我想从索引
文件重新导出 Dashboard
组件,则必须执行以下操作:
export { default as Dashboard } from "/path/to/Dashboard"
对于命名导出,解决方案更直接。
export { Dashboard } from "/path/to/Dashboard"
因此,请默认使用命名导入 🙏 。
类别 #5:高效的状态管理
28. 永远不要为可以从其他 state 或 props 派生的值创建 state
更多的状态 = 更多的麻烦。每个 state 都可以触发重新渲染,并使重置 state 变得很麻烦。因此,如果一个值可以从 state 或 props 派生,请跳过添加新的 state。
❌ Bad: 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>
);
}
✅ Good: 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>
);
}
29. 将状态保持在最小化重新渲染所需的最低级别
每当组件内部的状态发生变化时,React 就会重新渲染该组件及其所有 children(children 包装在 memo 中时会出现异常)。
即使这些子项不使用 changed 状态,也会发生这种情况。为了最大限度地减少重新渲染,请尽可能地将 state 在组件树中向下移动。
❌ Bad: 当 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>
);
}
✅ Good: 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>
);
}
30. 明确初始状态和当前状态之间的区别
❌ Bad: 目前尚不清楚 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>
);
}
✅ Good: 命名清楚地说明了什么是初始状态,什么是当前状态。
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>
);
}
31. 根据之前的状态更新 state,尤其是在使用 useCallback
进行记忆时
React 允许你将 updater 函数从 useState
传递给 set
函数。此更新器函数使用当前状态来计算下一个状态。
每当我需要根据之前的状态更新状态时,我都会使用这种行为,尤其是在使用 useCallback
包装的函数中。事实上,这种方法不需要将 state 作为 hook 依赖项之一。
❌ Bad: 每当 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>
);
}
✅ Good: 即使 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>
);
}
32. 使用 useState
中的函数来延迟初始化和性能提升,因为它们只调用一次
在 useState 中使用函数可确保初始状态只计算一次。这可以提高性能,尤其是当初始状态来自“昂贵”的操作(如从本地存储读取)时。
❌ Bad: 每次组件渲染时,我们都会从本地存储中读取主题。
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>
);
}
✅ Good: 我们只在组件挂载时从本地存储中读取。
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>
);
}
33. 将 react context 用于广泛需要的静态状态,以防止 prop 钻探
每当我有一些数据时,我都会使用 React 上下文:
- 在多个地方需要(例如,主题、当前用户等)
- 大部分是静态或只读的(即,用户不能/不会经常更改数据)
这种方法有助于避免 prop 钻取(即通过组件层次结构的多个层向下传递数据或状态)。
请参阅下面的👇沙盒中的示例 。
34. React Context:将您的上下文拆分为经常更改的部分和不经常更改的部分,以提高应用程序性能
React context 的一个挑战是,每当上下文数据发生变化时,所有使用上下文的组件都会重新渲染,即使它们不使用上下文更改🤦 ♀️的部分。
解决方案?使用单独的上下文。在下面的示例中,我们创建了两个上下文:一个用于 actions (常量),另一个用于 state (可以更改)。
35. React Context:当值计算不简单时引入 Provider
组件
❌ Bad: App
内部有太多的逻辑来管理主题。
const THEME_LOCAL_STORAGE_KEY = "101-react-tips-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
theme: DEFAULT_THEME,
setTheme: () => null,
})
function App() {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
);
useEffect(() => {
if(theme !== "system") {
updateRootElementTheme(theme)
return;
}
// We need to get the class to apply based on the system theme
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
updateRootElementTheme(systemTheme)
// Then watch for changes in the system theme and update the root element accordingly
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 [selectedPostId, setSelectedPostId] = useState(undefined);
const onPostSelect = (postId) => {
// TODO: some logging
setSelectedPostId(postId);
};
const posts = useSWR("/api/posts", fetcher);
return (
<div className="App">
<ThemeContext.Provider value={themeContextValue}>
<Dashboard
posts={posts}
onPostSelect={onPostSelect}
selectedPostId={selectedPostId}
/>
</ThemeContext.Provider>
</div>
);
}
Good: 主题逻辑封装在 ThemeProvider 中
function App() {
const [selectedPostId, setSelectedPostId] = useState(undefined);
const onPostSelect = (postId) => {
// TODO: some logging
setSelectedPostId(postId);
};
const posts = useSWR("/api/posts", fetcher);
return (
<div className="App">
<ThemeProvider>
<Dashboard
posts={posts}
onPostSelect={onPostSelect}
selectedPostId={selectedPostId}
/>
</ThemeProvider>
</div>
);
}
function ThemeProvider({ children }) {
const [theme, setTheme] = useState(
() => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
);
useEffect(() => {
if (theme !== "system") {
updateRootElementTheme(theme);
return;
}
// We need to get the class to apply based on the system theme
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
updateRootElementTheme(systemTheme);
// Then watch for changes in the system theme and update the root element accordingly
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>
);
}
36. 考虑使用 useReducer
钩子作为轻量级状态管理解决方案🏖 Sandbox
每当我的 state 中有太多的值或复杂的 state 并且不想依赖外部库时,我就会使用 useReducer
。当与上下文结合使用以满足更广泛的状态管理需求时,它特别有效。例:见 #34。
37. 使用 useImmer
或 useImmerReducer
简化 state 更新
使用像 useState
和 useReducer
这样的 hook,state 必须是不可变的(即所有更改都需要创建一个新的 state,而不是修改当前的 state)。这通常很难实现。这就是 useImmer 和 useImmerReducer 提供更简单替代方案的地方。它们允许您编写自动转换为不可变更新的 “可变” 代码。
❌ Tedious: 我们必须仔细确保我们正在创建一个新的 state 对象。
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
}
38. 使用 Redux(或其他状态管理解决方案)处理跨多个组件访问的复杂客户端状态
每当我求助于 Redux:
- 我有一个复杂的 FE 应用程序,其中包含许多共享的客户端状态(例如,仪表板应用程序)
- 我希望用户能够回到过去并恢复更改
- 我不希望我的组件像使用 React 上下文那样不必要地重新渲染
- 我有太多的上下文开始失控
为了获得简化的体验,我建议使用 redux-tooltkit。
39. Redux:使用 Redux DevTools 调试你的 state
Redux DevTools 浏览器扩展是调试 Redux 项目的有用工具。它允许您实时可视化您的状态和操作,在刷新之间保持状态持久性等等。要了解它的用途,请观看这个很棒的 YouTube 视频。
类别 #6:React 代码优化 🚀
40. 防止使用 memo
进行不必要的重新渲染
当处理渲染成本高昂且其父组件频繁更新的组件时,将它们包装在 memo 中可能会改变游戏规则。
memo
确保组件仅在其 props 发生变化时重新渲染,而不仅仅是因为它的父组件被重新渲染。
在下面的示例中,我通过 useGetDashboardData
从服务器获取一些数据。如果帖子
没有更改,将 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 编译器稳定😅下来,这个提示可能就无关紧要了。
41. 使用 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
);
}
)
42. 在声明记忆化组件时,首选命名函数而不是箭头函数
在定义记忆化组件时,使用命名函数而不是箭头函数可以提高 React DevTools 的清晰度。箭头函数通常会产生 _c2
等泛型名称,这使得调试和分析更加困难。
❌ Bad: 对记忆化组件使用箭头函数会导致 React DevTools 中名称的信息较少。
const ExpensiveList = memo(
({ posts }) => {
/// Rest of implementation
}
);
✅ Good: 组件的名称将在 DevTools 中可见。
const ExpensiveList = memo(
function ExpensiveListFn({ posts }) {
/// Rest of implementation
}
);
43. 使用 useMemo
缓存昂贵的计算或保留引用
我一般使用useMemo的场景
:
- 当我有昂贵的计算时,不应该在每次渲染时重复。
- 如果计算的值是一个非原始值,在
useEffect
等钩子中用作依赖项。 - 计算出的非原始值将作为 prop 传递给包装在
memo
中的组件;否则,这将破坏记忆化,因为 React 使用 Object.is 来检测 props 是否更改。
❌ Bad: ExpensiveList
的 memo
不会阻止重新渲染,因为每次渲染都会重新创建样式。
export function App() {
const { profileInfo, posts, baseStyles } = useGetDashboardData();
// We get a new `styles` object on every render
const styles = { ...baseStyles, padding: "10px" };
return (
<div className="App">
<h1>Dashboard</h1>
<Profile data={profileInfo} />
<ExpensiveList posts={posts} styles={styles} />
</div>
);
}
const ExpensiveList = memo(
function ExpensiveListFn({ posts, styles }) {
/// Rest of implementation
}
);
✅ Good: useMemo 的使用保证了 baseStyles
发生变化时,样式
只发生变化,让 memo
能够有效地防止不必要的重新渲染。
export function App() {
const { profileInfo, posts, baseStyles } = useGetDashboardData();
// We get a new `styles` object only if `baseStyles` changes
const styles = useMemo(
() => ({ ...baseStyles, padding: "10px" }),
[baseStyles]
);
return (
<div className="App">
<h1>Dashboard</h1>
<Profile data={profileInfo} />
<ExpensiveList posts={posts} styles={styles} />
</div>
);
}
44. 使用 useCallback
记忆函数
useCallback
类似于 useMemo
,但它是明确为记忆函数而设计的。
❌ Bad: 每当主题发生变化时,handleThemeChange
会被调用两次,我们会向服务器推送日志两次。
function useTheme() {
const [theme, setTheme] = useState("light");
// `handleThemeChange` changes on every render
// As a result, the effect will be triggered after each render
const handleThemeChange = (newTheme) => {
pushLog(["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;
}
✅ Good: 将 handleThemeChange
包装在 useCallback 中可确保仅在必要时重新创建它,从而减少不必要的执行。
const handleThemeChange = useCallback((newTheme) => {
pushLog(["Theme changed"], {
context: {
theme: newTheme,
},
});
setTheme(newTheme);
}, []);
45. 记住从工具钩子返回的回调或值以避免性能问题
当你创建自定义 hook 与他人共享时,记住返回的值和函数至关重要。这种做法可以提高你的 hook 效率,并防止任何使用它的人出现不必要的性能问题。
❌ Bad: loadData
未被记住,并产生性能问题。
function useLoadData(fetchData) {
const [result, setResult] = useState({
type: "notStarted",
});
async function loadData() {
setResult({ type: "loading" });
try {
const data = await fetchData();
setResult({ type: "loaded", data });
} catch (err) {
setResult({ type: "error", error: err });
}
}
return { result, loadData };
}
✅ Good: 我们会记住所有内容,因此不会出现意外的性能问题。
function useLoadData(fetchData) {
const [result, setResult] = useState({
type: "notStarted",
});
// Wrap in `useRef` and use the `ref` value so the function never changes
const fetchDataRef = useRef(fetchData);
useEffect(() => {
fetchDataRef.current = fetchData;
}, [fetchData]);
// Wrap in `useCallback` and use the `ref` value so the function never changes
const loadData = useCallback(async () => {
setResult({ type: "loading" });
try {
const data = await fetchDataRef.current();
setResult({ type: "loaded", data });
} catch (err) {
setResult({ type: "error", error: err });
}
}, []);
return useMemo(() => ({ result, loadData }), [result, loadData])
}
46. 利用延迟加载和 Suspense
让您的应用程序加载更快
在构建应用程序时,请考虑对以下代码使用延迟加载和 Suspense
:
- 加载成本高。
- 仅与某些用户相关(如高级功能)。
- 初始用户交互不需要立即实现。
在下面的沙箱👇中 ,Slider 资源 (JS + CSS) 仅在您单击卡片后加载。
47. 限制您的网络以模拟慢速网络
您知道您可以直接在 Chrome 中模拟慢速互联网连接吗?
这在以下情况下特别有用:
- 客户报告加载时间缓慢,您无法在更快的网络上复制。
- 您正在实现延迟加载,并希望观察文件在较慢条件下的加载方式,以确保适当的加载状态。
48. 使用 react-window
或 react-virtuoso
有效地渲染列表
切勿一次呈现一长串项目,例如聊天消息、日志或无限列表。这样做可能会导致浏览器卡死。相反,采用虚拟化列表。这意味着仅呈现可能对用户可见的项子集。
像 react-window、react-virtuoso 或 @tanstack/react-virtual 这样的库就是为此目的而设计的。
❌ Bad: NonVirtualList
同时呈现所有 50,000 行日志行,即使它们不可见。
function NonVirtualList({ items }) {
return (
<div style={{ height: "100%" }}>
{items.map((log, index) => (
<div
key={log.id}
style={{
padding: "5px",
borderBottom:
index === items.length - 1 ? "none" : "1px solid #ccc",
}}
>
<LogLine log={log} index={index} />
</div>
))}
</div>
);
}
✅ Good:VirtualList
仅呈现可能可见的项。
function VirtualList({ items }) {
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 #ccc",
}}
>
<LogLine log={log} index={index} />
</div>
)}
/>
);
}
您可以在下面的沙盒中的两个选项之间切换,并注意使用 👇 NonVirtualList
时应用程序的性能有多糟糕。
类别 #7:调试 React 代码 🐞
49. 在将组件部署到生产环境之前,使用 StrictMode
捕获组件中的 bug
使用 StrictMode 是在开发过程中检测应用程序中潜在问题的主动方法。它有助于识别以下问题:
- 清理不完整,例如忘记释放资源。
- React 组件中,确保它们在相同输入(props、state 和 context)下返回一致的 JSX。
下面的示例显示了一个错误,因为从未调用 clearInterval
。StrictMode
通过运行两次 effect 来帮助捕获此问题,这将创建两个间隔。
50. 安装 React Developer Tools 浏览器扩展以查看/编辑您的组件并检测性能问题
React Developer Tools 是必须的扩展(Chrome、Firefox)。此扩展允许您:
- 可视化并深入研究 React 组件的细节,检查从 props 到 state 的所有内容。
- 直接修改组件的 state 或 props 以查看更改如何影响行为和渲染。
- 分析您的应用程序以确定组件何时以及为何重新渲染,从而帮助您发现性能问题。
💡 在这份出色的指南中了解如何使用它。
51. React DevTools 组件:突出显示呈现以识别潜在问题的组件
每当我怀疑我的应用程序存在性能问题时,我都会使用这个技巧。您可以突出显示渲染的组件以检测潜在问题(例如,渲染过多)。
下面的 gif 显示 FollowersListFn
组件在时间发生变化时重新渲染,这是错误的。
52. 在你的自定义钩子中使用 useDebugValue
以提高在 React DevTools 中的可见性
useDebugValue 可以是一个方便的工具,用于在 React DevTools 中为你的自定义钩子添加描述性标签。这使得直接从 DevTools 界面监控其状态变得更加容易。例如,考虑一下我用来获取和显示当前时间的自定义钩子,每秒更新一次:
function useCurrentTime(){
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1_000);
return () => clearInterval(intervalId);
}, [setTime]);
return time;
}
❌ Bad: 如果没有 useDebugValue
,实际时间值不会立即可见;你需要扩展 CurrentTime 钩子:
✅ Good:使用 useDebugValue
,当前时间很容易看到:
useDebugValue(time)
注意:请谨慎使用
useDebugValue
。最好为共享库中的复杂 hook 保留,因为在这些 hooks 中,了解内部状态至关重要。
53. 使用 why-did-you-render
库跟踪组件渲染并识别潜在的性能瓶颈
有时,组件会重新渲染,但目前尚不清楚原因🤦 ♀️。
虽然 React DevTools 很有帮助,但在大型应用程序中,它可能只提供模糊的解释,例如 “hook #1 rendered”,这可能毫无用处。
在这种情况下,您可以求助于 why-did-you-render 库。它提供了有关组件重新渲染原因的更详细见解,有助于更有效地查明性能问题。
我在下面的👇沙盒中做了一个例子。多亏了这个库,我们可以找到 FollowersList
组件的问题。
54. 在 Strict Mode 的第二次渲染期间隐藏日志
StrictMode 有助于在应用程序开发的早期捕获错误。但是,由于它会导致组件渲染两次,这可能会导致重复的日志,这可能会使您的控制台变得混乱。您可以在 Strict Mode (严格模式) 的第二次渲染期间隐藏日志以解决此问题。在下面的 gif 👇 中查看如何操作:
类别 #8:测试 React 代码 🧪
55. 使用 React Testing Library
有效地测试你的 React 组件
想要测试您的 React 应用程序吗?确保使用 @testing-library/react。您可以在此处找到一个最小示例。
56. React 测试库:使用测试游乐场轻松创建查询
正在努力决定在测试中使用哪些查询?考虑使用 testing playground 从组件的 HTML 中快速生成它们。以下是利用它的两种方法:
选项 #1:在测试中使用 screen.logTestingPlaygroundURL()
此函数会生成一个 URL,用于打开 Testing Playground 工具,其中已加载组件的 HTML。
选项 #2:安装 Testing Playground Chrome 扩展。此扩展允许您直接在浏览器中将鼠标悬停在应用程序中的元素上,以查找用于测试它们的最佳查询。
57. 与 Cypress
或 Playwright
进行端到端测试
需要进行端到端测试?请务必查看 Cypress 或 Playwright。
注意:在撰写本文时,Playwright 对组件的支持是试验性的。
58. 在 MSW
测试中模拟网络请求
有时,您的测试需要发出网络请求。与其实现自己的 mocks,不如考虑使用 MSW(Mock Service Worker)来处理您的 API 响应。
MSW 允许您直接在测试中拦截和操作网络交互,从而为模拟服务器响应提供强大而直接的解决方案,而不会影响实时服务器。
这种方法有助于维护受控且可预测的测试环境,从而提高测试的可靠性。
类别 #9:React 钩子 🎣
59. 确保在 useEffect
钩子中执行任何必要的清理
如果你正在设置任何需要稍后清理的东西,请始终在你的 useEffect
钩子中返回一个清理函数。这可以是从结束聊天会话到关闭数据库连接的任何内容。忽略此步骤可能会导致资源使用不佳和潜在的内存泄漏。
❌ Bad:此示例设置一个间隔。但是我们从来没有清除它,这意味着即使在组件卸载后它也会继续运行。
function Timer() {
const [time, setTime] = useState(new Date());
useEffect(() => {
setInterval(() => {
setTime(new Date());
}, 1_000);
}, []);
return <>Current time {time.toLocaleTimeString()}</>;
}
✅ Good:卸载组件时,将正确清除间隔。
function Timer() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1_000);
// We clear the interval
return () => clearInterval(intervalId);
}, []);
return <>Current time {time.toLocaleTimeString()}</>;
}
60. 使用 refs
访问 DOM 元素
像 document.getElementById
和 document.getElementsByClassName
这样的方法是被禁止的,因为 React 应该访问/操作 DOM。那么,当你需要访问 DOM 元素时,你应该怎么做呢?
你可以使用 useRef 钩子,就像下面的例子一样,我们需要访问 canvas 元素。
注意:我们可以向画布添加一个 ID 并使用
document.getElementById
,但不推荐。
61. 使用 refs
在重新渲染中保留值
如果你的 React 组件中有可变值没有存储在 state 中,你会注意到对这些值的更改不会在重新渲染后持续存在。除非您全局保存它们,否则会发生这种情况。您可以考虑将这些值放在 state 中。但是,如果它们与渲染无关,则可能会导致不必要的重新渲染,从而浪费性能。这就是 useRef 的亮点。
在下面的示例中,我想在用户单击某个按钮时停止计时器。为此,我需要将 intervalId 存储在某个位置。
❌ Bad:下面的示例不会按预期工作,因为 intervalId
会随着每个组件的重新渲染而被重置。
function Timer() {
const [time, setTime] = useState(new Date());
let intervalId;
useEffect(() => {
intervalId = setInterval(() => {
setTime(new Date());
}, 1_000);
return () => clearInterval(intervalId);
}, []);
const stopTimer = () => {
intervalId && clearInterval(intervalId);
};
return (
<>
<>Current time: {time.toLocaleTimeString()} </>
<button onClick={stopTimer}>Stop timer</button>
</>
);
}
✅ Good:通过使用 useRef
,我们确保在渲染之间保留间隔 ID。
function Timer() {
const [time, setTime] = useState(new Date());
const intervalIdRef = useRef();
const intervalId = intervalIdRef.current;
useEffect(() => {
const interval = setInterval(() => {
setTime(new Date());
}, 1_000);
intervalIdRef.current = interval;
return () => clearInterval(interval);
}, []);
const stopTimer = () => {
intervalId && clearInterval(intervalId);
};
return (
<>
<>Current time: {time.toLocaleTimeString()} </>
<button onClick={stopTimer}>Stop timer</button>
</>
);
}
62. 在 useEffect
等钩子中更喜欢命名函数而不是箭头函数,以便在 React 开发工具中轻松找到它们
如果你有很多钩子,在 React DevTools 中找到它们可能很有挑战性。一个技巧是使用命名函数,以便您可以快速发现它们。
❌ Bad:在众多钩子中很难找到具体效果。
function HelloWorld() {
useEffect(() => {
console.log("🚀 ~ Hello, I just got mounted")
}, []);
return <>Hello World</>;
}
✅ Good:您可以快速发现效果。
function HelloWorld() {
useEffect(function logOnMount() {
console.log("🚀 ~ Hello, I just got mounted");
}, []);
return <>Hello World</>;
}
63. 使用自定义钩子封装逻辑
假设我有一个组件,它从用户的深色模式首选项中获取主题并在应用程序内使用它。最好将返回主题的逻辑提取到自定义钩子中(以重用它并保持组件干净)。
❌ Bad:App
过于拥挤
function App() {
const [theme, setTheme] = useState("light");
useEffect(() => {
const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
setTheme(dqMediaQuery.matches ? "dark" : "light");
const listener = (event) => {
setTheme(event.matches ? "dark" : "light");
};
dqMediaQuery.addEventListener("change", listener);
return () => {
dqMediaQuery.removeEventListener("change", listener);
};
}, []);
return (
<div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
);
}
✅ Good:App
要简单得多,我们可以重用 logic
function App() {
const theme = useTheme();
return (
<div className={`App ${theme === "dark" ? "dark" : ""}`}>Hello Word</div>
);
}
// Custom hook that can be reused
function useTheme() {
const [theme, setTheme] = useState("light");
useEffect(() => {
const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
setTheme(dqMediaQuery.matches ? "dark" : "light");
const listener = (event) => {
setTheme(event.matches ? "dark" : "light");
};
dqMediaQuery.addEventListener("change", listener);
return () => {
dqMediaQuery.removeEventListener("change", listener);
};
}, []);
return theme;
}
64. 更喜欢函数而不是自定义钩子
当可以使用 function 时🛑,永远不要将 logic 放在 hook 中。实际上:
- Hook 只能在其他 hook 或 component 中使用,而 functions 可以在任何地方使用。
- 函数比钩子更简单。
- 函数更易于测试。
❌ Bad:useLocale
钩子是不必要的,因为它不需要是一个钩子。它不使用其他钩子,如 useEffect
、useState
等。
function App() {
const locale = useLocale();
return (
<div className="App">
<IntlProvider locale={locale}>
<BlogPost post={EXAMPLE_POST} />
</IntlProvider>
</div>
);
}
function useLocale() {
return window.navigator.languages?.[0] ?? window.navigator.language;
}
✅ Good:改为创建函数 getLocale
function App() {
const locale = getLocale();
return (
<div className="App">
<IntlProvider locale={locale}>
<BlogPost post={EXAMPLE_POST} />
</IntlProvider>
</div>
);
}
function getLocale() {
return window.navigator.languages?.[0] ?? window.navigator.language;
}
65. 使用 useLayoutEffect
钩子防止视觉 UI 故障
如果效果不是由用户交互引起的,则用户将在效果运行之前(通常是短暂的)看到 UI。因此,如果效果修改了 UI,用户将在看到更新的 UI 版本之前非常快速地看到初始 UI 版本,从而产生视觉故障。
使用 useLayoutEffect
可确保 effect 在所有 DOM 更改后同步运行,从而防止初始渲染故障。
在下面的沙箱中,我们希望宽度在列之间均匀分布(我知道这可以在 CSS 中完成,但我需要一个示例😅)。
使用 useEffect
,您可以在开头短暂地看到表正在发生变化。列在调整为正确大小之前,会以其默认大小呈现。
如果您正在寻找其他出色的用法,请查看这篇文章。
66. 使用 useId
钩子为可访问性属性生成唯一 ID
厌倦了想出 ID 或让它们发生冲突?你可以使用 useId 钩子在 React 组件中生成一个唯一的 ID,并确保你的应用程序是可访问的。
function Form() {
const id = useId();
return (
<div className="App">
<div>
<label>
Name{" "}
<input type="text" aria-describedby={id} />
</label>
</div>
<span id={id}>Make sure to include full name</span>
</div>
);
}
67. 使用 useSyncExternalStore
订阅外部 store
这是一个很少需要但超级强大的钩子💪。在以下情况下使用此钩子:
- 你有一些 state 在 React 树中无法访问(即,不在 state 或 context 中)
- 状态可能会更改,您需要将组件通知更改
在下面的示例中,我希望 Logger
单例在我的整个应用程序中记录错误、警告、信息等。这些是要求:
- 我需要能够在我的 React 应用程序中的任何位置(甚至在非 React 组件中)调用 this,因此我不会将其放在 state/context 中。
- 我想在
Logs
组件中向用户显示所有日志
👉 我可以在 Logs
组件中使用 useSyncExternalStore
来访问日志并侦听更改。
function createLogger() {
let logs = [];
let listeners = [];
const pushLog = (log) => {
logs = [...logs, log];
listeners.forEach((listener) => listener());
};
return {
getLogs: () => Object.freeze(logs),
subscribe: (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((l) => l !== listener);
};
},
info: (message) => {
pushLog({ level: "info", message });
console.info(message);
},
error: (message) => {
pushLog({ level: "error", message });
console.error(message);
},
warn: (message) => {
pushLog({ level: "warn", message });
console.warn(message);
},
};
}
export const Logger = createLogger();
68. 使用 useDeferredValue
钩子显示以前的查询结果,直到新结果可用
假设您正在构建一个在地图上表示国家/地区的应用程序。用户可以进行筛选以查看特定人口规模的国家/地区。每次 maxPopulationSize
更新时,都会重新渲染地图(请参阅下面的沙盒)。
因此,请注意,当您将滑块移动得太快时,它是多么卡顿。这是因为每次滑块移动时都会重新渲染地图。为了解决这个问题,我们可以使用 useDeferredValue
钩子,以便滑块平滑更新。
<Map
maxPopulationSize={deferredMaxPopulationSize}
// …
/>
如果您正在寻找其他出色的用法,请查看这篇文章。
类别 #10:必须知道的 React 库/工具 🧰
69. 使用 react-router
将路由合并到您的应用程序中
如果你需要你的应用程序支持多个页面,请查看 react-router。您可以在此处找到一个最小示例。
70. 使用 swr
或 React Query
在您的应用程序中实现一流的数据获取
众所周知,数据获取可能非常棘手。但是,像 swr 或 React Query 这样的库让它变得容易得多。 我推荐 swr 用于简单的用例,而 React Query 用于更复杂的用例。
71. 使用 formik
、React Hook Form
或 TanStack Form
等库简化表单状态管理
我曾经讨厌 React 🥲 中的表单管理。直到我发现了这样的库:
因此,如果您正在为表单而苦苦挣扎,请务必查看这些。
72. 使用 Format.js,
Lingui,
或 react-i18next.
实现你的应用程序的国际化。
如果您的应用程序需要支持多种语言,则应将其国际化。您可以使用以下库来实现这一点:
73. 使用framer-motion
轻松创建令人印象深刻的动画
动画可以使您的应用程序脱颖而出🔥。您可以使用 framer-motion 轻松创建它们。
74. 厌倦了重新发明带有定制钩子的轮子?查看 useHooks – The React Hooks Library
如果你和我一样,你已经一遍又一遍地写同样的钩子了。请先检查 usehooks.com,看看是否有人已经为您完成了这项工作。
75. 利用 Shadcdn 或无头 UI 等 UI 库简化应用程序开发
很难大规模构建可访问、响应迅速且美观的 UI。Shadcdn 或 Headless UI 等库使它变得更容易。
- Shadcdn 提供了一组可访问、可重用和可组合的 React 组件,您可以将这些组件复制并粘贴到您的应用程序中。在撰写本文时,它需要 Tailwind CSS。
- Headless UI 提供无样式、完全可访问的 UI 组件,您可以使用这些组件来构建自己的 UI 组件。
76. 使用 axe-core-npm
库检查您网站的可访问性
网站应该对每个人都开放。但是,很容易错过辅助功能问题。axe-core-npm 是一种快速、安全且可靠的方法,可以在开发网站时检查网站的可访问性。
💡 提示: 如果你是 VSCode 用户,你可以安装相关的扩展:axe Accessibility Linter。
77. 使用 react-codemod
轻松重构 React 代码
Codemod 是以💻编程方式在代码库上运行的转换。它们使重构代码库变得容易。
例如,React codemods 可以帮助你从代码库中删除所有 React 导入,更新代码以使用最新的 React 功能等等。因此,请务必在手动重构代码之前检查这些内容。
78. 使用 vite-pwa 将您的应用程序转换为渐进式 Web 应用程序 (PWA)
渐进式 Web 应用程序 (PWA) 的加载方式与常规网页类似,但提供脱机工作、推送通知和设备硬件访问等功能。你可以使用 vite-pwa 在 React 中轻松创建 PWA。
类别#11:React & Visual Studio Code 🛠️
79. 使用 Simple React Snippets snippets扩展提高您的工作效率
引导一个新的 React 组件可能很乏味😩。来自 Simple React Snippets 扩展的 Snippets 使它变得更容易。
80. 将 editor.stickyScroll.enabled
设置为 true
,可快速定位当前组件
我喜欢这个功能❤️。如果您有一个大文件,则可能很难找到当前组件。通过将 editor.stickyScroll.enabled
设置为 true
,当前组件将始终位于屏幕顶部。
❌ 无粘性卷轴
✅ 带粘性卷轴
81. 使用 VSCode Glean 或 VSCode React Refactor 等扩展简化重构
如果您需要经常重构代码(例如将 JSX 提取到新组件中),请务必查看 VSCode Glean 或 VSCode React Refactor 等扩展。
类别#12:React & TypeScript 🚀
82. 使用 ReactNode
而不是 JSX.Element | null | undefined | ...
使代码更紧凑
我经常看到这个错误。而不是像这样键入 leftElement
和 rightElement
属性:
const Panel = ({ leftElement, rightElement }: {
leftElement:
| JSX.Element
| null
| undefined
// | ...;
rightElement:
| JSX.Element
| null
| undefined
// | ...
}) => {
// …
};
您可以使用 ReactNode
使代码更加紧凑。
const MyComponent = ({ leftElement, rightElement }: { leftElement: ReactNode; rightElement: ReactNode }) => {
// …
};
83. 使用 PropsWithChildren
简化 expecting children 属性的组件的键入
你不必手动键入 children
属性。实际上,您可以使用 PropsWithChildren
来简化类型。
// 🟠 Ok
const HeaderPage = ({ children,...pageProps }: { children: ReactNode } & PageProps) => {
// …
};
// ✅ Better
const HeaderPage = ({ children, ...pageProps } : PropsWithChildren<PageProps>) => {
// …
};
84. 使用 ComponentProps
、ComponentPropsWithoutRef
有效地访问元素 props
在某些情况下,你需要弄清楚组件的 props。例如,假设你想要一个按钮,该按钮在单击时将记录到控制台。你可以使用 ComponentProps
来访问 button
元素的 props,然后覆盖 click
props。
const ButtonWithLogging = (props: ComponentProps<"button">) => {
const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {
console.log("Button clicked"); //TODO: Better logging
props.onClick?.(e);
};
return <button {...props} onClick={handleClick} />;
};
此技巧也适用于自定义组件。
const MyComponent = (props: { name: string }) => {
// …
};
const MyComponentWithLogging = (props: ComponentProps<typeof MyComponent>) => {
// …
};
85. 利用 MouseEventHandler
、FocusEventHandler
等类型进行简洁的类型
您可以使用像 MouseEventHandler
这样的类型来保持代码更简洁和可读,而不是手动键入事件处理程序。
// 🟠 Ok
const MyComponent = ({ onClick, onFocus, onChange }: {
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
onFocus: (e: FocusEvent<HTMLButtonElement>) => void;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) => {
// …
};
// ✅ Better
const MyComponent = ({ onClick, onFocus, onChange }: {
onClick: MouseEventHandler<HTMLButtonElement>;
onFocus: FocusEventHandler<HTMLButtonElement>;
onChange: ChangeEventHandler<HTMLInputElement>;
}) => {
// …
};
86. 当类型不能或不应该从初始值推断时,在 useState、useRef 等中显式指定类型
当无法从初始值推断出类型时,请不要忘记指定类型。 例如,在下面的示例中,状态中存储了一个 selectedItemId
。它应该是一个string
或 undefined
。由于没有指定类型,TypeScript 会将类型推断为 undefined
,这不是我们想要的。
// ❌ Bad: `selectedItemId` will be inferred as `undefined`
const [selectedItemId, setSelectedItemId] = useState(undefined);
// ✅ Good
const [selectedItemId, setSelectedItemId] = useState<string | undefined>(undefined);
💡 注意:与此相反的是,当 TypeScript 可以为您推断类型时,您不需要指定类型。
87. 利用 Record
类型获得更简洁、更具可扩展性的代码
我喜欢这种帮助程序类型。假设我有一个表示对数级别的类型。
type LogLevel = "info" | "warn" | "error";
对于每个日志级别,我们都有一个相应的函数来记录消息。
const logFunctions = {
info: (message: string) => console.info(message),
warn: (message: string) => console.warn(message),
error: (message: string) => console.error(message),
};
您可以使用 Record 类型,而不是手动键入 logFunctions。
const logFunctions: Record<LogLevel, (message: string) => void> = {
info: (message) => console.info(message),
warn: (message) => console.warn(message),
error: (message) => console.error(message),
};
使用 Record
类型可使代码更加简洁和可读。此外,如果添加或删除了新的日志级别,它还有助于捕获任何错误。例如,如果我决定添加debug
日志级别,TypeScript 将引发错误。
88. 使用 as const
技巧准确地输入你的钩子返回值
假设我们有一个钩子 useIsHovered
来检测 div 元素是否悬停。钩子返回一个用于 div 元素的 ref
和一个指示 div 是否悬停的布尔值。
const useIsHovered = () => {
const ref = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
// TODO : Rest of implementation
return [ref, isHovered]
};
目前,TypeScript 无法正确推断函数返回类型。
您可以通过显式键入返回类型来解决此问题,如下所示:
const useIsHovered = (): [RefObject<HTMLDivElement>, boolean] => {
// TODO : Rest of implementation
return [ref, isHovered]
};
或者你可以使用 as const
技巧来准确键入返回值:
const useIsHovered = () => {
// TODO : Rest of implementation
return [ref, isHovered] as const;
};
89. Redux:通过参考 Usage with TypeScript | React Redux 确保正确输入您的 Redux state 和帮助程序
我喜欢使用 Redux 来管理繁重的客户端状态。它也适用于 TypeScript。你可以在这里找到有关如何将 Redux 与 TypeScript 一起使用的优秀指南。
90. 使用 ComponentType
简化类型
假设您正在设计一个像 Figma 这样的应用程序(我知道,您雄心勃勃😅)。该应用程序由小部件组成,每个小部件接受一个size
。要重用逻辑,我们可以定义一个共享的 WidgetWrapper
组件,该组件接受 Widget
类型的 widget,定义如下:
interface Size {
width: number;
height: number
};
interface Widget {
title: string;
Component: ComponentType<{ size: Size }>;
}
WidgetWrapper
组件将呈现 widget 并将相关大小传递给它。
const WidgetWrapper = ({ widget }: { widget: Widget }) => {
const { Component, title } = widget;
const { onClose, size, onResize } = useGetProps(); //TODO: better name but you get the idea 😅
return (
<Wrapper onClose={onClose} onResize={onResize}>
<Title>{title}</Title>
{/* We can render the component below with the size */}
<Component size={size} />
</Wrapper>
);
};
91. 使用 TypeScript 泛型使您的代码更具可重用性
如果你没有使用 TypeScript 泛型,则只会发生两种情况:
- 您要么正在编写非常简单的代码
- 您错过了
TypeScript 泛型使您的代码更具可重用性和灵活性。
例如,假设我在博客上有不同的项目(例如,Post
、Follower
等),并且我想要一个通用列表组件来显示它们。
export interface Post {
id: string;
title: string;
contents: string;
publicationDate: Date;
}
export interface User {
username: string;
}
export interface Follower extends User {
followingDate: Date;
}
每个列表都应该是可排序的。有个坏方法和好方法。
❌ Bad:我创建了一个接受项联合的 list 组件。这很糟糕,因为:
- 每次添加新项时,都必须更新函数/类型。
- 该函数不完全是类型安全的。
- 此代码依赖于其他文件(例如:
FollowerItem
、PostItem
)。
import { FollowerItem } from "./FollowerItem";
import { PostItem } from "./PostItem";
import { Follower, Post } from "./types";
type ListItem = { type: "follower"; follower: Follower } | { type: "post"; post: Post };
function ListBad({
items,
title,
vertical = true,
ascending = true,
}: {
title: string;
items: ListItem[];
vertical?: boolean;
ascending?: boolean;
}) {
const sortedItems = [...items].sort((a, b) => {
const sign = ascending ? 1 : -1;
return sign * compareItems(a, b);
});
return (
<div>
<h3 className="title">{title}</h3>
<div className={`list ${vertical ? "vertical" : ""}`}>
{sortedItems.map((item) => (
<div key={getItemKey(item)}>{renderItem(item)}</div>
))}
</div>
</div>
);
}
function compareItems(a: ListItem, b: ListItem) {
if (a.type === "follower" && b.type === "follower") {
return (
a.follower.followingDate.getTime() - b.follower.followingDate.getTime()
);
} else if (a.type == "post" && b.type === "post") {
return a.post.publicationDate.getTime() - b.post.publicationDate.getTime();
} else {
// This shouldn't happen
return 0;
}
}
function getItemKey(item: ListItem) {
switch (item.type) {
case "follower":
return item.follower.username;
case "post":
return item.post.id;
}
}
function renderItem(item: ListItem) {
switch (item.type) {
case "follower":
return <FollowerItem follower={item.follower} />;
case "post":
return <PostItem post={item.post} />;
}
}
相反,我们可以使用 TypeScript 泛型来创建更具可重用性和类型安全性的列表组件。
我在下面的👇沙盒中做了一个例子。
92. 确保使用 NoInfer
实用程序类型进行精确键入
想象一下你正在开发一个视频游戏🎮。游戏有多个地点(例如 LeynTir
、Forin
、Karin
等)。您想要创建一个函数,将玩家传送到新位置。
function teleportPlayer<L extends string>(
position: Position,
locations: L[],
defaultLocation: L,
) : L {
// Teleport the player and return the location
}
该函数将按以下方式调用:
const position = { x: 1, y: 2, z: 3 };
teleportPlayer(position, ["LeynTir", "Forin", "Karin"], "Forin");
teleportPlayer(position, ["LeynTir", "Karin"], "anythingCanGoHere"); // ❌ This will work, but it is wrong since "anythingCanGoHere" shouldn't be a valid location
第二个示例无效,因为 anythingCanGoHere
不是有效位置。
但是,TypeScript 不会引发错误,因为它从列表中推断出 L
的类型和默认位置。
要解决此问题,请使用 NoInfer
实用程序类型。
function teleportPlayer<L extends string>(
position: Position,
locations: L[],
defaultLocation: NoInfer<L>,
) : NoInfer<L> {
// Teleport the player and return the location
}
现在 TypeScript 会抛出一个错误:
teleportPlayer(position, ["LeynTir", "Karin"], "anythingCanGoHere"); // ❌ Error: Argument of type '"anythingCanGoHere"' is not assignable to parameter of type '"LeynTir" | "Karin"
使用 NoInfer
实用程序类型可确保默认位置必须是列表中提供的有效位置之一,从而防止无效输入。
93. 使用 ElementRef
类型帮助程序轻松键入 refs
有一种简单的方法来键入 refs。困难的方法是记住元素的类型名称并直接🤣使用它。
const ref = useRef<HTMLDivElement>(null);
最简单的方法是使用 ElementRef
类型帮助程序。更直接,因为您应该已经知道元素的名称。
const ref = useRef<ElementRef<"div">>(null);
类别 #13:杂项提示 🎉
94. 使用 eslint-plugin-react
和 Prettier 提高代码的质量和安全性。
如果你不使用 eslint-plugin-react😅,你就不能认真对待 React。它可以帮助您捕获潜在的错误并实施最佳实践。因此,请确保为您的项目安装和配置它。您还可以使用 Prettier 自动格式化代码并确保您的代码库一致。
95. 使用 Sentry 或 Grafana Cloud Frontend Observability 等工具记录和监控您的应用程序。
你无法改进你不衡量📏的东西。如果您正在为生产应用程序寻找监控工具,请查看 Sentry 或 Grafana Cloud Frontend Observability。
96. 使用 Code Sandbox 或 Stackblitz 等在线 IDE 快速开始编码
设置本地开发环境可能很痛苦。尤其是作为初学者 🐣 。因此,请从 Code Sandbox 或 Stackblitz 等在线 IDE 开始。这些工具允许您快速开始编码,而无需担心设置环境。
97. 寻找高级 react 技能?看看这些书 👇
如果你正在寻找高级 React 书籍📚,我会推荐:
98. 准备 React 面试?检查 reactjs-interview-questions
React 面试⚛️可能很棘手。幸运的是,您可以通过检查此 repo 来为它们做好准备。
99. 向 Nadia、Dan、Josh、Kent 等专家学习 React 最佳实践。
如果您想及时了解最佳实践并学习提示,请务必遵循以下专家:
- @adevnadia: https://x.com/adevnadia for advanced react tips
- @joshwcomeau: https://x.com/joshwcomeau
- @kentcdodds: https://x.com/kentcdodds
- @mattpocockuk: https://x.com/mattpocockuk for TypeScript tips
- @tejaskumar_: https://x.com/TejasKumar_
- @housecor: https://x.com/housecor
- https://x.com/_ndeyefatoudiop
100. 通过订阅 This Week In React 或 ui.dev 等时事通讯,随时了解 React 生态系统的最新动态
React 是一个快速发展的生态系统。有许多工具、库和最佳实践需要跟上。要保持更新,请务必订阅以下时事通讯💌:
101. 在 r/reactjs 等平台上与 React 社区互动
React 社区太棒了。您可以从其他开发人员那里学到很多东西并分享您的知识。因此,请在 r/reactjs 等平台上与社区互动。