Создание UI-компонентов React на продвинутом уровне

React  —  хорошая библиотека. Так давайте сделаем ее еще лучше, создавая многоразовые компоненты на высокопрофессиональном уровне.

Для использования этого руководства достаточно базовых знаний о React, TypeScript и Tailwind CSS.

В качестве примера поработаем с button, но принципы, которым будем следовать здесь, применимы к любому HTML-тегу.

const Button = ({children, className}) => {
return (
<button className={className}>{children}</button>
)
}

Приведенный выше код работает, но что, если понадобится ввести ARIA (accessible rich internet application attribute  —  доступные полнофункциональные интернет-приложения), ссылки или любые другие свойства обычного тега button?

Можно просто добавить больше свойств и на этом закончить.

const Button = ({children, className, ref, type}) => {
return (
<button className={className} ref={ref} type={type}>{children}</button>
)
}

Это одно из решений, но оно не эффективно. Чтобы сделать его более действенным, поработаем с TypeScript и React. Быстро создать проект с использованием React и TypeScript поможет документация Vite.


Первым шагом будет импорт необходимых типов из React и создание интерфейса для свойств кнопки (button).

Тем, кто не знаком с TypeScript и интерфейсами, замечу: интерфейсы позволяют расширять типы, что является важной функцией при создании компонента.

import React, { FC } from "react";

interface ButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {

}

const Button = () => {
return (
<button></button>
)
}

Если вы работаете в VS Code, то для устранения сомнений при выборе типа, который надо расширить, наведите курсор на тег button и воспользуйтесь поддержкой IntelliSense (системы автодополнения ввода).

Это будет выглядеть примерно так:

(property) JSX.IntrinsicElements.button: 
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>

Нас интересует часть “React.DetailHTMLProps…”, поэтому просто скопируйте ее и вставьте после ключевого слова “extends” в свой интерфейс.

Теперь соберем все вместе.

import React, { FC } from "react";

interface ButtonProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {}

const Button: FC<ButtonProps> = ({ className, children, ...props }) => {
return (
<button className={className} {...props}>
{children}
</button>
);
};

Примерно так должен выглядеть компонент. Теперь придадим ему индивидуальности.

Начнем со стилизации, для чего потребуется установка фреймворка Tailwind CSS.

Прежде чем приступить к созданию стилей, понадобится еще пара инструментов. Откройте терминал и установите следующие зависимости:

npm i class-variance-authority clsx tailwind-merge

После установки создайте файл utils в папке lib внутри папки src и вставьте в него следующее:

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

Вспомогательная функция Tailwind-merge поможет объединить классы Tailwind, например “px-5 py-5” превратится в “p-5”.

Вернувшись в файл button, будем создавать вариации состояний (под вариациями я подразумеваю предопределенные стили, такие как размеры и цвета).

Для этого импортируем “cva” из “class-variance-authority” и определенную ранее функцию “cn”.

import React, { FC } from "react";
import { cn } from "@/lib/utils"; // Я работаю с псевдонимами. Используйте соответствующий путь к файлу для вашего проекта
import { cva, type VariantProps } from "class-variance-authority";


const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input text-primary bg-background hover:bg-primary hover:text-white",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 rounded-md px-4 text-sm",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);

interface ButtonProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {}

const Button: FC<ButtonProps> = ({ className, children, ...props }) => {
return (
<button className={className} {...props}>
{children}
</button>
);
};

Вы можете скопировать этот buttonVariant и адаптировать его под свой случай использования.

Если вы заметили, я добавил такие ключевые слова, как “primary” (стиль активных кнопок), “secondary” (стиль дополнительных кнопок), “accent” (стиль выделенных кнопок, требующих особого внимания), “destructive” (стиль неактивных кнопок). Вы можете определить эти цвета в конфигурационном файле Tailwind.

Теперь настроим интерфейс. Добавим в button типы для вариаций состояний, чтобы они были доступны как свойства, а также для обеспечения корректной поддержки IntelliSense.

interface ButtonProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>, VariantProps<typeof buttonVariants> {}

Теперь ButtonProps расширяет два интерфейса: оригинальный интерфейс button и тип VariantProps.

Чтобы собрать все вместе, добавим вариации состояний в className button (помните, что вариации состояний  —  это просто предопределенные параметры className).

const Button: FC<ButtonProps> = ({ className, children, variant, size, ...props }) => {
return (
<button className={cn(buttonVariants({variant, size, className}))} {...props}>
{children}
</button>
);
};

Чтобы передать button ссылку, импортируйте “forwardRef” из React и оберните им компонент button. Это должно выглядеть следующим образом:

const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ className, children, variant, size, ...props }) => {
return (
<button className={cn(buttonVariants({variant, size, className}))} {...props}>
{children}
</button>
);
}

Теперь создание компонента завершено. Можно экспортировать его и использовать внутри приложения, с гарантией полной безопасности типов и корректной поддержки intelliSense.

export default function App () {
return (
<div>
<Button size={"lg"} variant={"secondary"}/>
</div>
)
}

Полный код компонента button должен выглядеть следующим образом:

import React, { FC } from "react";
import { cn } from "@/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";

const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input text-primary bg-background hover:bg-primary hover:text-white",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 rounded-md px-4 text-sm",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);

interface ButtonProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>, VariantProps<typeof buttonVariants> {} // Добавьте дополнительные свойства в соответствии с особенностями вашего проекта

const Button: FC<ButtonProps> = ({ className, children, variant, size, ...props }) => {
return (
<button className={cn(buttonVariants({variant, size, className}))} {...props}>
{children}
</button>
);
};

И вы уже скоро сможете создавать компоненты пользовательского интерфейса не хуже старшего React-разработчика!

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Emmanuel Alozie: Build React UI Components Like a Senior Developer

Предыдущая статьяTaipy: создание полнофункциональных приложений для работы с данными
Следующая статьяKotlin изнутри: как работают inline-функции