原文地址#
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. ヘルパー関数をコンポーネントの外に移動する#
コンポーネント内部のネストされたヘルパー関数は、コンポーネントを混乱させ、読みづらくする可能性があります。ヘルパー関数をコンポーネントの外に置くことで、可読性が向上し、関心の分離が実現します。
⛔ 閉包が必要ない場合、ヘルパー関数をコンポーネント内にネストすることを避ける
function UserProfile({ user }) {
function formatDate(date) {
return date.toLocaleDateString();
}
return <div>Joined: {formatDate(user.joinDate)}</div>;
}
✅ 推奨 ヘルパー関数をコンポーネントの外に移動する(ファイルを上から下に読みやすくするために)
function formatDate(date) {
return date.toLocaleDateString();
}
function UserProfile({ user }) {
return <div>Joined: {formatDate(user.joinDate)}</div>;
}
4. 設定オブジェクトを使用して重複したマークアップを抽出する#
重複したマークアップをハードコーディングすると、コードのメンテナンスや更新が難しくなります。マッピング / ループと設定オブジェクトを使用して重複したマークアップを抽出することで、コードのメンテナンス性と可読性が向上します。これにより、1 か所(設定オブジェクト内)で変更を行うだけで済むため、更新や追加が簡素化されます。
⛔ 避けるべき 重複したマークアップのハードコーディング
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>
</div>
<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 - オブジェクト vs プリミティブ#
多くのプリミティブを渡すと、コンポーネントが混乱し、関連データの管理が難しくなることがあります。関連する props をオブジェクトにグループ化することで、コンポーネントのインターフェースを簡素化し、可読性を向上させることができます。これは、関連データを論理的にグループ化することで、コードをより明確で理解しやすくします。
⛔ 避けるべき 関連する props を渡す際のプリミティブの使用
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>
);
}
✅ 推奨 オブジェクトを渡して 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. HOC やレンダープロップよりも Hooks を優先する#
高階コンポーネント(HOCs)やレンダープロップパターンは、コンポーネント間でロジックや動作を共有するために伝統的に使用されてきました。しかし、これらのパターンは複雑でネストが深いコンポーネントツリーを引き起こし、コードの可読性、デバッグ、メンテナンスを難しくする可能性があります。Hooks は、関数コンポーネント内でロジックをカプセル化し再利用するためのより簡潔で宣言的な方法を提供します。
Hooks は、外部のロジックや動作にアクセスするために一連の関数を組み合わせることで、よりシンプルな思考モデルを提供し、全体の JSX テンプレートを読みやすく理解しやすくします。
⛔ 避けるべき HOCs やレンダープロップの使用
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>;
}
// または別のコンポーネントを使用
function ProfileDetails({ user }) {
return <div>{user.name} - {user.age}</div>;
}
function UserProfile({ user }) {
return (
<div>
{renderProfileDetails(user)}
// または
<ProfileDetails user={user} />;
</div>
);
}
14. エラーバウンダリを使用する#
未処理のエラーはアプリケーション全体をクラッシュさせ、ユーザーエクスペリエンスに影響を与える可能性があります。エラーバウンダリは、エラーを優雅にキャッチして処理することを可能にし、アプリケーションの弾力性を向上させます。これにより、アプリケーション全体がクラッシュするのではなく、代替のユーザーインターフェース(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>
);
}