原文地址#
https://thetshaped.dev/p/15-react-component-principles-for-better-design
本文只翻译主要内容,当然这都是 微软翻译 和 ChatGPT (GPT-4o) 的功劳。
介绍#
该系列文章旨在弥合 React 初学者与成长为 React 专家和工程师的人之间的差距。
Caution
这不是初学者指南,因此大多数共享概念都需要一些 React 知识。
Tip
把一切都当作意见。软件可以通过多种方式构建。
1. 优先使用函数组件而不是类组件#
类组件可能很冗长且更难管理。函数组件更简单,更易于理解。使用函数组件,您可以获得更好的可读性。与类组件的状态管理、生命周期方法等相比,您必须记住和思考的事情要小得多。
唯一使用类组件的例外是使用错误边界(Error Boundaries)。
⛔ 避免 使用类组件
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment() {
this.setState(state => ({
count: state.count + 1
}));
}
return (
<div>
Count: {this.state.count}
<button onClick={() => this.increment()}>Increment</button>
</div>
);
}
✅ 首选 使用函数组件
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
Count: {count}
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
2. 命名组件#
无名称组件会使调试变得困难并降低代码可读性。命名组件改进了堆栈跟踪,并使您的代码库更易于导航、管理和理解。使用命名组件时,可以更轻松地在错误之间导航。
⛔ 避免 使用无名组件
export default () => <div>Details</div>;
✅ 首选 命名组件
export default function UserDetails() {
return <div>User Details</div>;
}
3. 将 Helper Functions 移到组件外部#
组件内部的嵌套 helper functions 可能会使组件混乱并使其更难阅读。将 helper functions 放在组件之外可提高可读性并分离关注点。
⛔ 如果不需要闭包,请避免将帮助程序函数嵌套在组件中
function UserProfile({ user }) {
function formatDate(date) {
return date.toLocaleDateString();
}
return <div>Joined: {formatDate(user.joinDate)}</div>;
}
✅ 首选 将这些 helper functions 移到组件外部(放在组件之前,以便您可以从上到下读取文件)
function formatDate(date) {
return date.toLocaleDateString();
}
function UserProfile({ user }) {
return <div>Joined: {formatDate(user.joinDate)}</div>;
}
4. 使用配置对象提取重复标记#
对重复标记进行硬编码使代码更难维护和更新。使用映射 / 循环和配置对象提取重复标记使代码更易于维护和可读。它简化了更新和添加,因为只需在一个地方(在配置对象内)进行更改。
⛔ 避免 对重复标记进行硬编码
function ProductList() {
return (
<div>
<div>
<h2>Product 1</h2>
<p>Price: $10</p>
</div>
<div>
<h2>Product 2</h2>
<p>Price: $20</p>
</div>
<div>
<h2>Product 3</h2>
<p>Price: $30</p>
</div>
</div>
);
}
✅ 首选 使用配置对象和循环提取重复标记
const products = [
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 20 },
{ id: 3, name: 'Product 3', price: 30 }
];
function ProductList() {
return (
<div>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>Price: ${product.price}</p>
</div>
))}
</div>
);
}
5. 管理组件的大小#
大型和冗长的组件可能难以理解、维护和测试。更小、更集中的组件更易于阅读、测试和维护。每个组件都有一个单一的责任,即更改和重新渲染的理由,使代码库更加模块化且更易于管理。
⛔ 避免 使用大而令人讨厌的组件
function UserProfile({ user }) {
return (
<div>
<div>
<img src={user.avatar} alt={`${user.name}'s avatar`} />
<h2>{user.name}</h2>
</d
<div>
<h3>Contact</h3>
<p>Email: {user.email}</p>
<p>Phone: {user.phone}</p>
</div>
</div>
);
}
✅ 首选 小巧易读的组件
function UserProfile({ user }) {
return (
<div>
<ProfileHeader avatar={user.avatar} name={user.name} />
<ProfileContact email={user.email} phone={user.phone} />
</div>
);
}
6. 解构 Props#
重复的 props 会使组件更难阅读和维护。解构 props 可以提高可读性并使代码更加简洁。它减少了重复,并清楚地表明使用了哪些 props。
⛔ 避免 在组件中到处重复 props
function UserProfile(props) {
return (
<>
<div>Name: {props.name}</div>
<div>Email: {props.email}</div>
</>
);
}
✅ 首选 解构你的 props
function UserProfile({ name, email }) {
return (
<>
<div>Name: {name}</div>
<div>Email: {email}</div>
</>
);
}
7. 管理 props 的数量#
拥有太多的 props 会使组件变得复杂且难以理解。更少的 props 使组件更易于使用和理解。
大多数时候,当我们有一个 > 5 个 props 的组件时,这表明它可以被拆分。但这并不是一个很难遵循的规则,因为作为一个 “好” 的例子,input
字段有很多 props,但不需要拆分。
当我们有 < 5 个 props 时,这表明可以提取某些东西。也许我们在单个组件中包含了太多数据。
Important
更少的 props ⇒ 更少的更改和重新渲染的理由。
⛔ 避免 使用很多 props(也许 > 5,你应该拆分它,但并非总是如此,例如: input
)
function UserProfile({
name, email, avatarUrl, address, paymentProfiles
}) {
return (
<div>
<img src={avatarUrl} alt={`${name}'s avatar`} />
<h1>{name}</h1>
<p>Email: {email}</p>
<p>Address: {address}</p>
<ul>
{paymentProfiles.map(paymentProfile => (
<li key={paymentProfile.id}>
<h2>{paymentProfile.cardNumber}</h2>
<p>{paymentProfile.cardName}</p>
</li>
))}
</ul>
</div>
);
}
✅ 首选 使用少量 props(可能 < 5 个)
function UserProfile({ user }) {
return (
<Info name={user.name} email={user.email} avatarUrl={user.avatarUrl} />
<Address address={user.address} />
<PaymentProfiles paymentProfiles={user.paymentProfiles} />
);
}
8. Props - Objects vs Primitives#
传递许多 primitives 可能会使组件混乱,并使管理相关数据变得更加困难。将相关 props 分组到一个 Object 中可以简化组件界面并提高可读性。它通过对相关数据进行逻辑分组,使代码更清晰、更易于理解。
⛔ 避免 当 props 相关时传递 primitives
function Address({ street, city, state, zip }) {
return (
<div>
<p>Street: {street}</p>
<p>City: {city}</p>
<p>State: {state}</p>
<p>ZIP: {zip}</p>
</div>
);
}
✅ 首选 传递一个 Object,对 props 进行分组
function Address({ address }) {
const { street, city, state, zip } = address;
return (
<div>
<p>Street: {street}</p>
<p>City: {city}</p>
<p>State: {state}</p>
<p>ZIP: {zip}</p>
</div>
);
}
9. 管理三元运算符#
嵌套的三元运算符会使代码难以阅读和维护。清晰的 if-else 语句可以提高代码的可读性和可维护性,使控制流更容易理解和调试。
⛔ 避免 嵌套或多个三元运算符 - 难以阅读和遵循
function Greeting({ isLoggedIn, age }) {
return (
<div>
{isLoggedIn ? (
age > 18 ? (
"Welcome back!"
) : (
"You are underaged!"
)
) : (
"Please log in."
)}
</div>
);
}
✅ 首选 组件中的 if-else 块和显式 return 语句
function Greeting({ isLoggedIn, age }) {
if (!isLoggedIn) {
return <div>Please log in.</div>;
}
if (age > 18) {
return <div>Welcome back!</div>;
}
return <div>You are underaged!</div>;
}
10. 抽象列表 map 到单独的组件中#
直接在 return 语句中对列表进行 map 会使组件显得杂乱且难以阅读。将 map 操作从主组件中分离出来,放到单独的组件中,可以使代码更简洁、更易读。主组件的样板代码变得更简单,并且将渲染逻辑与组件的主体结构分离,提升了可读性。
Note
主要组件不关心细节。
⛔ 避免 对组件内的列表使用 map 函数
function PaymentProfilesPage({ paymentProfiles }) {
return (
<h1>Payment Profiles:</h1>
<ul>
{paymentProfiles.map(paymentProfile => (
<li key={paymentProfile.id}>
<h2>{paymentProfile.cardNumber}</h2>
<p>{paymentProfile.cardName}</p>
</li>
))}
</ul>
);
}
✅ 首选 将 map 函数移到组件之外 - 易于阅读。主要组件不关心细节。
function PaymentProfilesPage({ paymentProfiles }) {
return (
<h1>Payment Profiles:</h1>
<PaymentProfilesList paymentProfiles={paymentProfiles} />
);
}
11. 优先使用 Hooks 而不是 HOC 和 Render Props#
高阶组件(HOCs)和渲染属性模式(render props)传统上用于在组件之间共享逻辑和行为。然而,这些模式可能会导致复杂且嵌套较深的组件树,使代码更难以阅读、调试和维护。Hooks 提供了一种更简洁和声明式的方法来在函数组件中封装和重用逻辑。
Hooks 提供了一个更简单的思维模型 —— 我们通过组合一组函数来访问外部逻辑和行为,使整个 JSX 模板更易于阅读和理解。
⛔ 避免 使用 HOCs 和 render props
function UserProfileForm() {
return (
<Form>
{({ values, handleChange }) => (
<input
value={values.name}
onChange={e => handleChange('name', e.target.value)}
/>
<input
value={values.password}
onChange={e => handleChange('password', e.target.value)}
/>
)}
</Form>
);
}
✅ 首选 使用 hooks
function UserProfileForm() {
const { values, handleChange } = useForm();
return (
<Form>
<input
value={values.name}
onChange={e => handleChange('name', e.target.value)}
/>
<input
value={values.password}
onChange={e => handleChange('password', e.target.value)}
/>
</Form>
);
}
12. 使用自定义 Hooks 重用和封装逻辑#
重复逻辑会导致代码冗余,并使维护更加困难。自定义钩子允许代码重用,使组件更简洁、更易于维护。使用自定义钩子,可以封装逻辑,从而减少重复并提高可读性。它还使其测试变得更加容易。
⛔ 避免 在多个组件中重复逻辑
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(response => response.json())
.then(data => setUsers(data));
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products')
.then(response => response.json())
.then(data => setProducts(data));
}, []);
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
✅ 首选 使用自定义 hooks 封装和重用逻辑
function useFetch(url) {
const [data, setData] = useState([]);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setData(data));
}, [url]);
return data;
}
function UserList() {
const users = useFetch('/api/users');
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function ProductList() {
const products = useFetch('/api/products');
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
13. 提取渲染函数#
在组件内部嵌套复杂的渲染函数会使组件显得杂乱,且难以阅读、测试和维护。将渲染函数定义在组件外部或使用独立的组件可以提高可读性和可维护性。这种做法使主组件保持简洁,并专注于其主要功能。
⛔ 避免 将渲染函数嵌套在组件中
function UserProfile({ user }) {
function renderProfileDetails() {
return <div>{user.name} - {user.age}</div>;
}
return <div>{renderProfileDetails()}</div>;
}
✅ 首选 在组件之外提取渲染函数 - 在组件之上或单独的组件上。
function renderProfileDetails(user) {
return <div>{user.name} - {user.age}</div>;
}
// OR using a separate component
function ProfileDetails({ user }) {
return <div>{user.name} - {user.age}</div>;
}
function UserProfile({ user }) {
return (
<div>
{renderProfileDetails(user)}
// OR
<ProfileDetails user={user} />;
</div>
);
}
14. 使用错误边界 (Error Boundaries)#
未处理的错误可能会导致整个应用程序崩溃,从而影响用户体验。错误边界(Error Boundaries)可以让你优雅地捕获和处理错误,提高应用程序的弹性。这确保了更好的用户体验,通过显示备用的用户界面(UI),而不是让整个应用程序崩溃。
⛔ 避免 让子组件中的错误导致整个应用程序崩溃
function App() {
return <UserProfile />;
}
✅ 首选 使用错误边界来捕获和处理子组件树中的错误
function App() {
return (
<ErrorBoundary>
<UserProfile />
</ErrorBoundary>
);
}
15. 使用 Suspense#
手动管理加载状态可能会重复且容易出错。Suspense 通过提供声明式的方法来管理加载状态,简化了异步操作的处理。这减少了样板代码,使组件逻辑更简洁。
⛔ 避免 手动处理异步操作的加载状态
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
✅ 首选 使用 Suspense 优雅地处理异步操作和加载状态
import { UserProfile } from './UserProfile';
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
);
}