Lose coupling abstractions in React using TypeScript

jimjja

Jemal Ahmedov

Posted on January 13, 2021

Lose coupling abstractions in React using TypeScript

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"> {}
Enter fullscreen mode Exit fullscreen mode

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"> {}
Enter fullscreen mode Exit fullscreen mode

Now, let's declare the custom Button component

const style = {
  // My custom Button style
};

function Button(props: IButtonProps) {
  return <button {...props} style={style} />;
}
Enter fullscreen mode Exit fullscreen mode

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"
  >;
}
Enter fullscreen mode Exit fullscreen mode

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 }} />;
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
jimjja
Jemal Ahmedov

Posted on January 13, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related