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-разработчика!
Читайте также:
- Обрабатываем ошибки в React: полное руководство
- Шаблоны проектирования в React
- Next.js и React.js: что выбрать для проекта
Читайте нас в Telegram, VK и Дзен
Перевод статьи Emmanuel Alozie: Build React UI Components Like a Senior Developer