原文:https://cekrem.github.io/posts/single-responsibility-principle-in-react/
作者:Cekrem
译者:ChatGPT 4 Turbo
引言
我们已经讨论过依赖倒置、接口隔离、里氏替换和开闭原则。现在是时候讨论 SOLID 的基础:单一职责原则(SRP)了。
再次感谢 Uncle Bob 在他的经典作品《Clean Architecture》中提醒我良好软件架构的重要性!那本书是本系列的主要灵感来源。
单一职责原则指出,一个类应该只有一个改变的理由。
多重职责的问题
这里有一个常见的反模式:
// DON'T DO THIS
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(); // Refresh data
} 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 */}>{/* Complex form fields */}</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} />;
};
关键要点
- 分离数据和展示 – 使用钩子处理数据,使用组件处理 UI
- 创建专注的组件 – 每个组件应该做好一件事
- 使用组合构建复杂功能的简单部分
- 将可复用逻辑 提取到自定义 hooks 中
- 分层思考 – 数据层、业务逻辑层、展示层
结论
当每个组件都有一个单一、明确的职责时,你的整个应用程序变得更加可维护、可测试和灵活。
正如 Uncle Bob 在 Clean Architecture 中强调的,关键在于有一个 变化的单一原因。这个细微的区别至关重要:
- 一个组件可能做几件相关的事情,但如果它们都因为同一个原因而变化(比如更新用户资料界面),它们可能属于一起
- 相反,两个看似简单的操作可能需要分开,如果它们因为不同的原因而变化(比如用户偏好与认证逻辑)
专业提示:当你发现自己用“和”来描述一个组件做的事情时,它可能违反了单一职责原则。将它分开!但也要考虑这些部分为什么需要变化,以及谁会请求这些变化。