Skip to content
Home
React Components: Composition, Patterns, and Best Practices

React Components: Composition, Patterns, and Best Practices

React React 9 min read 1744 words Intermediate ExcellentWiki Editorial Team

Components are the fundamental building blocks of any React application. A React component is a self-contained piece of UI that manages its own structure, behavior, and optionally its own state. React’s entire philosophy revolves around composing these components together to build complex interfaces. Understanding component patterns deeply is what separates junior from senior React developers.

Function Components vs Class Components

Before React 16.8, class components were the only way to use state and lifecycle methods. Today, function components with hooks are the standard. The React documentation explicitly states: “We recommend defining components as functions instead of classes.”

Class Components

Class components extend React.Component and must implement a render method. They have access to lifecycle methods such as componentDidMount, componentDidUpdate, and componentWillUnmount. While they remain functional for legacy codebases, class components have several drawbacks: verbose this binding, confusing lifecycle logic spread across methods, and difficulty sharing stateful logic without render props or higher-order components.

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }
  render() {
    return <button onClick={() => this.setState({ count: this.state.count + 1 })}>
      {this.state.count}
    </button>;
  }
---

Function Components

Function components are simpler JavaScript functions that accept props and return JSX. With hooks, they can manage state, handle side effects, and access context — everything class components can do, but with less boilerplate.

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
---

Dan Abramov’s article “How Are Function Components Different from Classes?” explains that function components capture the rendered values — props and state — at the time of rendering, which eliminates a class of bugs related to stale closures and asynchronous reads of this.state.

Component Composition Patterns

React’s component model is built on composition. The React documentation dedicates an entire page to “Composition vs Inheritance,” making clear that inheritance hierarchies are rarely appropriate in React.

Containment with Children

The simplest composition pattern uses the children prop. A component can wrap arbitrary content without knowing what it is in advance:

function Card({ children, title }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  );
---

Specialization

When one component is a “special case” of another, composition is cleaner than inheritance. A Dialog component can be specialized as a WelcomeDialog by configuring props:

function Dialog({ title, message }) {
  return (
    <Card title={title}>
      <p>{message}</p>
    </Card>
  );
---

function WelcomeDialog() {
  return <Dialog title="Welcome" message="Thanks for visiting!" />;
---

Compound Components

Compound components share implicit state while giving the consumer control over rendering order. The canonical example is a <select> and <option> pattern. Libraries like Reach UI and Radix UI popularized this pattern. A simple implementation uses React.Children.map and cloneElement to pass shared state:

function Tabs({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <div>
      {React.Children.map(children, (child, index) =>
        React.cloneElement(child, { isActive: index === activeIndex, onActivate: () => setActiveIndex(index) })
      )}
    </div>
  );
---

Higher-Order Components

A higher-order component (HOC) is a function that takes a component and returns a new component with enhanced behavior. Before hooks, HOCs were the primary mechanism for reusing stateful logic. Redux’s connect, React Router’s withRouter, and Relay’s createFragmentContainer are all HOCs.

function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const user = useContext(AuthContext);
    return <WrappedComponent {...props} user={user} />;
  };
---

While hooks have largely replaced HOCs for sharing logic, HOCs remain useful for cross-cutting concerns like logging, analytics, and conditional rendering. The React documentation recommends preferring hooks over HOCs for new code.

Render Props

A render prop is a function prop that a component uses to determine what to render. The term was coined by Michael Jackson (React Router author) and documented in the React docs under “Render Props.” The pattern makes components highly reusable by delegating rendering logic to the consumer:

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  return <div onMouseMove={e => setPosition({ x: e.clientX, y: e.clientY })}>
    {render(position)}
  </div>;
---

Render props are versatile but can lead to deeply nested code (wrapper hell). Hooks provide a cleaner alternative for most use cases, though render props still appear in libraries like Formik and React Router.

Controlled vs Uncontrolled Components

This distinction is critical for form elements but applies broadly. Controlled components store their state in React state and update it through event handlers. Uncontrolled components manage their own internal DOM state, accessed via refs.

The React documentation recommends controlled components for most cases because they give React single source of truth. However, uncontrolled components can improve performance for simple forms because they avoid re-rendering on every keystroke.

Error Boundaries

Error boundaries catch JavaScript errors in their child component tree and display a fallback UI instead of crashing the whole app. They are implemented with componentDidCatch lifecycle method or the static getDerivedStateFromError. Function components do not have an equivalent yet, though React RFC #213 discusses adding error boundary hooks.

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(error, info) { logError(error, info); }
  render() { return this.state.hasError ? <ErrorUI /> : this.props.children; }
---

Error boundaries cannot catch errors in event handlers, asynchronous code, or server-side rendering. For those cases, use try-catch or the onError event handler. The React documentation recommends placing error boundaries strategically — one at the root for catastrophic failures and additional boundaries around independent sections so one failure does not bring down the entire application.

Refs and DOM Access

Refs provide a way to access DOM nodes directly. While React encourages declarative programming, there are imperative use cases: managing focus, text selection, media playback, and integrating with non-React libraries.

function AutoFocusInput() {
  const inputRef = useRef(null);
  useEffect(() => { inputRef.current.focus(); }, []);
  return <input ref={inputRef} />;
---

Callback refs offer more control when the ref value needs to change dynamically. Forwarding refs with React.forwardRef allows parent components to pass refs down to child components, which is essential for reusable component libraries and form inputs that need imperative access from parent forms.

Portals for Modals and Overlays

Portals render children into a DOM node that exists outside the parent component’s DOM hierarchy. This is essential for modals, tooltips, and dropdowns that need to escape overflow or z-index constraints:

function Modal({ children }) {
  return ReactDOM.createPortal(
    <div className="modal-overlay">{children}</div>,
    document.getElementById('modal-root')
  );
---

Portals preserve React context — events bubble from the portal content to the parent React tree as expected, even though the DOM node is elsewhere. The React documentation’s portal page provides examples for accessible modal dialogs with focus trapping.

Best Practices for Components

Single Responsibility

Each component should do one thing well. If a component renders a list with filtering and pagination and inline editing, it is doing too much. Break it into smaller components. The React DevTools Profiler makes it easy to identify components that re-render too often due to excessive responsibilities.

Prop Design

Keep props minimal. Avoid passing entire objects when the component only needs a few properties. Use default props for optional configuration. TypeScript or PropTypes provide runtime validation, but TypeScript is preferred for production code because it catches errors at compile time.

Component Naming

Use PascalCase for component names so they are distinguishable from HTML elements. Prefix presentational components with descriptive names (e.g., UserAvatar, ErrorMessage). Use suffix conventions like Container for stateful wrappers and View for presentational components.

React Performance Best Practices

Performance optimization in React requires understanding when and why components re-render. React re-renders a component when its state changes, its parent re-renders, or the context it consumes changes. Unnecessary re-renders are the most common performance issue. Use React.memo to prevent re-renders when props have not changed — it performs a shallow comparison of props and skips rendering if they are identical. The useMemo hook memoizes expensive computation results, recalculating only when dependencies change. The useCallback hook memoizes function references, preventing child components from re-rendering due to new function references on every parent render. For lists, ensure each item has a stable, unique key prop — using array indices as keys causes bugs when items are reordered. Virtualization libraries like react-window and react-virtuoso render only visible items in long lists, dramatically reducing DOM nodes. Code splitting with React.lazy and Suspense loads components on demand, reducing initial bundle size. Profile your application with React DevTools Profiler before optimizing — measuring identifies actual bottlenecks that are worth fixing.

Testing React Components Effectively

React Testing Library encourages testing from the user’s perspective. Write tests that verify behavior, not implementation details. Use getByRole, getByLabelText, and getByText queries to find elements the way users do — by accessibility attributes and visible text. Avoid testing internal state or component internals; instead, test what the user sees and does. For async operations, use waitFor and findBy queries that retry until the element appears. Mock external dependencies at the network level using MSW (Mock Service Worker) rather than mocking imported modules. This creates more realistic tests that verify your integration with actual API contracts.

FAQ

When should I use class components instead of function components?

Only when maintaining legacy code that depends on class lifecycle methods or when your project has not upgraded to React 16.8+. For all new development, use function components with hooks.

What is the difference between a pure component and React.memo?

React.PureComponent is a class component that implements shouldComponentUpdate with a shallow prop and state comparison. React.memo is its function component equivalent, wrapping a function component to memoize its rendered output.

Why does React emphasize composition over inheritance?

Composition provides more flexibility at the component level. Inheritance creates tight coupling between parent and child, making changes difficult. Composition allows you to reorder, wrap, and configure children at runtime.

How do I share logic between components?

Use custom hooks. Before hooks, developers used HOCs and render props, but custom hooks are cleaner and avoid wrapper nesting. The React documentation provides several examples of custom hooks for sharing logic.

Conclusion

Mastering React components means understanding the patterns of composition, the trade-offs between controlled and uncontrolled components, and when to use advanced patterns like HOCs and render props. The React documentation’s “Composition vs Inheritance” page is essential reading, as is Dan Abramov’s blog for deeper conceptual understanding. As your components grow in complexity, revisit the React State Management guide to learn how to structure application-level state, and explore React Hooks Guide for patterns like custom hooks that replace older sharing mechanisms.

For a comprehensive overview, read our article on Next Js Guide.

For a comprehensive overview, read our article on React Authentication.

Section: React 1744 words 9 min read Intermediate 756 articles in section Report inaccuracy Back to top