Reactと関数の関係から良いコンポーネントを考える
Reactと関数の関係
Reactは、純関数であることを仮定して設計されているUIライブラリです。
であれば、Reactのpropsは関数の引数、ReactのUIは関数の戻り値と考えることができると思います。
したがって、良い関数の定義と良いコンポーネントの定義は似ているのではないかという仮説を軸に、良いコンポーネントを考察します。
良い関数の定義から導いた良いコンポーネント
- function定義
- propsを明示的に書く
- コンポーネント名から何を表示するかわかる
- コンポーネントが単一の役割を持つ
- propsが多すぎない
順に解説します。
function定義
Reactではコンポーネントをfunctionで定義するか、アロー関数で定義するかの選択肢があります。
僕は、Reactは純関数であることを仮定して設計されているということからfunctionを使ったほうが明示的に関数と示せる点で良いかと考えています。
また、functionを使ったほうが良いと思う理由には以下の点があります。
- わずかにfunctionのほうがパフォーマンスが良い
- ReactやVercelなどの公式サンプルコードではfunctionを採用している
function定義とアロー関数についての詳細な解説は、以下の記事をご参照ください。
propsを明示的に書く
関数では、引数を明示的に示しているので、propsの中身を明示的に書いたほうが関数に近づくと思いました。
以下が例です。
import React from 'react';
interface MyComponentProps {
name: string;
age: number;
}
function MyComponent({ name, age }: MyComponentProps) {
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
</div>
);
}
export default MyComponent;
import React from 'react';
interface MyComponentProps {
name: string;
age: number;
}
function MyComponent(props: MyComponentProps) {
return (
<div>
<p>Name: {props.name}</p>
<p>Age: {props.age}</p>
</div>
);
}
export default MyComponent;
前者のほうが可読性が高いと思いますし、不要なpropsの混入を防げるのが良いです。
コンポーネント名から何を表示するかわかる
コンポーネント名がわかりやすいと、そのコンポーネントが何を表示するのか簡単にイメージできます。
例えば、ListComponent
という名前だと、何のリストを表示するのかが不明瞭です。
しかし、UserList
と命名すれば、そのコンポーネントが「ユーザーのリスト」を表示することが明確になります。これにより、他の開発者や自分がコードを見たときに理解しやすくなり、メンテナンス性が向上します。
コンポーネントが単一の役割を持つ
コンポーネントも関数と同様に、単一の役割を持たせることが重要です。
そのため、適切にコンポーネントを分割する必要があります。また、これにより前述の「コンポーネント名から何を表示するかわかる」にも繋がり、より具体的で意味のあるコンポーネント名を付けることができるようになります。
例として、以下のように1つのコンポーネントに複数の役割を持たせると、複雑で管理しにくくなります。
function UserProfile() {
const [showDetails, setShowDetails] = React.useState(false);
const handleToggle = () => {
setShowDetails(!showDetails);
};
return (
<div>
<h2>User Profile</h2>
{/* ユーザー情報を表示 */}
<div>
<p>Name: John Doe</p>
<p>Email: john.doe@example.com</p>
</div>
{/* ログインフォーム (別の役割を担う部分) */}
<form>
<input type="text" placeholder="Username" />
<input type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
{/* ユーザー詳細の表示を切り替える */}
<button onClick={handleToggle}>
{showDetails ? 'Hide Details' : 'Show Details'}
</button>
{/* ユーザーの詳細情報 */}
{showDetails && (
<div>
<p>Address: 123 Main St</p>
<p>Phone: 555-1234</p>
</div>
)}
</div>
);
}
// ユーザー情報の表示に特化したコンポーネント
function UserInfo({ name, email }: { name: string; email: string }) {
return (
<div>
<p>Name: {name}</p>
<p>Email: {email}</p>
</div>
);
}
// ログインフォームの表示に特化したコンポーネント
function LoginForm() {
return (
<form>
<input type="text" placeholder="Username" />
<input type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
// ユーザー詳細の表示に特化したコンポーネント
function UserDetails({ address, phone }: { address: string; phone: string }) {
return (
<div>
<p>Address: {address}</p>
<p>Phone: {phone}</p>
</div>
);
}
// 親コンポーネントで、他のコンポーネントを組み合わせて使用
function UserProfile() {
const [showDetails, setShowDetails] = React.useState(false);
const handleToggle = () => {
setShowDetails(!showDetails);
};
return (
<div>
<h2>User Profile</h2>
{/* ユーザー情報 */}
<UserInfo name="John Doe" email="john.doe@example.com" />
{/* ログインフォーム */}
<LoginForm />
{/* ユーザー詳細の表示切り替え */}
<button onClick={handleToggle}>
{showDetails ? 'Hide Details' : 'Show Details'}
</button>
{/* ユーザーの詳細情報を表示 */}
{showDetails && <UserDetails address="123 Main St" phone="555-1234" />}
</div>
);
}
コンポーネント分割のメリット
-
テストが容易になる
各コンポーネントが単一の役割を持つため、個別にテストしやすくなります。 -
可読性・再利用性の向上
役割ごとにコンポーネントが分かれていることで、コードの可読性が高まり、再利用も容易になります。 -
メモ化がしやすい
コンポーネントごとにメモ化を行うことで、パフォーマンスの向上が期待できます。
例えば、ログインフォームの操作によってページ全体が再レンダリングされるのを防ぎ、ログインフォームだけを効率的に再レンダリングさせることが可能になります。 -
propsの数を減らす事ができる
後述する「propsが多すぎない」の項目で説明します。
propsが多すぎない
propsが少ないということはコンポーネントが分割されているということであり、前述した「コンポーネント分割のメリット」を享受できるということでもあります。
propsを少なくするためのテクニックを紹介します。
- propsをオブジェクトにする
- コンポーネントを分割する
- デフォルト値を使う
1. propsをオブジェクトにする
以下のようにまとめて問題ない値はpropsをオブジェクトにして良いと思います。
// 悪い例
<UserProfile name="John" age={30} email="john@example.com" />
// 良い例
const user = { name: 'John', age: 30, email: 'john@example.com' };
<UserProfile user={user} />
2. コンポーネントを分割する
「単一の役割を持つ」でも紹介しましたが、コンポーネントを分割することで、propsを減らすこともできます。
// 悪い例(多くのpropsを受け取るコンポーネント)
function UserCard({ name, age, email, address, phoneNumber }) {
// 処理
}
// 良い例(サブコンポーネントに分割)
function UserCard({ user }) {
return (
<div>
<UserInfo name={user.name} age={user.age} />
<UserContact email={user.email} phoneNumber={user.phoneNumber} />
</div>
);
}
3. デフォルト値を使う
以下の例のようにすると、propsを渡さなかったときに、デフォルト値を使うことができます。
ボタンやチェックボックスやラベルなどのアトム層のコンポーネントでデフォルトの大きさを指定するときなどに頻出です。
function Button({ color = 'blue', label = 'Click me' }) {
return <button style={{ color }}>{label}</button>;
}
<テクニック紹介>似たUIのpropsの条件分岐
これまでコンポーネントを分割することを推奨してきましたが、UIがほぼ一緒の場合、propsの条件分岐の方が管理が楽な場合もあります。
propsの条件分岐を用いる際、表示される内容が明確になるテクニックを紹介します。
例えば、型システムを活用したタグ付きユニオンを使うと、何が表示されるかを明示的にして、可読性を向上させることができます。
以下の例では、ユーザーアイテムや商品アイテムを表示するコンポーネントを定義しています。それぞれのpropsを明示的に分けることで、コンポーネントのイメージがつきやすくなっています。
import React from 'react';
// ユーザーアイテムのprops
interface UserListItemProps {
type: 'user';
userName: string;
}
// 商品アイテムのprops
interface ProductListItemProps {
type: 'product';
productName: string;
}
// 2つのタイプをまとめた型を定義
type ListItemProps = UserListItemProps | ProductListItemProps;
// アイテムコンポーネントの実装
function ListItem(props: ListItemProps) {
const name = props.type === 'user' ? props.userName : props.productName;
return (
<div>
<h3>{name}</h3>
</div>
);
}
// Appコンポーネント例: 2つのタイプのアイテムをリスト表示
function App() {
const items: ListItemProps[] = [
{ type: 'user', userName: 'John Doe' },
{ type: 'product', productName: 'Laptop' },
{ type: 'user', userName: 'Jane Smith' },
{ type: 'product', productName: 'Smartphone' },
];
return (
<div>
<h1>Item List</h1>
{items.map((item, index) => (
<ListItem key={index} {...item} />
))}
</div>
);
}
export default App;
上記の例では、nameに値を入れて使用することもできますが、あえてtypeというタグで分岐しています。これにより、どんなコンポーネントかを明示的に示すことができます。
また、UserListItem
、ProductListItem
でコンポーネントを分けてもよいですが、UIがほぼ一緒で、条件分岐も複雑でない場合、上記のようにpropsの型を利用したUI分岐のほうが管理が楽です。
最後に
この記事では、Reactと関数の関係から良いコンポーネント設計について考察しました。
Reactの公式ドキュメントには、コンポーネントやpropsの設計についての具体的な指針が明確に示されていないため、このような考察を行うことは有意義だと感じています。
これからも、より良いReactのコードを書けるよう、引き続き設計や実装について考察していきたいと思います。
Discussion