Dwarves
Memo
Type ESC to close search bar

Component composition patterns in React

Component composition patterns are foundational for creating scalable, flexible, and reusable React components. They allow us to build UIs by combining smaller, single-purpose components in various ways.

Key composition patterns in React

Higher-order components (HOCs)

HOCs are functions that take a component and return a new component, adding additional functionality. They’re particularly useful for cross-cutting concerns like logging, analytics, or authentication.

Example use case: Suppose you need to add logging functionality to multiple components. Instead of embedding logging code in each component, you create an HOC that wraps each component and handles the logging logic.

function withLogging(WrappedComponent) {
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log(`Component ${WrappedComponent.name} mounted`)
    }, [])
    return <WrappedComponent {...props} />
  }
}

When to use HOCs:

Trade-offs:

Render props

With render props, a component uses a prop as a function to control its output, allowing you to pass dynamic rendering logic.

Example use case: If you have a <DataFetcher /> component that retrieves data, you could use render props to define how that data should be rendered by the consuming component.

function DataFetcher({ render }) {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetchData().then(setData)
  }, [])
  return render(data)
}

// Usage:
;<DataFetcher render={(data) => <DisplayData data={data} />} />

When to use render props:

Trade-offs:

Compound components

Compound components are components that work together as a single unit but allow for great customization of individual parts.

Example use case: A <Dropdown /> component that lets you use <Dropdown.Toggle /> and <Dropdown.Menu /> as children, giving flexibility to control each part while keeping the structure consistent.

function Dropdown({ children }) {
  const [isOpen, setIsOpen] = useState(false)
  return (
    <DropdownContext.Provider value={{ isOpen, setIsOpen }}>
      <div className="dropdown">{children}</div>
    </DropdownContext.Provider>
  )
}

Dropdown.Toggle = function Toggle() {
  const { setIsOpen } = useContext(DropdownContext)
  return <button onClick={() => setIsOpen((open) => !open)}>Toggle</button>
}

Dropdown.Menu = function Menu({ children }) {
  const { isOpen } = useContext(DropdownContext)
  return isOpen ? <div className="menu">{children}</div> : null
}

When to use compound components:

Trade-offs:

Controlled and uncontrolled components

Controlled components let the parent manage the state, whereas uncontrolled components manage their own state internally. Combining them allows more flexibility in form components.

Example use case: A <TextInput /> component that can work either as a controlled component (with value and onChange passed from the parent) or an uncontrolled component (handling its own state).

function TextInput({ value, defaultValue, onChange }) {
    const [internalValue, setInternalValue] = useState(defaultValue);
    const isControlled = value !== undefined;

    const handleChange = (e) => {
        const newValue = e.target.value;
        if (isControlled) {
            onChange(newValue);
        } else {
            setInternalValue(newValue);
        }
    };

    return (
        <input
            value={isControlled ? value : internalValue}
            onChange={handleChange}
        />
    );
}

// Usage:
<TextInput defaultValue="Uncontrolled" />
<TextInput value={controlledValue} onChange={setControlledValue} />

When to use controlled and uncontrolled components:

Trade-offs:

Custom hooks

Custom hooks provide a way to abstract and encapsulate complex logic, making components more modular and readable. They can be used in place of certain HOCs and render props for handling things like async data, complex state, or side effects.

Example use case: If you frequently need to fetch data in multiple components, a useFetch custom hook encapsulates this logic, making the components cleaner and more testable.

function useFetch(url) {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetch(url)
      .then((response) => response.json())
      .then(setData)
  }, [url])
  return data
}

When to use custom hooks:

Trade-offs:

Choosing the right pattern

Selecting the right pattern often depends on: