React Component Patterns I Use Every Day

After years of React, these are the patterns that have stood the test of time. Simple, effective, and maintainable.

Patterns That Actually Help

There are hundreds of React patterns out there. Most are overengineered. Here are the ones I actually use.

Composition Over Props

Instead of prop drilling or complex configurations:

// ❌ Prop-heavy
<Card 
  title="Hello"
  subtitle="World"
  showFooter
  footerContent="Footer text"
  variant="primary"
/>

// ✅ Composition
<Card>
  <Card.Header>
    <Card.Title>Hello</Card.Title>
    <Card.Subtitle>World</Card.Subtitle>
  </Card.Header>
  <Card.Footer>Footer text</Card.Footer>
</Card>

More flexible, easier to extend.

Custom Hooks for Logic

Extract stateful logic into hooks:

// ❌ Logic in component
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);
  
  // ... render
}

// ✅ Custom hook
function useUser(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetchUser(userId)
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userId]);
  
  return { user, loading, error };
}

function UserProfile({ userId }) {
  const { user, loading, error } = useUser(userId);
  // ... render
}

The hook is reusable and testable.

Controlled Components

Keep form state in the parent:

function Form() {
  const [values, setValues] = useState({ name: '', email: '' });
  
  const handleChange = (e) => {
    setValues(prev => ({
      ...prev,
      [e.target.name]: e.target.value
    }));
  };
  
  return (
    <form>
      <Input 
        name="name" 
        value={values.name} 
        onChange={handleChange}
      />
      <Input 
        name="email" 
        value={values.email} 
        onChange={handleChange}
      />
    </form>
  );
}

Predictable state, easy debugging.

Early Returns

Handle edge cases first:

function UserCard({ user }) {
  if (!user) return null;
  if (user.isLoading) return <Spinner />;
  if (user.error) return <Error message={user.error} />;
  
  return (
    <div className="card">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Clear, readable, no nested conditionals.

Container/Presentational

Separate data fetching from rendering:

// Container - handles data
function UserListContainer() {
  const { users, loading } = useUsers();
  return <UserList users={users} loading={loading} />;
}

// Presentational - just renders
function UserList({ users, loading }) {
  if (loading) return <Spinner />;
  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}

Presentational components are easy to test and reuse.

Keep It Simple

The best pattern is the one that's easiest to understand six months later. Don't over-abstract.

Back to Journal