译:React 中的单一职责原则:组件关注点的艺术

原文: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),因为它负责:

  1. 数据获取
  2. 错误处理
  3. 加载状态
  4. 表单处理
  5. 布局和展示

更好的方式:关注点分离

让我们将其拆分为专注的组件:

// 数据获取钩子
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} />;
};

关键要点

  1. 分离数据和展示 – 使用钩子处理数据,使用组件处理 UI
  2. 创建专注的组件 – 每个组件应该做好一件事
  3. 使用组合构建复杂功能的简单部分
  4. 将可复用逻辑 提取到自定义 hooks 中
  5. 分层思考 – 数据层、业务逻辑层、展示层

结论

当每个组件都有一个单一、明确的职责时,你的整个应用程序变得更加可维护、可测试和灵活。

正如 Uncle Bob 在 Clean Architecture 中强调的,关键在于有一个 变化的单一原因。这个细微的区别至关重要:

  • 一个组件可能做几件相关的事情,但如果它们都因为同一个原因而变化(比如更新用户资料界面),它们可能属于一起
  • 相反,两个看似简单的操作可能需要分开,如果它们因为不同的原因而变化(比如用户偏好与认证逻辑)

专业提示:当你发现自己用“和”来描述一个组件做的事情时,它可能违反了单一职责原则。将它分开!但也要考虑这些部分为什么需要变化,以及谁会请求这些变化。