原文地址#
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>
);
}