Reusable React Components: Techniques and Strategies

One of the principles of software development is DRY (Don’t Repeat Yourself), which is fully applied in React through the creation of reusable components that can be used multiple times within an application. The ability to write reusable components be…


This content originally appeared on Level Up Coding - Medium and was authored by Davide Francesco Merico

One of the principles of software development is DRY (Don’t Repeat Yourself), which is fully applied in React through the creation of reusable components that can be used multiple times within an application. The ability to write reusable components becomes even more important when developing a component library to be used by third parties.

In this article, I want to go in-depth into various points to consider when creating a reusable component, which involves not only the technical aspect of implementation but also a more theoretical part of the ease of use for other developers who will use them.

To provide some context before starting, I should mention that the points raised in this article were identified during the development of a component library using tailwindCSS. If you would like to read more about this topic, you can refer to an article I wrote regarding the configuration of an accessible palette. Currently, I use Typescript and Sass by default in my projects, so any of the below examples will take into account their typing and styling.

1. Improving Props Names

One of the first theoretical aspects to consider is the naming convention to use when declaring components’ props.

In these instances, the best practice would be to use the naming convention already present in React, camelCase for props’ names and PascalCase for components’ names. We can therefore define the following rules:

  • on{Event}: functions that will be called when an action or event occurs. Examples: onClick, onReset, or more specific ones like onChangeUsername, onDownloadExcel.
  • is{Adjective}/has{Property}: boolean flags that inform the component of situations that change its behavior. Examples: isSelected, isAuthenticated or hasPeers, hasPhoneNumber.
  • with{Element}/no{Element}: more general properties (less common) that could sometimes be replaced by using is- or has- flags. I find using this convention suitable when informing a component that it should add or remove an element from its rendering. Additionally, these properties can be used both as boolean flags and as optional additional data to be included in the component. Examples: withIcon, withPopup, noStyle.

Regarding properties containing actual data, there is relatively more freedom. In the following section, I offer some ideas for improving the use of these properties.

2. Conforming with Other Libraries

Every developer wants to make their work unique, but sometimes standing out too much from widely used tools can make it frustrating for other developers to use our components. To avoid making our components difficult or cumbersome to use, it’s advisable to look for what we need into more famous libraries and incorporate the properties and use cases they were designed with into our library.

For a practical example, let’s imagine creating a UI library that includes various types of buttons. Other famous libraries we can use as reference are: Material UI, Primereact, Ant Design, etc.

In this case, we choose the one we or the Developer Team are most comfortable or familiar with. From there, we might structure our buttons with the following custom properties: color, endIcon, size, startIcon, variant.

3. Allowing for Specialization During Use

Moving on to a more practical topic, it’s essential to remember that no matter how hard we try to anticipate use cases, there will always be a particular situation where our component cannot adapt.

This lack of adaptability isn’t just our limitation, but it also concerns aspects of display style and accessibility. A real example might be using the “Button” component, which, depending on the action and label associated with it, will have different behaviors. In this case, only during the specific use of the component can the developer add ARIA properties and other information useful for testing or debugging.

Translating this concept into code, our Button component can be initialized as follows:import React, { PropsWithChildren } from 'react'
import { UIComponentSize, UIThemePalette } from '../types'

export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
color: UIThemePalette
endIcon: React.ReactNode
size: UIComponentSize
startIcon: React.ReactNode
variant: ButtonVariant
}

export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
children,
className,
color,
disabled,
endIcon,
size,
startIcon,
variant,
 …buttonProps
}) => {
return (
<button className={className} disabled={disabled} {...buttonProps}>
{!!startIcon && <span>{startIcon}</span>}
<span>{children}</span>
{!!endIcon && <span>{endIcon}</span>}
</button>
)
}

From this example, you can see that to type the attributes normally present in the HTML tag, we used the type React.ButtonHTMLAttributes (or the more generic variant React.HTMLAttributes) that React provides to extract properties from an interface of an HTML element and adds various base handlers like onClick, onKeyDown, and so on. In this specific case, for the element was used HTMLButtonElement, which contains all the properties assignable to the <button /> element.

While writing the component, we could use properties like className or disabled without defining them, since they are already included in the base properties of the button. All additional properties that we haven’t used will be present in the buttonProps variable and can be provided without modification to the actual component.

In this example, no optional properties or other logic for applying styles were used; we’ll address this point in the next sections.

4. Making the Component Easy to Use

As mentioned in previous sections, it is advisable to simplify the use of our components as much as possible. This goal can be achieved through writing documentation (using JSDoc) and paying attention to some details during the definition stage of the props.

For example, a first step could be to mark as optional all the non-essential attributes for the basic rendering of the component. It is recommended to make boolean properties optional and ensure they are always “enabling”, meaning that their default behavior is a false value. This way, they will be defined only when necessary.

For properties that cannot be associated with boolean values, a default value can be assigned.

Revisiting the Button component previously used, an example could be as follows:

import React, { PropsWithChildren } from 'react'
import { DefaultProps, UIComponentSize, UIThemePalette } from '../types'

import './Button.scss'

export type ButtonVariant = 'contained' | 'outlined' | 'text'

export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
color?: UIThemePalette
endIcon?: React.ReactNode
size?: UIComponentSize
startIcon?: React.ReactNode
variant?: ButtonVariant
}

export const ButtonDefaultProps: DefaultProps<ButtonProps, 'color' | 'size' | 'variant'> = {
color: 'primary',
size: 'medium',
variant: 'contained',
}

export const Button: React.FC<PropsWithChildren<ButtonProps>> = ({
children,
className,
color = ButtonDefaultProps.color,
disabled,
endIcon,
size = ButtonDefaultProps.size,
startIcon,
variant = ButtonDefaultProps.variant,
...buttonProps
}) => {
return (
<button className={className} disabled={disabled} {...buttonProps}>
{!!startIcon && <span>{startIcon}</span>}
<span>{children || ''}</span>
{!!endIcon && <span>{endIcon}</span>}
</button>
)
}

In this case, all properties will be optional, and only the color, size and variant props will have default values. This way, the component can be used immediately in its base version as follows:

<Button>Click Me</Button>

The generic type DefaultProps allows us to extract from the base properties the typing of those ones for which we need to declare a default value. Its definition is the following:

export type DefaultProps<Component, Props extends keyof Component> = Readonly<
Partial<Omit<Component, Props>> & Required<Pick<Component, Props>>
>

5. Reusing What We Have Already Created

When developing UI library components, we will likely be the first ones to use them. Once the basic components (e.g.: buttons, typography elements, input fields) are created, we will move on to developing more complex ones.

To bring more coherence and ease of development, it is good to reuse what we have already created.

A practical example might be a pagination element that involves:

  • logic on the number of pages to display
  • actions to quickly move to the first or last page
  • other application logics

From a rendering perspective, however, this component will use buttons and texts. In this case, before building the <Pagination /> component, we need to create the following components:

  • <ButtonsGroup /> to create groups of buttons
  • <Button /> to manage the rendering of the single button
  • <Text /> to standardize typography styles
  • Other useful logic components like a <Skeleton /> or an <Icon />

6. Order when Managing Styles

The style of the component often changes depending on the project’s requirements and specific situations that demand a behavior different from the default one. Therefore, it is necessary to think about how to allow customization of our components during use.

A first aid comes from the classnames library, which allows applying specific classes only if certain conditions are met:

// variant = 'contained', fullWidth = false
const classes = classNames('ui-button', variant, { 'w-full': fullWidth }) // 'ui-button contained'

Another focus point concerns how to apply style to the components. We can choose between CSS Modules or more generic class names. Both solutions present advantages and disadvantages that must be subjectively evaluated:

CSS Modules

Pros: avoid conflicts with other libraries and styles used in the application, easier to reuse, and allows tree-shaking.

Cons: Difficult to customize by external components.

Public Classes

Pros: Better ease of customization by external components and easier application of nested styles.

Cons: Higher likelihood of conflict and difficulty of reuse in different parts of the application, at the expense of the generated bundle size.

When using public classes, the risk of conflict can be mitigated by using a custom prefix on each class that is used by our components.

Another advice related to style application is to avoid using margins as a default, unless explicitly related to the component’s function. This way, it will be the developer to define the best positioning of the component within the layout.

The last aspect to consider is the theme of the component. This point can be divided into two possible solutions:

  • CSS Variables: Set a color palette within CSS variables that can then be customized by the main application. The advantages are being independent from specific libraries for style application and an easier definition during runtime.
  • SASS Variables: Set the palette and other values using SASS variables to allow more precise manipulation of values using mixins. The advantages are, besides more advanced control over the values used, also the possibility of automatically generating class variants used by the components.

7. Organize the file structure and allow tree-shaking

When the number of components starts to increase, it is necessary to establish an internal file organization. As already suggested in one of the first sections, files can be ordered into folders by category. To know which categories to use, we can use existing libraries as reference.

In my specific case, I have organized the components into the following categories: DataDisplay, Feedback, Inputs, Navigation, and Surfaces.

At the same level as the categories, files containing all definitions that are independent of the specific use of components can be added. An example is the types’ and style’s variables:

// types.ts
export const UIThemePaletteValues = ['primary', 'secondary', 'accent', 'success', 'error', 'info', 'warning'] as const
export type UIThemePalette = (typeof UIThemePaletteValues)[number]

export const UIComponentSizeValues = ['small', 'medium', 'large'] as const
export type UIComponentSize = (typeof UIComponentSizeValues)[number]

export const UISizeValues = ['sm', 'md', 'lg', 'xl', '2xl'] as const
export type UISize = (typeof UISizeValues)[number]

export const UIStatesValues = ['error', 'info', 'success', 'warning'] as const
export type UIStates = (typeof UIStatesValues)[number]
// variables.scss
$colors: 'primary', 'secondary', 'accent', 'success', 'error', 'info', 'warning';
$states: 'error', 'info', 'success', 'warning';

Continuing with individual components, files can be placed within the reference category, using the same name for both the style and the component file, and eventually also for the test file.

ui-components/
├─ Feedback/
│ ├─ Progress.scss
│ ├─ Progress.tsx
│ ├─ Skeleton.scss
│ ├─ Skeleton.tsx
│ ├─ index.ts

In this example, the index.ts file has also been added. This practice is recommended to facilitate the use of components in each category, as well as potentially allowing us to add internal logic for exporting components. A simple index file can be created as follows:

// Feedback/index.ts
import { Progress } from './Progress'
import { Skeleton } from './Skeleton'

export { Progress, Skeleton }

8. Allow DOM manipulation

A small detail that can be useful in some situations is to allow the developers to add a reference to the component. This will probably change in the next React release, but can currently be implemented as follows:

import { forwardRef, PropsWithChildren } from 'react'

export const CustomElement = forwardRef<HTMLDivElement, PropsWithChildren<CustomElementProps>>(
({ children, …divProps }, ref) => {

return <div ref={ref} {…divProps}>{children}</div>
})

9. Generalize the used DOM

A use case that arises in some specific situations is to give the developer the possibility to define which HTML element to use for rendering the component each time. A practical example of this situation is the typographic aspect: imagine having our component that simplifies and standardizes the management of typographic variants but, depending on the use case within the page, it must be represented in the DOM via an h1 element or h2, h3, p, etc.

From the point of view of typing, we could use this definition:

export type TypographyProps<T extends React.ElementType = React.ElementType> = ComponentPropsWithoutRef<T> & {
align?: UIHorizontalAlignment | 'justify'
as?: T
noWrap?: boolean
variant?: UITypographyVariants | 'inherit'
strong?: boolean
italic?: boolean
underline?: boolean
}

In this type, we have defined a generic type T that can be associated with any HTML element that we will read through the as property. When we will use this property, it will be linked with an alias in PascalCase and it will be used directly as a component itself. (eg.: <Text as="h1">Hello</Text> )

const TypographyDefaultProps: DefaultProps<TypographyProps, 'align' | 'as' | 'variant'> = {
align: 'left',
as: 'span',
variant: 'inherit',
}

const Typography: React.FC<PropsWithChildren<TypographyProps>> = ({
align = TypographyDefaultProps.align,
as: Element = TypographyDefaultProps.as,
className,
children,
noWrap,
variant = TypographyDefaultProps.variant,
strong,
italic,
underline,
...props
}) => {
return (
<Element
className={classNames(
'ui-typography',
`text-${align}`,
variant,
{
strong,
italic,
'underline underline-offset-2': underline,
'whitespace-nowrap': noWrap,
},
className,
)}
{...props}
>
{children}
</Element>
)
}

This will be a generic typographic component that will be rendered by default using a <span /> element. To simplify its use, we can create additional components that use it to set basic attributes for titles, labels, or links:

const Title: React.FC<PropsWithChildren<TypographyProps>> = ({
as = 'h1',
className,
children,
variant = 'title',
...props
}) => (
<Typography as={as} variant={variant} className={classNames('ui-title', className)} {...props}>
{children}
</Typography>
)

const Text: React.FC<PropsWithChildren<TypographyProps>> = ({
as = 'p',
className,
children,
variant = 'paragraph',
...props
}) => (
<Typography as={as} variant={variant} className={classNames('ui-text', className)} {...props}>
{children}
</Typography>
)

const Link: React.FC<PropsWithChildren<TypographyProps & { color?: UIThemePalette }>> = ({
as = 'a',
className,
children,
color = 'primary',
variant = 'paragraph',
...props
}) => (
<Typography as={as} variant={variant} className={classNames('ui-link', className, color)} {...props}>
{children}
</Typography>
)

This way, we could directly use a <Link /> component for navigation, another <Title /> component for titles, and a <Text /> component for normal labels.

Conclusions

The above are the key points that, in my experience, could help us to create some strongly typed React components, which are easier to reuse.

I haven’t included some aspects regarding particular typing cases, but generally I would recommend using generic types to allow the developer to easily recall the correct data typing, while using the component. An interesting typing exercise like the one just mentioned, is to create a <Table /> component that allows defining the data to be displayed and potentially customizing the rendering type for each field of the source data provided.

In case you are interested in delving deeper into some of these points, have questions, or just want to provide feedback, feel free to comment or contact me. Happy Coding!


Reusable React Components: Techniques and Strategies was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Davide Francesco Merico


Print Share Comment Cite Upload Translate Updates
APA

Davide Francesco Merico | Sciencx (2024-07-21T17:03:15+00:00) Reusable React Components: Techniques and Strategies. Retrieved from https://www.scien.cx/2024/07/21/reusable-react-components-techniques-and-strategies/

MLA
" » Reusable React Components: Techniques and Strategies." Davide Francesco Merico | Sciencx - Sunday July 21, 2024, https://www.scien.cx/2024/07/21/reusable-react-components-techniques-and-strategies/
HARVARD
Davide Francesco Merico | Sciencx Sunday July 21, 2024 » Reusable React Components: Techniques and Strategies., viewed ,<https://www.scien.cx/2024/07/21/reusable-react-components-techniques-and-strategies/>
VANCOUVER
Davide Francesco Merico | Sciencx - » Reusable React Components: Techniques and Strategies. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/07/21/reusable-react-components-techniques-and-strategies/
CHICAGO
" » Reusable React Components: Techniques and Strategies." Davide Francesco Merico | Sciencx - Accessed . https://www.scien.cx/2024/07/21/reusable-react-components-techniques-and-strategies/
IEEE
" » Reusable React Components: Techniques and Strategies." Davide Francesco Merico | Sciencx [Online]. Available: https://www.scien.cx/2024/07/21/reusable-react-components-techniques-and-strategies/. [Accessed: ]
rf:citation
» Reusable React Components: Techniques and Strategies | Davide Francesco Merico | Sciencx | https://www.scien.cx/2024/07/21/reusable-react-components-techniques-and-strategies/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.