🎨 Component Patterns

Reusable & Scalable Component Design

Design Patterns in React

Learn proven patterns for building flexible, maintainable, and reusable components. These patterns solve common problems in React development.

🔄 Container/Presentational Pattern

Separate logic (container) from UI (presentational).

// Presentational Component - only UI
function UserListUI({ users, loading, error }) {
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Container Component - logic & data fetching
function UserListContainer() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);
  
  return <UserListUI users={users} loading={loading} error={error} />;
}

🎣 Custom Hook Pattern

// Extract logic into custom hook
function useUsers() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(setUsers)
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, []);
  
  return { users, loading, error };
}

// Use in component
function UserList() {
  const { users, loading, error } = useUsers();
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error} />;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

🎭 Render Props Pattern

// Component with render prop
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, [url]);
  
  return render({ data, loading });
}

// Usage
function App() {
  return (
    <div>
      <DataFetcher 
        url="/api/users"
        render={({ data, loading }) => (
          loading ? <Spinner /> : (
            <ul>
              {data.map(user => (
                <li key={user.id}>{user.name}</li>
              ))}
            </ul>
          )
        )}
      />
      
      <DataFetcher 
        url="/api/posts"
        render={({ data, loading }) => (
          loading ? <Spinner /> : (
            <div>
              {data.map(post => (
                <article key={post.id}>{post.title}</article>
              ))}
            </div>
          )
        )}
      />
    </div>
  );
}

🏗️ Higher-Order Component (HOC)

// HOC that adds loading behavior
function withLoading(Component) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) return <Spinner />;
    return <Component {...props} />;
  };
}

// Original component
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Enhanced component
const UserListWithLoading = withLoading(UserList);

// Usage
function App() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  
  return (
    <UserListWithLoading 
      isLoading={loading} 
      users={users} 
    />
  );
}

// Authentication HOC
function withAuth(Component) {
  return function WithAuthComponent(props) {
    const { user, loading } = useAuth();
    
    if (loading) return <Spinner />;
    if (!user) return <Navigate to="/login" />;
    
    return <Component {...props} user={user} />;
  };
}

const ProtectedDashboard = withAuth(Dashboard);

🧩 Compound Components

// Context for shared state
const TabsContext = createContext();

function Tabs({ children, defaultTab }) {
  const [activeTab, setActiveTab] = useState(defaultTab);
  
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ id, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  
  return (
    <button
      className={activeTab === id ? 'active' : ''}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  );
}

function TabPanel({ id, children }) {
  const { activeTab } = useContext(TabsContext);
  return activeTab === id ? <div>{children}</div> : null;
}

// Export as compound component
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage
function App() {
  return (
    <Tabs defaultTab="home">
      <Tabs.List>
        <Tabs.Tab id="home">Home</Tabs.Tab>
        <Tabs.Tab id="profile">Profile</Tabs.Tab>
        <Tabs.Tab id="settings">Settings</Tabs.Tab>
      </Tabs.List>
      
      <Tabs.Panel id="home">Home content</Tabs.Panel>
      <Tabs.Panel id="profile">Profile content</Tabs.Panel>
      <Tabs.Panel id="settings">Settings content</Tabs.Panel>
    </Tabs>
  );
}

🎛️ Control Props Pattern

// Component can be controlled or uncontrolled
function Counter({ value, onChange, defaultValue = 0 }) {
  // Uncontrolled state
  const [internalValue, setInternalValue] = useState(defaultValue);
  
  // Use controlled value if provided, otherwise use internal
  const isControlled = value !== undefined;
  const currentValue = isControlled ? value : internalValue;
  
  const handleIncrement = () => {
    const newValue = currentValue + 1;
    if (!isControlled) {
      setInternalValue(newValue);
    }
    onChange?.(newValue);
  };
  
  const handleDecrement = () => {
    const newValue = currentValue - 1;
    if (!isControlled) {
      setInternalValue(newValue);
    }
    onChange?.(newValue);
  };
  
  return (
    <div>
      <button onClick={handleDecrement}>-</button>
      <span>{currentValue}</span>
      <button onClick={handleIncrement}>+</button>
    </div>
  );
}

// Uncontrolled usage
<Counter defaultValue={5} />

// Controlled usage
function App() {
  const [count, setCount] = useState(0);
  return <Counter value={count} onChange={setCount} />;
}

🎪 Props Getter Pattern

function useDropdown() {
  const [isOpen, setIsOpen] = useState(false);
  
  const getToggleProps = (props = {}) => ({
    ...props,
    onClick: (e) => {
      setIsOpen(!isOpen);
      props.onClick?.(e);
    },
    'aria-expanded': isOpen,
    'aria-haspopup': true
  });
  
  const getMenuProps = (props = {}) => ({
    ...props,
    hidden: !isOpen,
    role: 'menu'
  });
  
  const getItemProps = (props = {}) => ({
    ...props,
    role: 'menuitem',
    onClick: (e) => {
      setIsOpen(false);
      props.onClick?.(e);
    }
  });
  
  return {
    isOpen,
    getToggleProps,
    getMenuProps,
    getItemProps
  };
}

// Usage
function Dropdown() {
  const { getToggleProps, getMenuProps, getItemProps } = useDropdown();
  
  return (
    <div>
      <button {...getToggleProps()}>
        Menu
      </button>
      
      <ul {...getMenuProps()}>
        <li {...getItemProps({ onClick: () => console.log('Edit') })}>
          Edit
        </li>
        <li {...getItemProps({ onClick: () => console.log('Delete') })}>
          Delete
        </li>
      </ul>
    </div>
  );
}

🎨 State Reducer Pattern

// Built-in reducer
const defaultReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

function useCounter({ initialCount = 0, reducer = defaultReducer } = {}) {
  const [state, dispatch] = useReducer(reducer, { count: initialCount });
  
  const increment = () => dispatch({ type: 'INCREMENT' });
  const decrement = () => dispatch({ type: 'DECREMENT' });
  
  return { count: state.count, increment, decrement };
}

// Usage - default behavior
function Counter1() {
  const { count, increment, decrement } = useCounter();
  return (
    <div>
      <button onClick={decrement}>-</button>
      {count}
      <button onClick={increment}>+</button>
    </div>
  );
}

// Usage - custom reducer with max limit
function Counter2() {
  const customReducer = (state, action) => {
    const newState = defaultReducer(state, action);
    // Enforce max of 10
    return { count: Math.min(newState.count, 10) };
  };
  
  const { count, increment, decrement } = useCounter({ 
    reducer: customReducer 
  });
  
  return (
    <div>
      {count} (max: 10)
      <button onClick={increment}>+</button>
    </div>
  );
}

📊 Pattern Comparison

Pattern Use Case Pros Cons
Custom Hook Share stateful logic Simple, composable Can't share JSX
Render Props Share rendering logic Flexible JSX Callback hell
HOC Enhance components Reusable wrappers Prop name collisions
Compound Related components Flexible API More complex
Control Props Flexible control Controlled/uncontrolled More code

🎯 Key Takeaways