Reusable Component Patterns

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 2 min

Reusable Component Patterns

Creating truly reusable components in React is challenging. A component that works perfectly on one page might fail on another because it makes too many assumptions about its environment or data.

To build robust component libraries or design systems, you should leverage standard Reusable Component Patterns.

1. The Polymorphic Component Pattern

Often, you build a component (like a <Button>) that looks like a button, but semantically, sometimes it needs to be an <a> tag (for navigation), or a <Link> component (from React Router).

Instead of creating separate <Button>, <LinkButton>, and <RouterButton> components, you use the as prop to dictate the underlying HTML element.

function Button({ as: Component = 'button', children, ...props }) {
  return (
    <Component className="btn-primary" {...props}>
      {children}
    </Component>
  );
}

// Usage:
// Renders a <button>
<Button type="submit">Submit</Button> 

// Renders an <a> tag
<Button as="a" href="https://google.com">Go to Google</Button> 

// Renders a React Router Link
<Button as={Link} to="/about">About Us</Button>

2. Forwarding Refs (forwardRef)

If you are building a reusable, low-level component (Input, Button, Checkbox), you must always wrap it in forwardRef.

If you don’t, consumers of your component will not be able to manage focus, measure the element, or integrate it with animation libraries, significantly limiting its reusability.

const ReusableInput = React.forwardRef((props, ref) => {
  return <input ref={ref} className="custom-input" {...props} />;
});

3. Spreading Rest Props (...rest)

A highly reusable component should not try to define every single valid HTML attribute (like aria-hidden, tabIndex, onMouseEnter, etc.) explicitly in its parameter list.

You should extract the props you specifically need, and use the spread operator (...rest) to pass all remaining props down to the root DOM element.

// ❌ Bad: Forgetting to pass down onFocus, aria-label, etc.
function BadInput({ value, onChange, placeholder }) {
  return <input className="input" value={value} onChange={onChange} placeholder={placeholder} />
}

// ✅ Good: Accepting all valid HTML attributes
function GoodInput({ className, ...rest }) {
  // We merge the user's className with our default one
  return <input className={`input-base ${className || ''}`} {...rest} />
}

4. Custom Hooks for Logic Extraction

If you find yourself copying and pasting complex useEffect or useState logic across multiple components (e.g., detecting clicks outside a modal, or handling infinite scroll), you should extract that logic into a Custom Hook.

Components should be thin wrappers around reusable Hooks.

// Reusable logic
function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener('mousedown', listener);
    return () => document.removeEventListener('mousedown', listener);
  }, [ref, handler]);
}

// Usage in a component
function Dropdown() {
  const ref = useRef(null);
  useClickOutside(ref, () => setOpen(false));
  
  return <div ref={ref}>...</div>;
}

Interview Questions

Q: What is the most critical rule when building a truly reusable component library?
A: Separation of Concerns. A reusable component (like a Dropdown) should handle its own internal UI state (is it open or closed), but it should absolutely never contain hardcoded API calls, global state dependencies, or specific business logic. It must accept overarching business data completely from the parent via props.