Overview
Building React components with OOP design principles in mind can really take a turn in how the component will behave in the future and how easy it will be to be used. This article is an introduction of the concept for Liskov Substitution Principle and how React components and the benefits of applying it in React.
General Idea
The idea behind the principle is that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. This requires the superclass object to behave the same way as the subclass and to have the same input.
In React terms, if we remove an abstraction of a component, then the component should behave the same way as it was while using the abstraction.
Enforcing the Liskov Substitution Principle in React
Let's see this in action.
We need to build a custom React component library. One of the components we will need to use is a custom Button. The Button component will need to have the same functionality as the usual button except for the style of the button, which will be closed for modification.
The props interface for the button will look like this:
interface IButtonProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, "style"> {}
Let's examine the interface.
-
IButtonProps
extends the HTML attributes of the native HTML<button/>
, e.g.React.HTMLAttributes<HTMLButtonElement>
. This way we can just reuse the attributes from the native<button/>
instead of writing them manually.
The beauty of this approach is that if we decide to ditch the custom Button component and just use the default <button/>
, it will just work.
Another BIG plus for using this approach is that the rest of the team will already be familiar with the custom Button's interface as the props are inherited by the native HTML element.
- The next thing to look at is the word
Omit
, used when declaring the interface.Omit
is a Typescript helper which helps to unselect properties from a provided interface. Omitting multiple props can be done by using the|
operator like this:
interface IButtonProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, "style" | "className"> {}
Now, let's declare the custom Button component
const style = {
// My custom Button style
};
function Button(props: IButtonProps) {
return <button {...props} style={style} />;
}
Another thing that needs mentioning here is how the props are passed to the <button/>
. To make sure that the style
prop cannot be overridden by the props
, by any chance, we should define the style
prop after destructuring the rest of the props
. This way even if style
prop has been passed via the properties, it will be overridden by our custom styling. Even if someone decides to ignore the TypeScript error, this will still prevent them from passing that style
.
This all looks great so far, but let's see another example.
As part of the component library, we need to build a custom Paragraph
component. We need to make sure that we can apply some of the styling, e.g. text-align
, font-weight
... Keep in mind the idea again is to enforce the Liskov Substitution Principle.
For this example we can build our interface as shown below:
interface IParagraphProps extends React.HTMLAttributes<HTMLParagraphElement> {
style?: Pick<
React.CSSProperties,
"textAlign" | "fontWeight"
>;
}
Let's dig in and see what is happening.
The IParagraphProps
extends the native HTML <p/>
element's attributes. Like the custom Button, the idea is to share the same properties as the native element. The next thing defined is the style
property. The word Pick
is another TypeScript helper which allows to pick some of the properties from a predefined interface. In this case, the component will only allow for textAlign
and fontWeight
.
Let's implement the Paragraph component.
const style = {
// My custom Paragraph style
};
function Paragraph(props: IParagraphProps) {
return <p {...props} style={{ ...style, ...props.style }} />;
}
Conclusion
We just saw how the Liskov Substitution Principle can be enforced when building React components using TypeScript. This allows us to reuse the attributes of the native elements on the abstraction and to pick only the functionality the custom components are allowed to implement without breaking the interface between the abstraction and the native element.
Top comments (0)