How Composition Over Inheritance Reshaped Component Development
Min-jun Kim
Dev Intern · Leapcell

Introduction
For years, object-oriented programming (OOP) principles, particularly inheritance, heavily influenced how we structured our frontend components. Developers often found themselves creating class hierarchies, extending base components, and navigating complex this contexts to reuse logic. While initially appealing for its perceived orderliness, this approach frequently led to issues like the "wrapper hell," prop drilling, and brittle component structures where a change in a parent class could unexpectedly break a child. This rigid inheritance model constrained flexibility and made it challenging to extract and share stateful logic across disparate components. Enter the paradigm shift: composition over inheritance. This fundamental principle, long championed in software engineering, has been dramatically re-energized in the frontend world with the advent of React Hooks and Vue Composition API. These groundbreaking features have not only simplified state management and side effect handling but have also fundamentally altered how we think about and write reusable component logic, paving the way for more maintainable and scalable applications.
A New Era of Component Development
Before diving into the specifics of Hooks and the Composition API, let's briefly define some key concepts that underpin this discussion:
- Composition: In software engineering, composition refers to the act of combining smaller, more focused functions or objects to build larger, more complex ones. Unlike inheritance, where a new class extends an existing one, composition focuses on "has-a" relationships rather than "is-a" relationships. This promotes flexibility and reusability.
- Encapsulation: The bundling of data (state) and methods (behavior) that operate on the data into a single unit. In frontend components, this typically means keeping related logic together.
- Separation of Concerns: The practice of dividing a computer program into distinct features that overlap in functionality as little as possible. This makes software easier to design, understand, and maintain.
The Rise of React Hooks
React Hooks, introduced in React 16.8, revolutionized how functional components manage state and side effects. Prior to Hooks, state and lifecycle methods were exclusive to class components, forcing developers to convert functional components to classes when state or effects were needed. This often led to the "HOC hell" or "render prop hell" for logic reuse. Hooks provide a way to reuse stateful logic without changing your component hierarchy.
At its core, a Hook is a special function that lets you "hook into" React features from functional components. Let's look at useState and useEffect as prime examples:
useState for State Management
import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); // Initialize count to 0 return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
Here, useState allows a functional component to maintain its own state. The component doesn't inherit state management capabilities from a base class; instead, it composes them by calling useState.
useEffect for Side Effects
import React, { useState, useEffect } from 'react'; function DocumentTitleUpdater() { const [count, setCount] = useState(0); useEffect(() => { // This runs after every render document.title = `Count: ${count}`; }, [count]); // Rerun effect only when count changes return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(prevCount => prevCount + 1)}>Increment</button> </div> ); }
useEffect handles side effects like data fetching, subscriptions, or manually changing the DOM. Instead of scattering logic across componentDidMount, componentDidUpdate, and componentWillUnmount, useEffect allows you to group related logic together based on what it does, not when it runs. The dependency array [count] ensures the effect only re-runs when count changes, further enhancing control and performance.
Custom Hooks for Reusable Logic
The true power of Hooks lies in custom Hooks. These are JavaScript functions whose names start with use and can call other Hooks. They allow you to extract stateful logic into reusable functions.
import { useState, useEffect } from 'react'; function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => { // Cleanup function window.removeEventListener('resize', handleResize); }; }, []); // Empty dependency array means this effect runs once on mount and cleans up on unmount return width; } function MyComponent() { const width = useWindowWidth(); // Compose the window width logic return ( <div> <p>Window width: {width}px</p> </div> ); }
The useWindowWidth custom Hook encapsulates the logic for tracking window width. Any component can now compose this logic simply by calling useWindowWidth(), without any inheritance or prop drilling. This directly addresses the problem of sharing non-visual logic.
Vue Composition API
Vue 3 introduced the Composition API, a set of additive APIs that allow us to compose component logic using imported functions. Much like React Hooks, it provides a powerful alternative to the Options API (which was often criticized for its scattered logic across data, methods, computed, watch, and lifecycle hooks). The Composition API promotes logical concerns being grouped together, irrespective of their type.
setup Function and Reactive State
The setup function is the entry point for using the Composition API in a component. It runs before the component is created, and it's where you declare reactive state, computed properties, watchers, and lifecycle hooks.
<template> <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </template> <script> import { ref, onMounted } from 'vue'; export default { setup() { const count = ref(0); // Reactive state using ref function increment() { count.value++; } onMounted(() => { console.log('Component mounted!'); }); return { count, increment, }; }, }; </script>
Here, ref creates a reactive reference to a value, similar to React's useState for primitive types. The onMounted lifecycle hook is imported and called inside setup, keeping lifecycle concerns close to the logic they affect.
Reusable Logic with Composables
The Vue equivalent of custom Hooks is called "composables". These are functions that encapsulate stateful logic and can be reused across components.
// useWindowWidth.js import { ref, onMounted, onUnmounted } from 'vue'; export function useWindowWidth() { const width = ref(window.innerWidth); const handleResize = () => { width.value = window.innerWidth; }; onMounted(() => { window.addEventListener('resize', handleResize); }); onUnmounted(() => { window.removeEventListener('resize', handleResize); }); return { width }; }
<template> <div> <p>Window width: {{ width }}px</p> </div> </template> <script> import { useWindowWidth } from './useWindowWidth'; export default { setup() { const { width } = useWindowWidth(); // Compose the window width logic return { width }; }, }; </script>
Just like React's custom Hooks, this useWindowWidth composable effectively abstracts away the window resizing logic. Components can now import and utilize this logic without directly implementing or inheriting it, leading to cleaner, more focused components.
The Impact on Component Design
Both React Hooks and Vue Composition API exemplify the "composition over inheritance" principle by:
- Improving Code Organization: Related logic (e.g., data fetching and its loading/error states) can be grouped together within a single Hook or composable, rather than being split across multiple lifecycle methods or options.
- Enhancing Reusability: Logic can be extracted and shared as plain JavaScript functions, making it incredibly easy to reuse stateful and side-effectful logic across different components without introducing new layers in the component tree.
- Flattening Component Hierarchies: They eliminate the need for Higher-Order Components (HOCs) or render props for sharing logic, thereby reducing wrapper hell and deeply nested component trees.
- Increasing Readability and Maintainability: Components become easier to understand because the logic for a specific feature is co-located. Debugging is simplified as you can trace logic within self-contained units.
- Flexible and Powerful Abstractions: Developers can build powerful abstractions tailored to their application's needs, creating a domain-specific set of reusable tools.
In essence, these APIs empower developers to think of a component as a composition of behaviors rather than a monolithic class or a collection of disparate options.
Conclusion
The shift from inheritance-heavy component design to composition-based approaches with React Hooks and Vue Composition API marks a significant evolution in frontend development. By enabling developers to extract, reuse, and organize stateful logic as discrete, callable functions, these innovations have drastically improved code organization, reusability, and readability. They empower us to build more robust, scalable, and maintainable applications by favoring modular, flexible composition over rigid, fragile inheritance. This transition represents a conscious move towards designing components that are simpler, more focused, and ultimately, easier to reason about, fundamentally reshaping how we craft our user interfaces.

