React и Typescript являются очень мощными инструментами для создания динамичных пользовательских интерфейсов. Создание универсального компонента, который может использоваться в различных ситуациях, является одной из ключевых задач при разработке веб-приложений. В этой статье мы рассмотрим создание такого компонента на примере кнопки, разберемся как использовать типизацию для создания динамических компонентов с использованием React и Typescript.
Что мы получим в результате?
Кнопку, которая сможет в качестве необязательного пропа принимать компонент или имя тега и рендерить все в единообразном стиле
Пример 3 кнопок:
//Самая обычная кнопка <Button onClick={showAlert}>Hello! It's button!</Button> //Ссылка выглядящая как кнопка <Button component={'a'} target="_blank" href="https://www.google.ru/" > It's Google link! </Button> //Ссылка из react-router-dom которая выглядит как наша кнопка //import { Link } from 'react-router-dom' <Button component={Link} to="/link-route"> It's router link! </Button>
Все эти три элемента используют один и тот же компонент, и реализуют состояния disabled и loading которые работают одинаково во всех 3 случаях
Так же typescript нам во всех трех случаях помогает с пропами, т.e. он корректно указывает обязательные пропы и их типы
Что я буду использовать для создания компонента
-
React
-
Typescript
-
SCSS — для удобства описания стилей
-
clsx — утилита для условного построения строк className
Базовая структура
-
Создадим папку «BaseButton», а в ней файл
BaseButton.tsx.BaseButtonкомпонент будет содержать базовую логику. -
Создадим папку «Button», а в ней файл
Button.tsx.Buttonбудет уже содержать стили и различные состояния отображения. -
Создадим в папке «Button» scss файл
Button.module.scss, я буду использовать именно модуль, но если вам удобнее можете просто создать обычный файл стилей.
Реализация BaseButton
Давайте добавим в файл «BaseButton.tsx» код ниже и разберем его по частям:
import { Attributes, createElement, CSSProperties, ComponentType, ComponentPropsWithRef, } from 'react' export type BaseButtonComponent = | keyof JSX.IntrinsicElements | ComponentType<any> type BaseProps<C extends BaseButtonComponent = 'button'> = { component?: C className?: string style?: CSSProperties } & Attributes export type BaseButtonProps<C extends BaseButtonComponent = 'button'> = C extends keyof JSX.IntrinsicElements ? Omit<ComponentPropsWithRef<C>, keyof BaseProps<C>> & BaseProps<C> : C extends ComponentType<infer P> ? P extends ComponentPropsWithRef<any> ? Omit<P, keyof BaseProps<C>> & BaseProps<C> : never : never export default function BaseButton<C extends BaseButtonComponent = 'button'>({ component = 'button', children, ...props }: BaseButtonProps<C>) { return createElement(component, props, children) }
Типы BaseButton
Тип BaseProps — он дженерик и определяет базовые пропы компонента: component, className, style
Проп component отвечает непосредственно за то, какой элемент мы будем рендерить, он наследуется от типа BaseButtonComponent и по умолчанию будет button это определяет запись <C extends BaseButtonComponent = 'button'>
Тип BaseButtonComponent определяет каким может быть наш компонент:
-
ComponentType(если посмотреть внутрь он будет выглядеть какtype ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>) т.e. в качестве пропаcomponentмы можем передать класс или функциональный компонент -
keyof JSX.IntrinsicElements(если мы заглянем в его определения то мы увидим такой код:interface IntrinsicElements { // HTML a: React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>; abbr: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>; address: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>; area: React.DetailedHTMLProps<React.AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>; article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>; ... //и остальные валидные теги по порядку }т.e
IntrinsicElementsсодержит перечисление всех валидных тэгов для реакта.
C помощью командыkeyofмы как раз получаем все ключи валидных тэгов, это выглядит для тайпскрипта как'a' | 'abbr' | 'address' | 'area' | <и все по порядку>
Т.e. BaseButtonComponent может быть либо классом, либо функциональным компонентом, либо любым валидным тэгом для реакта
А теперь непосредственно разберем тип BaseButtonProps, если попытаться описать его тремя словами то это большое тернарное выражение, давайте рассмотрим его по частям
-
Он у нас тоже является дженериком:
<C extends BaseButtonComponent = 'button'> -
Если наш компонент наследуется от «валидных тэгов» то мы берем пропы этого валидного тэга с помощью
ComponentPropsWithRefудаляем из него пропы с такими же ключами, которые содержаться в BaseProps (как мы помним этоcomponent,classNames,styles) и добавляем наши пропы которые содержаться вBaseProps. Eсли выражаться по другому, то мы просто перезаписываем все пропы компонента с ключами, которые содержаться вBasePropsна наши пропыBasePropsC extends keyof JSX.IntrinsicElements ? Omit<ComponentPropsWithRef<C>, keyof BaseProps<C>> & BaseProps<C> : <...> -
Если наш компонент наследуется от класса или функционального компонента, то мы сначала получаем с помощью оператора
inferего пропы и записываем их в переменнуюP, а потом выполняем ту же самую процедуру перезаписывания с этими пропамиC extends ComponentType<infer P> ? P extends ComponentPropsWithRef<any> ? Omit<P, keyof BaseProps<C>> & BaseProps<C> -
Если же компонент не удовлетворяет условиям то мы возвращаем
neverт.e. компонент не сможет содержать никаких пропов
Компонент BaseButton
А теперь перейдем непосредственно к реализации BaseButton
export default function BaseButton<C extends BaseButtonComponent = 'button'>({ component = 'button', children, ...props }: BaseButtonProps<C>) { return createElement(component, props, children) }
-
Закономерно наш компонент тоже является дженериком
-
Пропы собираются рест оператором для, оставляем только component и children
-
Сам же элемент создается с помощью функции реакта createElement, первым аргументом функции является сам компонент, вторым его пропы, третим чилдрены
например можно написать так:
createElement('div', { className: <какой то класс> }, 'текст')Так же можно реализовать его немного другим способом, как говориться на вкус и цвет 🙂
Реализация Button
Давайте добавим в файл Button.tsx код и вкратце посмотрим что он делает:
import clsx from 'clsx' import BaseButton, { BaseButtonProps, BaseButtonComponent, } from '../BaseButton/BaseButton' import styles from './Button.module.scss' export type ButtonProps<C extends BaseButtonComponent = 'button'> = BaseButtonProps<C> & { loading?: boolean disabled?: boolean } export function Button<C extends BaseButtonComponent = 'button'>({ className, loading, disabled, ...props }: ButtonProps<C>) { return ( <BaseButton<C> className={clsx( styles.button, { [styles.loading]: loading, [styles.disabled]: disabled, }, className, )} {...(props as BaseButtonProps<C>)} /> ) }
-
Он использует наш компонент
BaseButton -
Содержит дополнительные пропы
disabledиloading, т.e. типButtonPropsрасширяет типBaseButtonProps -
Тип
BaseButtonPropsтак же является дженериком, что наследует тип компонента отBaseButtonComponent -
Т.к.
BaseButtonявляется дженериком, а так же наш компонентButtonтоже является дженериком то мы указываем, что тип BaseButton является наследуемым отButtonтаким способом:<BaseButton<C>иначе компилятор ts не будет понимать, что мы от него хотим -
В зависимости от состояния
disabledилиloading мы добавляемразные стили нашей кнопке
Все готово, осталось лишь добавить стилей, сделаем их максимально простыми для наглядности
Стили кнопки под спойлером
.button { outline: none; border: none; position: relative; display: flex; align-items: center; justify-content: center; min-height: 60px; padding: 0 20px; font-size: 16px; font-weight: 500; text-align: center; text-decoration: none; color: #fff; background-color: #2196f3; border-radius: 10px; cursor: pointer; transition: background-color 0.2s ease-in-out; &:hover { background-color: #1976d2; color: #fff; } &:focus { outline: none; box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.3); } &:active { background-color: #0d47a1; color: #fff; } &.disabled, &.loading { background-color: grey; pointer-events: none; &:hover { background-color: grey; } &:focus { box-shadow:none; } } &.loading { &:after { content: ""; position: absolute; top: 6px; left: 6px; width: 6px; height: 6px; border-radius: 50%; border: 2px solid #fff; border-top-color: transparent; animation: loading 0.8s linear infinite; } } } @keyframes loading { to { transform: rotate(360deg); } }
Примеры использования нашей кнопки
-
Обычная кнопка
<Button onClick={showAlert}>Hello! It's button!</Button> -
Ссылка
<Button component={'a'} target="_blank" href="https://www.google.ru/" > It's Google link! </Button> -
Роутер Link
<Button component={Link} to="/link-route"> It's router link! </Button>
Cсылки на codesandbox и github
codesandbox — кликайте по кнопкам, юзайте инструменты разработчика, чтобы исследовать элементы кнопок
github
Послесловие
Из нашего типа ButtonBaseProps мы можем выделить отдельный тип для переопределения, вдруг кому нибудь пригодится
type DistributiveOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never
Если статья вам оказалась полезна или вы хотите что то добавить — вэлкам ту коментариес
ссылка на оригинал статьи https://habr.com/ru/post/721862/
Добавить комментарий