No history yet

Advanced Component Architecture

Advanced Component Architecture

You’ve mastered the basics of React. You can build components, manage state with useState, and handle side effects with useEffect. But as applications grow, so does their complexity. How do you build components that are not just functional, but also flexible, reusable, and easy to maintain? The answer lies in understanding advanced component patterns.

The core philosophy behind modern React is composition over inheritance . Instead of creating complex, rigid component hierarchies where components inherit behavior from a base class, we build smaller, focused components and assemble them like building blocks. This approach leads to more predictable and reusable code. We'll explore several patterns that embody this principle, helping you write cleaner, more scalable React applications.

Sharing State with Compound Components

Imagine building a custom <Accordion> component. It has a main wrapper and several <Accordion.Item> components inside. When you click on one item's header, it should expand while others collapse. How do the items know about each other? How does the parent Accordion manage which item is currently open?

This is a classic case for the Compound Components pattern. This pattern allows you to create a set of components that work together, sharing implicit state. The parent component manages the state, and the child components tap into it, often using React's Context API.

// AccordionContext.js
const AccordionContext = React.createContext();

function Accordion({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  const value = { activeIndex, setActiveIndex };
  return <AccordionContext.Provider value={value}>{children}</AccordionContext.Provider>;
}

function AccordionItem({ index, title, children }) {
  const { activeIndex, setActiveIndex } = useContext(AccordionContext);
  const isActive = activeIndex === index;

  return (
    <div className="accordion-item">
      <div className="accordion-title" onClick={() => setActiveIndex(index)}>
        {title}
      </div>
      {isActive && <div className="accordion-content">{children}</div>}
    </div>
  );
}

Accordion.Item = AccordionItem;

// Usage:
<Accordion>
  <Accordion.Item index={0} title="First Item">
    Content for the first item.
  </Accordion.Item>
  <Accordion.Item index={1} title="Second Item">
    Content for the second item.
  </Accordion.Item>
</Accordion>

Here, the Accordion component acts as a state manager, providing the activeIndex and setActiveIndex function through context. Each Accordion.Item can then access this shared context to determine if it should be open and to update the state when its title is clicked. This creates a clean, declarative API for the developer using the component, without the mess of passing props down through multiple layers, a problem often called prop drilling .

Giving Consumers Control

Our Accordion component is great, but it manages its own state entirely. What if the parent component needs to control which item is open, perhaps in response to a button click outside the accordion? This is where the Control Props pattern comes in.

This pattern is a form of Inversion of Control (IoC). Instead of the component managing its state internally, it allows the parent component to “control” that state by passing it down as props. The component then checks if a controlled prop is provided. If it is, it uses that prop's value. If not, it falls back to its own internal state.

// A more flexible Accordion with Control Props
function Accordion({ children, activeIndex: controlledIndex, onSelect }) {
  const [internalIndex, setInternalIndex] = useState(0);

  // Is the component controlled or uncontrolled?
  const isControlled = controlledIndex !== undefined;
  
  // Determine the active index and the update function
  const activeIndex = isControlled ? controlledIndex : internalIndex;
  const setActiveIndex = isControlled ? onSelect : setInternalIndex;

  const value = { activeIndex, setActiveIndex };

  return <AccordionContext.Provider value={value}>{children}</AccordionContext.Provider>;
}

// Usage (Uncontrolled - default behavior)
<Accordion>
  {/* ... Accordion.Item children ... */}
</Accordion>

// Usage (Controlled)
const [currentIndex, setCurrentIndex] = useState(1);
<Accordion activeIndex={currentIndex} onSelect={setCurrentIndex}>
  {/* ... Accordion.Item children ... */}
</Accordion>

This makes our component much more flexible. It can work as a simple, self-contained unit or be fully controlled by its parent, depending on the needs of the application. Many popular component libraries use this pattern for components like dropdowns, tabs, and toggles.

Sharing Reusable Logic

Before React Hooks, there were two primary patterns for sharing reusable logic between components: Render Props and Higher-Order Components (HOCs).

A Render Prop is a function prop that a component uses to know what to render. The component provides its internal state or logic as arguments to this function. For example, a MouseTracker component could provide the current mouse coordinates to its render prop.

An HOC is a function that takes a component and returns a new, enhanced component. It's a way to wrap components with additional functionality. For example, a withMouseData HOC could inject mouse coordinates as props into any component passed to it.

While both patterns work, they can lead to deeply nested component trees, often called "wrapper hell," which can be difficult to read and debug. This is where custom Hooks come in.

Custom Hooks let us extract component logic into reusable functions. A custom hook is just a JavaScript function whose name starts with "use" and that can call other Hooks. This approach achieves a clean separation of concerns by pulling business logic out of the view layer.

// Custom Hook: useMousePosition.js
import { useState, useEffect } from 'react';

function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    window.addEventListener('mousemove', handleMouseMove);

    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Empty dependency array means this effect runs once on mount

  return position;
}

// Usage in a component
function MouseDisplay() {
  const { x, y } = useMousePosition();

  return (
    <div>The mouse position is ({x}, {y})</div>
  );
}

This is much cleaner. There's no extra nesting, the logic is self-contained and easy to test, and the component using it is simple and declarative. While HOCs and render props still have their uses, especially in older codebases or for specific cross-cutting concerns, custom Hooks are now the standard way to share stateful logic in React.

Quiz Questions 1/6

What is the core design philosophy emphasized in modern React for building reusable and flexible components?

Quiz Questions 2/6

You're creating a custom <Menu> component that contains <Menu.Item> and <Menu.Dropdown> children. You want these components to share state (like which item is active) without explicitly passing props down. Which component pattern is best suited for this scenario?