SOLID is an acronym for five key principles of object-oriented programming that aim to improve the readability, maintainability, extensibility, and testability of code. However, SOLID principles are not limited to object-oriented programming that uses classes. They can also be applied to other paradigms, such as functional programming, that use functions, modules, or components as the main building blocks of software. With this idea, we can apply SOLID principle when building a frontend app.
Single Responsibility Principle
This principle states that a module/class/function should have only one responsibility and one reason to change. For example, we can also use custom hooks to encapsulate the logic for fetching data, managing state, or performing side effects.
For example, if we have a page/component to render list of schedules, any unrelated tasks like fetching data from server should be handled by other module.
// src/hooks/useDataSchedule.ts
export const useDataSchedule = () => {
const fetcher = (url: string) =>
fetch(process.env.NEXT_PUBLIC_API_URL + url).then((res) => (res.json()));
const { data, error, isLoading, mutate } = useSWR<TSchedule[], Error>(
'/api/schedule/list/',
fetcher
);
return {
data,
error,
isLoading,
mutate,
};
}
// src/components/schedule/ScheduleList.tsx
const ScheduleList = () => {
// We call the hook and retrieve the Schedule
const { data, error, isLoading, mutate } = useDataSchedule();
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
return (
<div>
<h1>Schedule List:</h1>
<ul>
{data?.map((schedule) => (
<li key={schedule.id}>
<h2>{schedule.name}</h2>
<ScheduleDetail title={schedule.detail.title}
startTime={schedule.detail.start_time}
endTime={schedule.detail.end_time} />
</li>
))}
</ul>
</div>
);
};
The benefit of applying the single responsibility principle is that it makes the code more modular, maintainable, and testable. By separating the concerns of different modules, we can avoid coupling and dependency issues that may arise when changing or adding new features. We can also reuse the modules in different contexts, such as different components or pages, without duplicating the code.
Open-closed principle
A class or module should be open for extension, but closed for modification. This means that we should be able to add new features or behaviors without changing the existing code. This principle can be fulfilled when we create a reuseable components for our project.
// src/components/common/CustomButton.tsx
// Interface for the props of the button component
interface CustomButtonProps {
text: string;
className: string;
onClick: () => void;
};
// A button component that takes a string and applies a tailwind class to the button element
export const CustomButton: React.FC<ColorButtonProps> = ({ text, className, onClick }) => {
return (
<button
type="button"
className={`text-white font-bold py-2 px-4 rounded ${className}`}
onClick={onClick}
>
{text}
</button>
);
};
//src/components/modules/LoginModule/buttons.tsx
import { CustomButton } from '@components/common'
export const LandingPageModule = () => {
return (
<div className="flex flex-col">
<CustomButton text="Login" className="bg-green" />
<CustomButton text="Create account" className="bg-gray" />
</div>
)
}
When we have a reuseable components like above code, we can simply add add more styling to the button by passing it through the props but we can't change the default styling of the CustomButton
.
Liskov substitution principle
A subclass or subcomponent should be able to replace its superclass or supercomponent without breaking the functionality. This means that we should follow the contract or interface defined by the parent class or component. The explanation may sounds a lot harder than the implementation.
We can modify the previous example to adhere Liskov substituton principle.
// src/components/common/CustomButton.tsx
// interface for the custom button component
interface ICustomButton extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode
className: string;
};
// A button component that takes a string and applies a tailwind class to the button element
export const CustomButton:React.FC<ICustomButton> = ({ text, className, child, ...props}) => {
return (
<button
type="button"
className={`text-white font-bold py-2 px-4 rounded ${className}`}
{...props}
>
{child}
</button>
);
};
We give the new button the attributes that we have inherited from the original button. This preserves program behavior and adhere the Liskov Substitution Principle by allowing any instance of CustomButton to be used in place of an instance of Button.
Liskov Substitution in React essentially promotes the development of a unified and adaptable component structure for creating reliable and manageable user interfaces.
Interface Segregation Principle
In React interface segregation means that components should have simple and specific interfaces that suit their purpose. We should avoid creating component interfaces that have too many or irrelevant properties or methods. This way, the code becomes more organized, easy to understand, and easy to change, as components are smaller and more focused. ISP helps improve the quality and performance of the code by using component interfaces that are clear and concise.
// src/components/schedule/ScheduleDetail.tsx
interface IScheduleDetail {
title:string
startTime:string
endTime:string
}
const ScheduleDetail = ({ title, startTime, endTime}:IScheduleDetail) => {
return (
<div>
<h3>{title}</h3>
<h4>Time Duration</h4>
<p>{startTime} - {endTime}</p>
</div>
);
};
To implement this principle we can simply limit the props to only what the component need.
Dependency inversion principle
This principle states that a high-level module/class should not depend on low-level module/class, both should depend abstractions, not on concretions.
For example, when we have a create schedule feature and edit schedule feature that using the same field of form, we should abstract the form to adhere this principle.
export const ScheduleForm = ({ onSubmit }: () => void) => {
return (
<form onSubmit={onSubmit}>
<input name="title" />
<input name="startTime" />
<input name="endTime" />
</form>
);
};
const CreateScheduleForm = () => {
const handleCreateSchedule = async () => {
try {
// Logic to handle create schedule
} catch (err) {
console.error(err.message);
}
};
return <ScheduleForm onSubmit={handleCreateSchedule} />;
};
const UpdateScheduleForm = () => {
const handleUpdateSchedule= async () => {
try {
// Logic to handle update schedule
} catch (err) {
console.error(err.message);
}
};
return <ScheduleForm onSubmit={handleUpdateSchedule} />;
};
Implementing this principle will give us a better separation of concerns and more scalable code.
Top comments (1)
nice read fajar!