Understanding the Abstract Method pattern in React and exploring how it can be implemented in your applications.
Today, I am going to showcase ADP(Abstract design pattern) inside react, and how can we achieve it. If you want see this happening in action inside Angular, please read this blog Angular-ADP.
Before jumping into react, we need to know that what is an Abstract design pattern and whether it is there in react too. So, let me make it clear that, in any language or framework we are using in today’s world, to make your code organized and reliable to be bug/error free, you should always adopt a design pattern. A design pattern is universal, you can implement in any language as it states only the way you are designing your application.
It is a misconception that a design pattern is used for increasing the performance only. Its primary goal is to promote code reuse and remove redundancy by defining a common structure and behavior.
So, let’s get started
If you don't know what is an ADP(Abstract Design Pattern), please read this blog Angular-ADP.
In React, the Abstract Method pattern provides a way to define a common structure for components while allowing specific implementations to be defined in child components. This pattern promotes code reuse, reduces redundancy, and helps maintain a clean and scalable codebase. In this article, we will explore the Abstract Method pattern in React and demonstrate its implementation using a functional approach. We will showcase detailed examples and discuss best practices to remove redundancy and avoid code smells.
Understanding the Abstract Method Pattern:
The Abstract Method pattern is based on the concept of abstract classes and methods in object-oriented programming. While React does not directly support abstract classes, we can achieve a similar effect by leveraging higher-order components (HOCs) or custom hooks.
Example Scenario:
Consider a scenario in which you want to create a notification system in your application that supports different types of notifications, such as success, error, and warning. We want to define a common structure for these notifications, but allow specific rendering and behavior for each type
Implementation steps
- Defines an AbstractNotificationcomponent that describes the general structure and behavior of notifications. These components can contain common user interface elements and functions common to all types of notifications.
- Create child components, such as SuccessNotification, ErrorNotification, and WarningNotification, that extend the AbstractNotification component. These child components will provide the specific rendering and behavior for each notification type.
- Implement abstract methods in AbstractNotification component. This method can serve as a placeholder to override certain logic in child components. For example, you could define an abstract method called renderDataContent for child components to implement to render content specific to each notification type.
- Utilize the abstract method within the AbstractNotification component's render method. This allows child components to inject their specific content while maintaining the common structure provided by the abstract component.
// AbstractNotification.js
import React from 'react';
const AbstractNotification = ({ title, message }) => {
const renderDataContent= () => {
throw new Error('Child component must override the renderDataContent method.');
};
return (
<div className="notification">
<h2>{title}</h2>
<div className="content">{renderDataContent()}</div>
</div>
);
};
------------------------------------------------------------------------------
export default AbstractNotification;
// SuccessNotification.js
import React from 'react';
import AbstractNotification from './AbstractNotification';
const SuccessNotification = ({ title, message }) => {
const renderDataContent= () => {
return <p className="success-message">{message}</p>;
};
return <AbstractNotification title={title} message={message} renderDataContent={renderDataContent} />;
};
export default SuccessNotification;
------------------------------------------------------------------------------
// App.js
import React from 'react';
import SuccessNotification from './SuccessNotification';
const App = () => {
return (
<div className="app">
<SuccessNotification title="Success!" message="Operation completed successfully." />
</div>
);
};
export default App;
In the above code, we have the AbstractNotification component that serves as the abstract component. It receives title and message props and defines an abstract method called renderDataContent. The renderDataContentmethod throws an error, indicating that child components must override it.
In the SuccessNotification component, we define the renderDataContentmethod specific to the success notification. Here, we return a p tag element with a CSS class for styling.
In the App component, we import and render the SuccessNotification component. We pass the title and message props specific to the success notification.
By following this pattern, you can easily create additional child components, such as ErrorNotification or WarningNotification, that extend the abstract component and provide their own implementation of the renderDataContentmethod.
This approach helps remove redundancy and code smells by centralizing common functionality in the abstract component while allowing child components to focus on their specific rendering and behavior.
Remember to apply appropriate error handling and validation in the abstract component to ensure that child components correctly implement the required abstract methods.
This one is the easy one to demo the React ADP pattern.Lets see other more realistic demo
Let’s continue to see it for a finance app that focuses on managing personal finances using the abstract method pattern in React.
First, let’s create the abstract component for a financial transaction:
// AbstractTransaction.js
import React from 'react';
const AbstractTransaction = ({ transaction }) => {
const formatAmount = () => {
throw new Error('Child component must override the formatAmount method.');
};
return (
<div className="transaction">
<h3>{transaction.description}</h3>
<p>Amount: {formatAmount()}</p>
<p>Date: {transaction.date}</p>
</div>
);
};
export default AbstractTransaction;
In the above code, we have the AbstractTransaction component as our abstract component. It receives a transaction prop and defines an abstract method called formatAmount. The formatAmount method throws an error, indicating that child components must override it.
Now, let’s create a child component that extends the abstract component for an expense transaction:
// ExpenseTransaction.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
const ExpenseTransaction = ({ transaction }) => {
const formatAmount = () => {
// Display the expense amount with a negative sign
return `- $${transaction.amount}`;
};
return <AbstractTransaction transaction={transaction} formatAmount={formatAmount} />;
};
export default ExpenseTransaction;
In the ExpenseTransaction component, we provide the implementation for the formatAmount method specific to an expense transaction. Here, we display the expense amount with a negative sign.
Next, let’s create a child component that extends the abstract component for an income transaction:
// IncomeTransaction.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
const IncomeTransaction = ({ transaction }) => {
const formatAmount = () => {
// Display the income amount with a positive sign
return `+ $${transaction.amount}`;
};
return <AbstractTransaction transaction={transaction} formatAmount={formatAmount} />;
};
export default IncomeTransaction;
In the IncomeTransaction component, we provide the implementation for the formatAmount method specific to an income transaction. Here, we display the income amount with a positive sign.
Now, let’s create a child component that extends the abstract component for a transfer transaction:
// TransferTransaction.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
const TransferTransaction = ({ transaction }) => {
const formatAmount = () => {
// Display the transfer amount without any sign
return `$${transaction.amount}`;
};
return <AbstractTransaction transaction={transaction} formatAmount={formatAmount} />;
};
export default TransferTransaction;
In the TransferTransaction component, we provide the implementation for the formatAmount method specific to a transfer transaction. Here, we display the transfer amount without any sign.
Now, let’s use these components in our financial app:
// FinancialApp.js
// FinancialApp.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
import ExpenseTransaction from './ExpenseTransaction';
import IncomeTransaction from './IncomeTransaction';
import TransferTransaction from './TransferTransaction';
const FinancialApp = () => {
const transactions = [
{ description: 'Groceries', amount: 50, date: '2023-05-01' },
{ description: 'Salary', amount: 2000, date: '2023-05-15' },
{ description: 'Transfer to Savings', amount: 500, date: '2023-05-10' },
];
return (
<div className="financial-app">
<h2>Transactions</h2>
{transactions.map((transaction, index) => (
<div key={index}>
<ExpenseTransaction transaction={transaction} />
<IncomeTransaction transaction={transaction} />
<TransferTransaction transaction={transaction} />
</div>
))}
</div>
);
};
export default FinancialApp;
In the updated FinancialApp component, we have added a transfer transaction to the transactions array. We now render the TransferTransaction component alongside the ExpenseTransaction and IncomeTransaction components for each transaction.
By extending the abstract component and overriding the formatAmount method, we can handle different types of financial transactions with their specific formatting requirements. This approach helps remove code redundancy and promotes code reuse and maintainability.
In a real-world financial app , you can further extend the child components to include additional functionality or customize the rendering based on specific transaction types. The abstract method pattern provides a flexible and scalable approach to handle diverse financial transactions while keeping the core logic centralized in the abstract component.
This pattern helps remove redundant code and promotes code reuse and maintainability in our financial app.
To improve performance in rendering the transactions in our financial app, we can make use of React’s memoization technique and optimize the child components.
Let’s modify the child components (ExpenseTransaction, IncomeTransaction, and TransferTransaction) to leverage React's React.memo to memoize the components and prevent unnecessary re-renders:
// ExpenseTransaction.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
const ExpenseTransaction = React.memo(({ transaction }) => {
const formatAmount = () => {
// Display the expense amount with a negative sign
return `- $${transaction.amount}`;
};
return <AbstractTransaction transaction={transaction} formatAmount={formatAmount} />;
});
export default ExpenseTransaction;
Similarly, update the IncomeTransaction and TransferTransaction components with React.memo:
// IncomeTransaction.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
const IncomeTransaction = React.memo(({ transaction }) => {
const formatAmount = () => {
// Display the income amount with a positive sign
return `+ $${transaction.amount}`;
};
return <AbstractTransaction transaction={transaction} formatAmount={formatAmount} />;
});
export default IncomeTransaction;
-------------------------------------------------------------------------
// TransferTransaction.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
const TransferTransaction = React.memo(({ transaction }) => {
const formatAmount = () => {
// Display the transfer amount without any sign
return `$${transaction.amount}`;
};
return <AbstractTransaction transaction={transaction} formatAmount={formatAmount} />;
});
export default TransferTransaction;
By memoizing the child components using React.memo , we ensure that the components are only re-rendered if their props (transaction in this case) have changed. This optimization can significantly improve the performance of the financial app, especially when dealing with a large number of transactions.
Additionally, you can further optimize the financial app by implementing a key prop when rendering the child components. The key prop should be a unique identifier associated with each transaction to help React efficiently update and re-render the components when necessary.
{transactions.map((transaction, index) => (
<div key={transaction.id}>
<ExpenseTransaction transaction={transaction} />
<IncomeTransaction transaction={transaction} />
<TransferTransaction transaction={transaction} />
</div>
))}
By providing a unique key for each transaction, React can perform more efficient updates and prevent unnecessary re-renders of the child components.
These performance optimizations help ensure that the financial app runs smoothly and efficiently, even when dealing with a large number of transactions . By memoizing the components and providing proper keys, we can minimize unnecessary re-renders and improve the overall performance of the app.
Let’s further improve the code to make it more generic and performant.
We’ll refactor the child components (ExpenseTransaction, IncomeTransaction, and TransferTransaction) to eliminate code redundancy and improve performance by avoiding unnecessary re-renders.
First, let’s create a single optimized component called TransactionItem that handles all transaction types:
// TransactionItem.js
import React from 'react';
import AbstractTransaction from './AbstractTransaction';
const TransactionItem = React.memo(({ transaction, formatAmount }) => {
return <AbstractTransaction transaction={transaction} formatAmount={formatAmount} />;
});
export default TransactionItem;
The TransactionItem component accepts the transaction object and the formatAmount function as props, which will be specific to each transaction type.
Next, we can create a higher-order component (HOC) called withTransactionType that takes in a transaction type and returns a memoized component with the appropriate formatAmount function:
// withTransactionType.js
import React from 'react';
const withTransactionType = (transactionType, formatAmount) => {
const TransactionComponent = React.memo(({ transaction }) => {
return <TransactionItem transaction={transaction} formatAmount={formatAmount} />;
});
return TransactionComponent;
};
export default withTransactionType;
Now, we can create specific transaction components using the withTransactionType HOC. For example, let's create the ExpenseTransaction component:
// ExpenseTransaction.js
import React from 'react';
import withTransactionType from './withTransactionType';
const formatExpenseAmount = (amount) => `- $${amount}`;
const ExpenseTransaction = withTransactionType('expense', formatExpenseAmount);
export default ExpenseTransaction;
Similarly, create the IncomeTransaction and TransferTransaction components:
// IncomeTransaction.js
import React from 'react';
import withTransactionType from './withTransactionType';
const formatIncomeAmount = (amount) => `+ $${amount}`;
const IncomeTransaction = withTransactionType('income', formatIncomeAmount);
export default IncomeTransaction;
----------------------------------------------------------------------
// TransferTransaction.js
import React from 'react';
import withTransactionType from './withTransactionType';
const formatTransferAmount = (amount) => `$${amount}`;
const TransferTransaction = withTransactionType('transfer', formatTransferAmount);
export default TransferTransaction;
Now, we can use these components in our financial app:
// FinancialApp.js
import React from 'react';
import TransactionItem from './TransactionItem';
const FinancialApp = () => {
const transactions = [
{ type: 'expense', description: 'Groceries', amount: 50, date: '2023-05-01' },
{ type: 'income', description: 'Salary', amount: 2000, date: '2023-05-15' },
{ type: 'transfer', description: 'Transfer to Savings', amount: 500, date: '2023-05-10' },
];
return (
<div className="financial-app">
<h2>Transactions</h2>
{transactions.map((transaction, index) => {
const { type, ...rest } = transaction;
return (
<div key={index}>
<TransactionItem transaction={rest} formatAmount={getFormatAmount(type)} />
</div>
);
})}
</div>
);
};
const getFormatAmount = (type) => {
// Define the formatAmount function based on transaction type
switch (type) {
case 'expense':
return (amount) => `- $${amount}`;
case 'income':
return (amount) => `+ $${amount}`;
case 'transfer':
return (amount) => `$${amount}`;
default:
return null;
}
};
export default FinancialApp;
In the above code, we define the getFormatAmount function, which takes in the transaction type and returns the appropriate formatAmount function based on the type.
Now, when rendering the transactions, we pass the transaction data (rest) and the corresponding formatAmount function to the TransactionItem component.
By organizing the code in this manner, we achieve a more generic and performant solution. The TransactionItem component handles the common logic for rendering transactions, while the specific formatAmount functions are provided dynamically based on the transaction type.
This approach eliminates code redundancy , reduces the number of components, and improves performance by leveraging memoization and avoiding unnecessary re-renders.
Additionally, by using a key prop when rendering the TransactionItem component, we enable efficient updates and re-renders:
<div key={index}>
<TransactionItem transaction={rest} formatAmount={getFormatAmount(type)} />
</div>
Providing a unique key for each transaction helps React accurately track and update only the necessary components, further optimizing performance.
With these improvements, the financial app can efficiently manage and render various types of transactions while maintaining code cleanliness, reusability, and performance.
It’s important to profile and analyze the performance of your application using tools like React DevTools, Chrome DevTools, or other performance monitoring tools. By identifying and addressing specific performance bottlenecks, you can optimize the code and improve the overall performance of your financial app.
In the provided code, there are a few potential areas where performance issues could arise
let see what are they and how to avoid it in the next upcoming article. Here is the link https://vivekdogra02.medium.com/react-abstract-design-pattern-dry-single-shared-responsibility-9fbef42a6e56
So, stay tuned guys.
Hope you liked it,
Start incorporating the Abstract Design pattern into your React projects and experience the benefits firsthand. Happy coding!
Thanks for reading
- 👏 Please clap for the story and follow me 👉
- 📰 View more content
- 🔔 Follow me: LinkedIn| Twitter| Github
Top comments (0)