Design Converter
Education
Last updated on Feb 12, 2025
Last updated on Feb 12, 2025
Software Development Executive - I
Understanding how to extend a component can be very useful—especially when working with legacy code or third‑party libraries where you might want to reuse and customize behavior.
This blog shows how to extend React components. We start with a background on components in React and then explore how inheritance works in the context of React class components. We also examine how extending a component helps enforce the DRY (Don’t Repeat Yourself) principle, and we discuss when it might be better to use composition instead.
Throughout, you’ll see code examples, detailed explanations, and a visual diagram that ties everything together.
In React, a component is an independent, reusable piece of code that defines what appears on the screen. Components can be defined as functions or as ES6 classes. Each component accepts inputs called “props” and maintains its own internal state (if needed). In class components, you typically extend React.Component
and override the render()
method to return JSX.
Consider a simple function component:
1function Greeting(props) { 2 return <h1>Hello, {props.name}!</h1>; 3}
And here’s a class component version:
1class Greeting extends React.Component { 2 render() { 3 return <h1>Hello, {this.props.name}!</h1>; 4 } 5}
Both forms are valid in React. However, before the introduction of hooks in React 16.8, class components were the only way to manage state and lifecycle methods. With hooks, function components can now handle state and side effects, which has led many developers to favor them over class components. Despite this, many enterprise or legacy codebases still rely on class components—and with them, the ability to extend components via inheritance.
Inheritance is a fundamental principle in object‑oriented programming (OOP). When a class inherits from another, it automatically gains the properties and methods of the parent (or base) class. This enables developers to reuse code, extend functionality, and enforce consistency across similar objects. For instance, if you have several user interface elements that share common behavior (such as logging or error handling), you could encapsulate that behavior in a base class and extend it in more specific subclasses.
Composition, on the other hand, involves assembling components together. Instead of inheriting behavior, you pass components as children or use higher‑order components (HOCs) to “wrap” existing components and extend their functionality. Composition is often seen as more flexible and less tightly coupled than inheritance. In the React community, composition is widely recommended because it encourages more modular and maintainable code.
Even though composition is the preferred pattern, there are times when extending a component via inheritance is beneficial. Consider these scenarios:
It is important, however, to recognize that inheritance can lead to deep hierarchies that are hard to understand if overused. The rule of thumb is to use inheritance sparingly and prefer composition for general code reuse.
When we talk about “extending a component” in React, we refer to the process of creating a new class component that inherits from an existing one. This technique lets you build on top of the base component’s logic and output.
Imagine you have a simple base component:
1class BaseComponent extends React.Component { 2 render() { 3 return <div>Hello from BaseComponent!</div>; 4 } 5}
You can create a new component that extends BaseComponent
:
1class ChildComponent extends BaseComponent { 2 render() { 3 return ( 4 <div> 5 <h2>This is ChildComponent</h2> 6 </div> 7 ); 8 } 9}
In the example above, ChildComponent
completely overrides the render()
method of BaseComponent
. As a result, if you render <ChildComponent />
, you will only see the output of ChildComponent
.
super.render()
to Reuse LogicOften, you want to reuse the base component’s rendering logic and add more content. You can do this by calling super.render()
inside the child component’s render()
method:
1class ChildComponent extends BaseComponent { 2 render() { 3 return ( 4 <div> 5 {super.render()} 6 <h2>This is additional content from ChildComponent.</h2> 7 </div> 8 ); 9 } 10}
By invoking super.render()
, the child component calls the parent’s render()
method and includes its output in the final JSX. This is particularly useful when the base component contains a complex UI that you want to augment rather than completely replace.
In this section, we present several practical examples that illustrate how to extend a component in React.
Let’s start with a basic example. Assume we have a base component that displays a simple greeting:
1class BaseGreeting extends React.Component { 2 render() { 3 return <h1>Hello from BaseGreeting!</h1>; 4 } 5}
Now, we want to create a more specific greeting that not only uses the base message but also adds some extra detail. We create a new component that extends BaseGreeting
:
1class ExtendedGreeting extends BaseGreeting { 2 render() { 3 return ( 4 <div> 5 {super.render()} 6 <p>Welcome to our awesome application!</p> 7 </div> 8 ); 9 } 10}
If you render <ExtendedGreeting />
, the output will include both the greeting from BaseGreeting
and the additional message from ExtendedGreeting
.
Below is a complete example that can be rendered in a simple React application:
1import React from 'react'; 2import ReactDOM from 'react-dom/client'; 3 4class BaseGreeting extends React.Component { 5 render() { 6 return <h1>Hello from BaseGreeting!</h1>; 7 } 8} 9 10class ExtendedGreeting extends BaseGreeting { 11 render() { 12 return ( 13 <div> 14 {super.render()} 15 <p>Welcome to our awesome application!</p> 16 </div> 17 ); 18 } 19} 20 21const root = ReactDOM.createRoot(document.getElementById('root')); 22root.render(<ExtendedGreeting />);
This example demonstrates the simplest form of extension using inheritance.
Another common scenario is extending a component from a third‑party library (such as Material‑UI) so that you can tweak its behavior without rewriting it entirely.
Suppose you have a Material‑UI Button that you want to extend to always have a default variant (say, “contained”). You could create your own component that extends the Material‑UI Button and assigns default props:
1import React from 'react'; 2import ReactDOM from 'react-dom'; 3import Button from '@material-ui/core/Button'; 4 5class ExtendedButton extends Button { 6 render() { 7 // Merge default props with any passed-in props using Object.assign 8 const props = Object.assign({ variant: 'contained' }, this.props); 9 return <Button {...props}>{this.props.children}</Button>; 10 } 11} 12 13const App = () => ( 14 <div> 15 <Button variant="outlined">Original Button</Button> 16 <ExtendedButton>Extended Button</ExtendedButton> 17 </div> 18); 19 20ReactDOM.render(<App />, document.getElementById('root'));
In this code, ExtendedButton
inherits from the Material‑UI Button and forces a default variant
property. Notice that the child component still uses props.children
so that the button’s label remains dynamic. This approach lets you modify and extend the base component’s behavior without copying the original implementation.
While extending components can be beneficial, it also comes with potential pitfalls. Here are some best practices and things to avoid:
The React documentation states, “React doesn’t recommend using inheritance to reuse code between components.” Instead, you can often achieve code reuse by composing components together. For instance, rather than extending a component to add extra UI elements, consider wrapping it inside another component.
When you extend a component, you may also override lifecycle methods (e.g., componentDidMount
). Be careful: if you override a lifecycle method without calling the parent’s version, you might lose critical functionality defined in the base component. To preserve the base functionality, always call super.componentDidMount()
(or the corresponding method) when overriding.
While inheritance and composition are often seen as alternatives, there are cases where you might combine them. For example, you might extend a base component for some common logic and then compose additional behavior by including other components as children. However, this mix should be used sparingly and only when it clearly improves clarity.
Inherited behavior can sometimes be less obvious when writing tests. Make sure to write thorough tests for both base and extended components. Document the expected behavior and any deviations that result from the extension. This will help future maintainers understand why the extension was necessary.
Although extending components through inheritance is sometimes necessary, the React community generally favors composition. Here’s why and how you might choose alternatives:
In composition, you create components by assembling simpler ones together. For example:
1function BaseMessage(props) { 2 return <div>{props.message}</div>; 3} 4 5function ExtendedMessage(props) { 6 return ( 7 <div> 8 <BaseMessage message={props.message} /> 9 <p>Have a great day!</p> 10 </div> 11 ); 12}
This approach is often more flexible than inheritance because it allows you to change the composition without being tightly coupled to a base component’s implementation.
A higher‑order component is a function that takes a component and returns a new component with extended behavior. HOCs allow you to “decorate” a component without modifying its class directly. For example:
1function withGreeting(WrappedComponent) { 2 return class extends React.Component { 3 render() { 4 return ( 5 <div> 6 <h1>Greeting Header</h1> 7 <WrappedComponent {...this.props} /> 8 </div> 9 ); 10 } 11 }; 12} 13 14class SimpleComponent extends React.Component { 15 render() { 16 return <div>This is a simple component.</div>; 17 } 18} 19 20const EnhancedComponent = withGreeting(SimpleComponent);
Here, EnhancedComponent
has the added header while still retaining the original behavior of SimpleComponent
. HOCs are widely used to add features like theming, error boundaries, and more.
TypeScript is increasingly popular in the React ecosystem for its strong typing and enhanced tooling. When extending components with TypeScript, you can leverage interfaces and generics to ensure that your components receive the correct props.
Here’s an example using a base component and an extended component with TypeScript:
1import React from 'react'; 2 3interface BaseProps { 4 greeting?: string; 5} 6 7class BaseComponent extends React.Component<BaseProps> { 8 render() { 9 return <div>{this.props.greeting || "Hello from BaseComponent!"}</div>; 10 } 11} 12 13interface ExtendedProps extends BaseProps { 14 additionalMessage?: string; 15} 16 17class ExtendedComponent extends BaseComponent { 18 render() { 19 return ( 20 <div> 21 {super.render()} 22 <p>{this.props.additionalMessage || "This is the extended part."}</p> 23 </div> 24 ); 25 } 26} 27 28// Usage in a TypeScript file 29import ReactDOM from 'react-dom/client'; 30 31const root = ReactDOM.createRoot(document.getElementById('root')!); 32root.render(<ExtendedComponent greeting="Hi there!" additionalMessage="Enjoy your coding!" />);
In this example, TypeScript ensures that both the base and extended components receive the correct props. The ExtendedProps
interface extends BaseProps
to include additional properties.
For example, extending a Material‑UI component:
1import React from 'react'; 2import ReactDOM from 'react-dom'; 3import Button, { ButtonProps } from '@material-ui/core/Button'; 4 5interface ExtendedButtonProps extends ButtonProps { 6 extraInfo?: string; 7} 8 9class ExtendedButton extends React.Component<ExtendedButtonProps> { 10 render() { 11 // Merge default props with incoming props 12 const mergedProps = Object.assign({ variant: 'contained' }, this.props); 13 return ( 14 <div> 15 <Button {...mergedProps}>{this.props.children}</Button> 16 {this.props.extraInfo && <small>{this.props.extraInfo}</small>} 17 </div> 18 ); 19 } 20} 21 22const App: React.FC = () => ( 23 <div> 24 <Button variant="outlined">Standard Button</Button> 25 <ExtendedButton extraInfo="Extended button with extra info"> 26 Extended Button 27 </ExtendedButton> 28 </div> 29); 30 31ReactDOM.render(<App />, document.getElementById('root'));
TypeScript allows you to extend and customize third‑party components while maintaining type safety and clear documentation of what props are expected.
When extending components, performance is a key factor to consider. Although inheritance itself is not inherently slow, there are some subtle considerations:
componentDidMount
), you might miss important side effects.super.render()
may cause redundant rendering if not carefully structured. Always ensure that you are not rendering the same UI twice unless that is intended.Each extended component carries the baggage of its parent’s methods and state. In a large application, if many components are extended unnecessarily, the memory overhead might be slightly higher. In most cases, this is negligible, but it is something to keep in mind in high‑performance applications.
When a component is extended, any change in the base component’s implementation might affect all subclasses. This can sometimes lead to performance bottlenecks if the base component is inefficient. Therefore, if you choose to extend a component for performance reasons, be sure to measure and profile your application.
Imagine you are using a UI library that provides a highly functional, but not quite perfect, date picker. You want to change its styling and add some additional features (like a custom tooltip) without forking the library. By extending the base date picker component, you can override its render()
method to include the additional features while still reusing most of the original functionality.
In many legacy React applications, class components were the norm. Extending components in these environments can help you incrementally refactor or upgrade the UI without rewriting everything. For instance, if a base component handles form validation in a certain way, you can extend it to add logging or error handling without modifying the original code.
When developing a custom UI library, you may have a base component that encapsulates common functionality (such as theming or animation logic). Developers using your library might extend this base component to create their own specialized versions. This pattern promotes code reuse and ensures consistency across various UI elements.
Sometimes, you might need to add cross‑cutting features such as analytics or debugging information to several components. Instead of modifying each component individually, you can create a base component with the common features and extend it. For example, a base component might log render times, and every extended component automatically inherits that behavior.
While the focus of this article is on extending components via inheritance, it’s important to recognize that you can combine inheritance with other patterns for maximum flexibility.
You can extend a base component for the majority of shared functionality, but also compose additional behavior by including extra child components. For example, if you have a base card component, you might extend it to add extra headers or footers while still composing it with other presentational components.
Although mixins are no longer recommended in React (and were never officially supported with ES6 classes), some patterns used to involve mixing in reusable functionality. In modern React development, you can often achieve similar results with higher‑order components or custom hooks. Still, understanding mixins can give you historical context on why extending components might be necessary.
Sometimes, you need to extend a component while also preserving its ref forwarding capability. The official React documentation provides guidance on forwarding refs with class components. When you extend a component that uses refs, be sure to maintain the proper handling of those refs so that parent components can still interact with the underlying DOM elements.
Let’s delve deeper into performance:
super.render()
, you are effectively reusing the parent's output. In many cases, the performance difference is minimal, but if the parent's render method is expensive, you might be duplicating work.componentDidMount
, ensure that any overridden lifecycle methods in the child either call super.componentDidMount()
or re-establish the connection.shouldComponentUpdate
to prevent unnecessary renders. However, be careful when doing so because this optimization might prevent essential updates from being applied.In a real-world application, always use profiling tools (like the React Profiler) to measure the impact of your component hierarchy on performance.
Let’s build a more complete example that extends a base component and then uses composition to add extra functionality. Consider a base component for displaying user cards:
1class BaseUserCard extends React.Component { 2 render() { 3 const { user } = this.props; 4 return ( 5 <div className="user-card"> 6 <h2>{user.name}</h2> 7 <p>Email: {user.email}</p> 8 </div> 9 ); 10 } 11}
Now, suppose you want to add extra features such as a status indicator and a custom greeting. You can extend BaseUserCard
:
1class ExtendedUserCard extends BaseUserCard { 2 render() { 3 const { user } = this.props; 4 return ( 5 <div className="extended-user-card"> 6 {super.render()} 7 <p>Status: {user.status || "Offline"}</p> 8 <p> 9 {user.isPremium 10 ? "Thank you for being a premium member!" 11 : "Upgrade to premium for more benefits."} 12 </p> 13 </div> 14 ); 15 } 16}
1const user = { 2 name: "Alice Johnson", 3 email: "alice@example.com", 4 status: "Online", 5 isPremium: true 6}; 7 8ReactDOM.createRoot(document.getElementById('root')).render( 9 <ExtendedUserCard user={user} /> 10);
This example demonstrates how you can build upon a base component to add additional context and behavior without duplicating the core logic of rendering user information.
TypeScript’s static type checking adds another layer of safety when extending components. Let’s see how you can extend a component with TypeScript:
1import React from 'react'; 2 3interface UserProps { 4 name: string; 5 email: string; 6} 7 8class BaseUserCard extends React.Component<UserProps> { 9 render() { 10 const { name, email } = this.props; 11 return ( 12 <div className="user-card"> 13 <h2>{name}</h2> 14 <p>Email: {email}</p> 15 </div> 16 ); 17 } 18}
1interface ExtendedUserProps extends UserProps { 2 status?: string; 3 isPremium?: boolean; 4} 5 6class ExtendedUserCard extends BaseUserCard { 7 // Notice that ExtendedUserCard automatically inherits props from BaseUserCard. 8 render() { 9 const { name, email } = this.props as ExtendedUserProps; 10 return ( 11 <div className="extended-user-card"> 12 {super.render()} 13 <p>Status: {(this.props as ExtendedUserProps).status || "Offline"}</p> 14 <p> 15 {(this.props as ExtendedUserProps).isPremium 16 ? "Thank you for being a premium member!" 17 : "Upgrade to premium for more benefits."} 18 </p> 19 </div> 20 ); 21 } 22}
1const user: ExtendedUserProps = { 2 name: "Bob Smith", 3 email: "bob@example.com", 4 status: "Active", 5 isPremium: false 6}; 7 8import ReactDOM from 'react-dom/client'; 9 10const root = ReactDOM.createRoot(document.getElementById('root')!); 11root.render(<ExtendedUserCard {...user} />);
Using TypeScript interfaces ensures that both the base and extended components adhere to a well-defined contract, which makes the code easier to understand and maintain.
The decision between extending a component and composing components is one that depends on your use case:
Extend When:
Compose When:
In most modern React projects, composition (often combined with higher‑order components and hooks) is generally preferred over inheritance. However, understanding component extension is still valuable, especially when dealing with legacy code or complex third‑party libraries.
Sometimes you need to blend multiple techniques to achieve the desired outcome. For instance, you might extend a component to reuse its core logic but also use composition to inject additional UI or behavior. Here’s an example that uses both:
Imagine a base component that displays a notification:
1class BaseNotification extends React.Component { 2 render() { 3 return ( 4 <div className="notification"> 5 {this.props.message} 6 </div> 7 ); 8 } 9}
You want an extended version that also shows a timestamp. Instead of completely rewriting the component, you extend it:
1class TimestampedNotification extends BaseNotification { 2 render() { 3 const currentTime = new Date().toLocaleTimeString(); 4 return ( 5 <div className="timestamped-notification"> 6 {super.render()} 7 <small>{`Sent at ${currentTime}`}</small> 8 </div> 9 ); 10 } 11}
Now, if you want to further compose functionality (for example, wrapping this notification in a dismissible container), you can use composition:
1function DismissibleNotification(props) { 2 const [visible, setVisible] = React.useState(true); 3 if (!visible) return null; 4 return ( 5 <div className="dismissible-container"> 6 {props.children} 7 <button onClick={() => setVisible(false)}>Dismiss</button> 8 </div> 9 ); 10} 11 12// Usage: 13ReactDOM.createRoot(document.getElementById('root')).render( 14 <DismissibleNotification> 15 <TimestampedNotification message="This is an important message!" /> 16 </DismissibleNotification> 17);
This example shows how you can extend a base component to add new behavior and then use composition to integrate it into a larger UI structure.
To summarize the concept of extending a component via inheritance, the following Mermaid diagram visually represents a base component and its subclass:
In this diagram, ChildComponent extends BaseComponent (denoted by the inheritance arrow). The child component inherits the render()
method from the base component and can override or enhance it.
React’s ability to extend components via inheritance is a powerful tool for reusing functionality and reducing code duplication. While modern development leans more towards composition, extending components still has its place—especially when working with legacy code. Using super.render()
lets you bring in the output of a base component while adding your own custom logic or UI elements.
As you grow in React, think about how to structure your code for maintainability. Sometimes, custom hooks or higher‑order components will keep things cleaner and more modular. When you need to extend a component, just remember that balance is key. Aim for simplicity and clarity, and always choose the approach that best fits your project.
Happy coding!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.