Frontend Development 6 min read

Applying the Single Responsibility Principle in React: Refactor Your Components

This article explains how the Single Responsibility Principle applies to React components, shows a common anti‑pattern with a monolithic component, and demonstrates a cleaner architecture using custom hooks, presentation components, and container components for better maintainability.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
Applying the Single Responsibility Principle in React: Refactor Your Components

This article is translated from "Single Responsibility Principle in React: The Art of Component Focus" and discusses how to apply SRP in React.

The Single Responsibility Principle states that a class should have only one reason to change.

Problems of Multiple Responsibilities

Here is a common anti‑pattern:

<code>// 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>
  );
};</code>

This component violates SRP because it handles:

Data fetching

Error handling

Loading state

Form handling

Layout and presentation

Better Approach: Separation of Concerns

Let's split it into focused components:

<code>// Data fetching hook
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 };
};

// Presentation component
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>
);

// Container component
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) {
      // error handling
    }
  };
  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <NotFound message="User not found" />;
  return <UserProfileView user={user} onUpdate={handleUpdate} />;
};</code>

Key Takeaways

Separate data and presentation – use hooks for data, components for UI.

Create focused components – each component should do one thing well.

Use composition – build complex features from simple parts.

Reuse logic – extract reusable logic into custom hooks.

Think in layers – data layer, business logic layer, presentation layer.

Conclusion

When every component has a clear, single responsibility, the application becomes easier to maintain, test, and extend.

As emphasized by Bob Uncle in "Clean Architecture," the key is that there is only one reason for change. This subtle distinction is crucial: a component may do several related tasks if they change for the same reason, but should be split when different reasons cause changes.

If you find yourself describing a component's functionality with "and," it likely violates SRP – split it, while still considering why each part changes and who requests those changes.

Reactfrontend architectureHookscomponent designSingle Responsibility Principle
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.