Technology · React

React Component Patterns

Build scalable component architectures with established design patterns.

TL;DR
  1. 01Use compound components to create flexible component APIs.
  2. 02Use render props for shared logic without nesting.
  3. 03Use higher-order components to wrap and enhance components.

Compound Components

  • Build components that work together as a unit.
    function Tabs() {
      const [active, setActive] = useState(0);
      
      return (
        <TabsContext.Provider value={{ active, setActive }}>
          {/* Children use context */}
        </TabsContext.Provider>
      );
    }
    
    Tabs.TabList = function TabList({ children }) {
      return <div>{children}</div>;
    };
    
    Tabs.Tab = function Tab({ index, children }) {
      const { active, setActive } = useContext(TabsContext);
      return (
        <button onClick={() => setActive(index)}>
          {children}
        </button>
      );
    };
    
    Tabs.Panel = function Panel({ index, children }) {
      const { active } = useContext(TabsContext);
      return active === index && <div>{children}</div>;
    };
    
    // Usage
    <Tabs>
      <Tabs.TabList>
        <Tabs.Tab index={0}>Tab 1</Tabs.Tab>
        <Tabs.Tab index={1}>Tab 2</Tabs.Tab>
      </Tabs.TabList>
      <Tabs.Panel index={0}>Content 1</Tabs.Panel>
      <Tabs.Panel index={1}>Content 2</Tabs.Panel>
    </Tabs>
  • Compound components allow flexible nesting and customization.

Render Props

  • Pass a function as a prop to share logic.
    function MouseTracker({ children }) {
      const [position, setPosition] = useState({ x: 0, y: 0 });
      
      function handleMouseMove(event) {
        setPosition({
          x: event.clientX,
          y: event.clientY
        });
      }
      
      return (
        <div onMouseMove={handleMouseMove}>
          {children(position)}
        </div>
      );
    }
    
    // Usage
    <MouseTracker>
      {(position) => (
        <p>Mouse position: {position.x}, {position.y}</p>
      )}
    </MouseTracker>
  • Render props are more flexible than HOCs for multiple consumers.
    function DataFetcher({ url, children }) {
      const [data, setData] = useState(null);
      const [error, setError] = useState(null);
      
      useEffect(() => {
        fetch(url)
          .then(r => r.json())
          .then(setData)
          .catch(setError);
      }, [url]);
      
      return children({ data, error });
    }

Higher-Order Components

  • Wrap a component to add behavior or props.
    function withAuthentication(Component) {
      return function ProtectedComponent(props) {
        const { isAuthenticated } = useAuth();
        
        if (!isAuthenticated) {
          return <div>Please log in</div>;
        }
        
        return <Component {...props} />;
      };
    }
    
    const ProtectedPage = withAuthentication(Page);
  • Use HOCs to share state logic across components.
    function withToggle(Component) {
      return function ToggledComponent(props) {
        const [isOpen, setIsOpen] = useState(false);
        
        return (
          <Component
            {...props}
            isOpen={isOpen}
            toggle={() => setIsOpen(!isOpen)}
          />
        );
      };
    }

Custom Hooks Pattern

  • Extract logic into custom hooks for reusability.
    function useFetch(url) {
      const [data, setData] = useState(null);
      const [loading, setLoading] = useState(true);
      
      useEffect(() => {
        fetch(url)
          .then(r => r.json())
          .then(setData)
          .finally(() => setLoading(false));
      }, [url]);
      
      return { data, loading };
    }
    
    // Usage in any component
    function MyComponent() {
      const { data, loading } = useFetch('/api/data');
      return loading ? <p>Loading...</p> : <p>{data}</p>;
    }
  • Custom hooks are the modern approach to code sharing.

Container/Presentational Pattern

  • Separate logic (container) from presentation (component).
    // Container: handles logic and state
    function UserListContainer() {
      const [users, setUsers] = useState([]);
      
      useEffect(() => {
        fetch('/api/users')
          .then(r => r.json())
          .then(setUsers);
      }, []);
      
      return <UserList users={users} />;
    }
    
    // Presentational: pure UI component
    function UserList({ users }) {
      return (
        <ul>
          {users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      );
    }
  • Makes components easier to test and reuse.

Tip: Use custom hooks as the first choice for sharing logic — they're simpler than HOCs and more flexible than render props.

Warning: Avoid deeply nested render props or multiple HOCs — this creates "wrapper hell" that's hard to debug.

React AccessibilityReact Context Performance