← Каталог
React — компоненты-рецепты — Те же UI-компоненты на TypeScript (`.tsx`)
Фрагмент из «React — компоненты-рецепты»: Те же UI-компоненты на TypeScript (`.tsx`).
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import type { ButtonVariant } from '../types/ui';
const VARIANT_CLASS: Record<ButtonVariant, string> = {
primary: 'button button--primary',
outline: 'button button--outline',
danger: 'button button--danger',
};
export type ButtonProps = {
variant?: ButtonVariant;
loading?: boolean;
children: ReactNode;
} & ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = 'primary',
loading = false,
disabled,
children,
...rest
}: ButtonProps) {
const isDisabled = Boolean(disabled) || loading;
return (
<button
type="button"
className={VARIANT_CLASS[variant]}
disabled={isDisabled}
aria-busy={loading || undefined}
{...rest}
>
{loading ? 'Загрузка…' : children}
</button>
);
} import type { ButtonHTMLAttributes, ReactNode } from 'react';
import type { ButtonVariant } from '../types/ui';
const VARIANT_CLASS: Record<ButtonVariant, string> = {
primary: 'button button--primary',
outline: 'button button--outline',
danger: 'button button--danger',
};
export type ButtonProps = {
variant?: ButtonVariant;
loading?: boolean;
children: ReactNode;
} & ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = 'primary',
loading = false,
disabled,
children,
...rest
}: ButtonProps) {
const isDisabled = Boolean(disabled) || loading;
return (
<button
type="button"
className={VARIANT_CLASS[variant]}
disabled={isDisabled}
aria-busy={loading || undefined}
{...rest}
>
{loading ? 'Загрузка…' : children}
</button>
);
} import { useEffect, useState } from 'react';
import type { Profile, ProfileLoadState } from '../types/ui';
import { CardSkeleton } from './CardSkeleton';
export function ProfileCard() {
const [state, setState] = useState<ProfileLoadState>({ status: 'loading' });
useEffect(() => {
const timer = window.setTimeout(() => {
setState({
status: 'ok',
profile: { name: 'Анна', role: 'Frontend-разработчик' },
});
}, 1000);
return () => window.clearTimeout(timer);
}, []);
if (state.status === 'loading') return <CardSkeleton />;
if (state.status === 'error') return <p className="error">{state.message}</p>;
return (
<article className="profile-card">
<h2>{state.profile.name}</h2>
<p>{state.profile.role}</p>
</article>
);
} import { useEffect, useState } from 'react';
import type { Profile, ProfileLoadState } from '../types/ui';
import { CardSkeleton } from './CardSkeleton';
export function ProfileCard() {
const [state, setState] = useState<ProfileLoadState>({ status: 'loading' });
useEffect(() => {
const timer = window.setTimeout(() => {
setState({
status: 'ok',
profile: { name: 'Анна', role: 'Frontend-разработчик' },
});
}, 1000);
return () => window.clearTimeout(timer);
}, []);
if (state.status === 'loading') return <CardSkeleton />;
if (state.status === 'error') return <p className="error">{state.message}</p>;
return (
<article className="profile-card">
<h2>{state.profile.name}</h2>
<p>{state.profile.role}</p>
</article>
);
} import { useEffect, useState, type ChangeEvent } from 'react';
const STORAGE_KEY = 'app-theme-dark';
export function ThemeSwitch() {
const [dark, setDark] = useState(() => localStorage.getItem(STORAGE_KEY) === '1');
useEffect(() => {
document.documentElement.classList.toggle('theme-dark', dark);
localStorage.setItem(STORAGE_KEY, dark ? '1' : '0');
}, [dark]);
function onChange(e: ChangeEvent<HTMLInputElement>) {
setDark(e.target.checked);
}
return (
<label className="switch switch--theme">
<input
type="checkbox"
className="switch__input"
role="switch"
checked={dark}
onChange={onChange}
aria-labelledby="theme-switch-label"
/>
<span className="switch-track" aria-hidden="true" />
<span id="theme-switch-label" className="switch__label">
{dark ? 'Тёмная тема' : 'Светлая тема'}
</span>
</label>
);
} import { useEffect, useState, type ChangeEvent } from 'react';
const STORAGE_KEY = 'app-theme-dark';
export function ThemeSwitch() {
const [dark, setDark] = useState(() => localStorage.getItem(STORAGE_KEY) === '1');
useEffect(() => {
document.documentElement.classList.toggle('theme-dark', dark);
localStorage.setItem(STORAGE_KEY, dark ? '1' : '0');
}, [dark]);
function onChange(e: ChangeEvent<HTMLInputElement>) {
setDark(e.target.checked);
}
return (
<label className="switch switch--theme">
<input
type="checkbox"
className="switch__input"
role="switch"
checked={dark}
onChange={onChange}
aria-labelledby="theme-switch-label"
/>
<span className="switch-track" aria-hidden="true" />
<span id="theme-switch-label" className="switch__label">
{dark ? 'Тёмная тема' : 'Светлая тема'}
</span>
</label>
);
} import { useId, useState, type ReactElement } from 'react';
export type TooltipProps = {
label: string;
children: ReactElement;
};
export function Tooltip({ label, children }: TooltipProps) {
const [open, setOpen] = useState(false);
const tipId = useId();
const trigger = (
<span aria-describedby={open ? tipId : undefined}>{children}</span>
);
return (
<span
className="tooltip-wrap"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
>
{trigger}
{open ? (
<span id={tipId} role="tooltip" className="tooltip-bubble">
{label}
</span>
) : null}
</span>
);
} import { useId, useState, type ReactElement } from 'react';
export type TooltipProps = {
label: string;
children: ReactElement;
};
export function Tooltip({ label, children }: TooltipProps) {
const [open, setOpen] = useState(false);
const tipId = useId();
const trigger = (
<span aria-describedby={open ? tipId : undefined}>{children}</span>
);
return (
<span
className="tooltip-wrap"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
>
{trigger}
{open ? (
<span id={tipId} role="tooltip" className="tooltip-bubble">
{label}
</span>
) : null}
</span>
);
} import { useState } from 'react';
import { Button } from './components/Button';
import { ProfileCard } from './components/ProfileCard';
import { ThemeSwitch } from './components/ThemeSwitch';
import { Tooltip } from './components/Tooltip';
import './App.css';
export default function App() {
const [busy, setBusy] = useState(false);
function simulateSave() {
setBusy(true);
window.setTimeout(() => setBusy(false), 1200);
}
return (
<div className="app">
<header className="app-header">
<ThemeSwitch />
</header>
<main>
<h1>UI на TypeScript</h1>
<section>
<h2>Кнопки</h2>
<div className="button-row">
<Button variant="primary">Сохранить</Button>
<Button variant="outline">Отмена</Button>
<Button variant="danger">Удалить</Button>
<Tooltip label="Имитация отправки на сервер">
<Button variant="primary" loading={busy} onClick={simulateSave}>
Отправить
</Button>
</Tooltip>
</div>
</section>
<section>
<h2>Профиль</h2>
<ProfileCard />
</section>
</main>
</div>
);
} import { useState } from 'react';
import { Button } from './components/Button';
import { ProfileCard } from './components/ProfileCard';
import { ThemeSwitch } from './components/ThemeSwitch';
import { Tooltip } from './components/Tooltip';
import './App.css';
export default function App() {
const [busy, setBusy] = useState(false);
function simulateSave() {
setBusy(true);
window.setTimeout(() => setBusy(false), 1200);
}
return (
<div className="app">
<header className="app-header">
<ThemeSwitch />
</header>
<main>
<h1>UI на TypeScript</h1>
<section>
<h2>Кнопки</h2>
<div className="button-row">
<Button variant="primary">Сохранить</Button>
<Button variant="outline">Отмена</Button>
<Button variant="danger">Удалить</Button>
<Tooltip label="Имитация отправки на сервер">
<Button variant="primary" loading={busy} onClick={simulateSave}>
Отправить
</Button>
</Tooltip>
</div>
</section>
<section>
<h2>Профиль</h2>
<ProfileCard />
</section>
</main>
</div>
);
}