本文翻译自 Single Responsibility Principle in React: The Art of Component Focus,主要讨论单一职责原则在 React 中的应用。
单一职责原则指出,一个类应该只有一个引起变化的原因。
以下是一个常见的反模式:
// 不要这样做
const UserProfile = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchUser();
}, []);
const fetchUser = async () => {
try {
const response = await fetch("/api/user");
const data = await response.json();
setUser(data);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
const handleUpdateProfile = async (data: Partial<User>) => {
try {
await fetch("/api/user", {
method: "PUT",
body: JSON.stringify(data),
});
fetchUser(); // 刷新数据
} catch (e) {
setError(e as Error);
}
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<form onSubmit={/* form logic */}>{/* 复杂的表单字段 */}</form>
<UserStats userId={user.id} />
<UserPosts userId={user.id} />
</div>
);
};
这个组件违反了 SRP,因为它负责了以下内容:
让我们将其拆分为专注的组件:
// 数据获取钩子
const useUser = (userId: string) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchUser();
}, [userId]);
const fetchUser = async () => {
try {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
setUser(data);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
return { user, loading, error, refetch: fetchUser };
};
// 展示组件
const UserProfileView = ({
user,
onUpdate,
}: {
user: User;
onUpdate: (data: Partial<User>) => void;
}) => (
<div>
<h1>{user.name}</h1>
<UserProfileForm user={user} onSubmit={onUpdate} />
<UserStats userId={user.id} />
<UserPosts userId={user.id} />
</div>
);
// 容器组件
const UserProfileContainer = ({ userId }: { userId: string }) => {
const { user, loading, error, refetch } = useUser(userId);
const handleUpdate = async (data: Partial<User>) => {
try {
await fetch(`/api/user/${userId}`, {
method: "PUT",
body: JSON.stringify(data),
});
refetch();
} catch (e) {
// 错误处理
}
};
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound message="User not found" />;
return <UserProfileView user={user} onUpdate={handleUpdate} />;
};
当每个组件都有一个明确且单一的职责时,整个应用程序将变得更易于维护、测试和扩展。
正如 Bob 大叔在《Clean Architecture》中强调的那样,关键在于“只有一个引起变化的原因”。这种细微的区分至关重要:
一个组件可能做几件相关的事情,但如果它们都因相同的原因而改变(例如更新用户资料 UI),那么它们可能应该放在一起。
相反,两个看似简单的操作如果因不同的原因而改变(例如用户偏好与认证逻辑),则可能需要分开。
当你发现自己用“和”来描述一个组件的功能时,它可能违反了 SRP。拆分开它!但也要考虑这些部分为何需要改变,以及谁会请求这些改变。