Skip to main content
Saved

Single Responsibility

Category Fundamentals

Level Beginner

Examples

React

Ensure each component, module, or function has one clear purpose and reason to change.

In this article
  1. Problem
  2. Solution
  3. Example
  4. Before: Multiple Responsibilities
  5. After: Single Responsibilities
  6. At the Function Level
  7. Benefits
  8. Tradeoffs
  9. Summary
  10. See also

Single Responsibility

Problem

We’ve all seen the 1,500-line component that fetches data, validates inputs, formats dates, manages auth state, and renders six different UI sections. When you need to change how a date displays, you’re scrolling past authentication logic and hoping you don’t break something.

I call these “kitchen sink” components, and they’re painful to work with. Testing email validation means mocking the API, setting up authentication, and rendering the entire component just to check if ”@” is present.

It gets worse as your team grows. Two developers can’t work on the same feature without constant merge conflicts. A bug fix in validation logic somehow breaks navigation. Nobody wants to touch the file because everyone’s been burned before.

Solution

I follow a simple rule: if you can’t describe what a function or component does in one sentence without using “and,” it’s doing too much. A function that fetches user data should only fetch it, without validating or formatting it. A component that renders a form should just render, receiving data through props and emitting events when users interact.

When you follow this rule, each kind of change stays in one file, whether it’s a change to the API response, a new validation rule, or a new button style.

A quick check is to describe each of your functions in a single sentence: “This function validates email addresses.” “This component renders a user avatar.” If the sentence keeps needing “and,” the code probably needs splitting.

Example

The before-and-after below shows a component doing too much, then the same component broken into focused pieces.

Before: Multiple Responsibilities

// ❌ Everything crammed into one component
function UserProfile({ userId, currentUserId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [errors, setErrors] = useState({});

  // Data fetching
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => { setUser(data); setLoading(false); });
  }, [userId]);

  // Validation
  const validateForm = (data) => {
    const errs = {};
    if (!data.email.includes('@')) errs.email = 'Invalid email';
    if (data.age < 18) errs.age = 'Must be 18+';
    return errs;
  };

  // Formatting
  const formatDate = (date) => new Date(date).toLocaleDateString('en-US');

  // Auth logic
  const canEdit = () => user.role === 'admin' || user.id === currentUserId;

  // Submit handler
  const handleSubmit = async (e) => {
    e.preventDefault();
    const validationErrors = validateForm(user);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    await fetch(`/api/users/${userId}`, { method: 'PUT', body: JSON.stringify(user) });
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div className="profile">
      <h1>{user.name}</h1>
      <p>Joined: {formatDate(user.createdAt)}</p>
      {canEdit() && (
        <form onSubmit={handleSubmit}>
          <input value={user.email} onChange={e => setUser({...user, email: e.target.value})} />
          {errors.email && <span>{errors.email}</span>}
          <button>Save</button>
        </form>
      )}
    </div>
  );
}

After: Single Responsibilities

// ✅ Hook: data fetching only
function useUser(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => { setUser(data); setLoading(false); });
  }, [userId]);

  return { user, loading };
}

// ✅ Pure functions: one job each
const validateUserForm = (data) => {
  const errors = {};
  if (!data.email?.includes('@')) errors.email = 'Invalid email';
  if (data.age < 18) errors.age = 'Must be 18+';
  return errors;
};

const formatDate = (date) => new Date(date).toLocaleDateString('en-US');
const canEditProfile = (user, currentUserId) => user.role === 'admin' || user.id === currentUserId;
const updateUser = (userId, data) => fetch(`/api/users/${userId}`, { method: 'PUT', body: JSON.stringify(data) });

// ✅ Component: display only
function UserInfo({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Joined: {formatDate(user.createdAt)}</p>
    </div>
  );
}

// ✅ Component: form only
function UserEditForm({ user, errors, onSubmit, onChange }) {
  return (
    <form onSubmit={onSubmit}>
      <input value={user.email} onChange={e => onChange('email', e.target.value)} />
      {errors.email && <span>{errors.email}</span>}
      <button type="submit">Save</button>
    </form>
  );
}

// ✅ Container: coordinates the pieces
function UserProfile({ userId, currentUserId }) {
  const { user, loading } = useUser(userId);
  const [editedUser, setEditedUser] = useState(null);
  const [errors, setErrors] = useState({});

  useEffect(() => { if (user) setEditedUser(user); }, [user]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const validationErrors = validateUserForm(editedUser);
    if (Object.keys(validationErrors).length > 0) {
      setErrors(validationErrors);
      return;
    }
    await updateUser(userId, editedUser);
  };

  if (loading || !editedUser) return <div>Loading...</div>;

  return (
    <div className="profile">
      <UserInfo user={user} />
      {canEditProfile(user, currentUserId) && (
        <UserEditForm
          user={editedUser}
          errors={errors}
          onSubmit={handleSubmit}
          onChange={(field, val) => setEditedUser(prev => ({ ...prev, [field]: val }))}
        />
      )}
    </div>
  );
}

At the Function Level

The same principle applies to functions; separating fetching from transforming allows each to change independently:

// ❌ Fetching AND transforming
async function getUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return {
    ...data,
    fullName: `${data.firstName} ${data.lastName}`,
    age: new Date().getFullYear() - new Date(data.birthDate).getFullYear()
  };
}

// ✅ Separate concerns
async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}

function transformUser(userData) {
  return {
    ...userData,
    fullName: `${userData.firstName} ${userData.lastName}`,
    age: new Date().getFullYear() - new Date(userData.birthDate).getFullYear()
  };
}

// Usage
const rawUser = await fetchUser(userId);
const user = transformUser(rawUser);

Benefits

  • Immediate clarity: you can open any file and see what it does in seconds.
  • Simple testing: a function that does one thing needs one focused test, with no complex setup and no mocking everything around it.
  • Contained changes: updating validation rules leaves everything else untouched, so one change doesn’t spread into unrelated code.
  • Real reuse: a date formatting function that only formats dates can be used anywhere.
  • Faster debugging: “the date is wrong” means checking one file, not five.
  • Parallel work: different developers own different responsibilities without constant merge conflicts.

Tradeoffs

  • More files: you’ll end up with a lot more navigation, imports, and things to name.
  • Harder to trace: following code flow across many modules takes more effort, and sometimes you just want to see everything in one place.
  • Subjective boundaries: deciding what counts as “one responsibility” is tricky, and reasonable people disagree.
  • Easy to overdo: I’ve seen codebases where a three-line function got its own file.
  • Onboarding cost: more abstractions mean more things for new developers to learn.
  • Team alignment: without agreed-upon boundaries, you’ll have inconsistent patterns scattered everywhere.

Summary

Single responsibility means each function, component, or module does exactly one thing describable in a single sentence. When code has one reason to change, changes stay small, testing becomes simple, and reuse works. The hard part is recognizing when something is doing too much, and splitting it before the complexity builds up.

See also

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.