Skip to main content
Saved

Pattern Fundamentals Beginner

Single Responsibility

Each function and component should have one job, and you should be able to describe that job in a single sentence without using “and”.

Examples
React
Read
3 min

The 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 is displayed, you’re scrolling past authentication logic and hoping you don’t break something.

The pattern

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 only render the form. It gets its data through props, and it emits events when users interact with it.

Problem

I call these “kitchen sink” components, and they’re painful to work with. To test the email validation, you have to mock the API and render the entire component, just to check whether the address contains ”@”.

It gets worse as your team grows. Two developers can’t work on the same feature without running into merge conflicts all the time. A bug fix in the validation logic might somehow break navigation, and after that’s happened a few times, developers start avoiding the file.

Solution

When you follow this rule, you only need to edit one file for each kind of change, whether it’s a new validation rule or a new button style.

A quick way to check is to try describing each of your functions in a single sentence, such as “This function validates email addresses” or “This component renders a user avatar.” If the sentence keeps needing an “and,” the code probably needs to be split up.

Example

The first example below is a component that does too much, and the second is the same component split into smaller pieces that each do one job.

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. If you separate fetching from transforming, you can change each one without touching the other.

// ❌ 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, without complex setup or mocks for everything around it.
  • Contained changes: when you update the validation rules, the rest of the code stays the same.
  • Real reuse: a date formatting function that only formats dates can be used anywhere.
  • Faster debugging: when someone reports that “the date is wrong”, you only need to check one file instead of five.
  • Parallel work: different developers can own different pieces of code without constantly running into merge conflicts.

Tradeoffs

  • More files: you’ll end up with a lot more 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: with more abstractions, there’s more for new developers to learn.
  • Team alignment: if your team hasn’t agreed where the boundaries go, you might end up with inconsistent patterns all over the codebase.

Summary

With single responsibility, each function or component does exactly one thing, and you can describe that thing in a single sentence. When code only has one reason to change, your changes stay small and the code is simple to test. It can be hard to recognize when one of your components is doing too much, but it’s worth splitting it up before the complexity builds.

See also

Next in Fundamentals Barrel Export Index files that re-export a directory's modules from one entry point, so import paths are shorter and the directory has a clear public API.

Newsletter

A monthly email
from Den Odell.

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

No spam. Unsubscribe anytime.