Как сделать стили гибкими при наследовании?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Гибкие стили при наследовании в компонентах React
Это вопрос о том, как создавать переиспользуемые компоненты, которые легко кастомизировать через пропсы, не нарушая принцип открытости/закрытости (Open/Closed Principle) из SOLID. Рассмотрю несколько подходов.
1. Классический подход: пропсы для модификаций
Самый простой способ — передавать разные пропсы для управления стилями.
// components/Button.tsx
interface ButtonProps {
children: React.ReactNode;
variant?: primary | secondary | danger;
size?: sm | md | lg;
disabled?: boolean;
className?: string; // Для дополнительных стилей
}
const variantStyles = {
primary: bg-blue-600 text-white hover:bg-blue-700,
secondary: bg-gray-200 text-gray-800 hover:bg-gray-300,
danger: bg-red-600 text-white hover:bg-red-700
};
const sizeStyles = {
sm: px-2 py-1 text-sm,
md: px-4 py-2 text-base,
lg: px-6 py-3 text-lg
};
export function Button({
children,
variant = primary,
size = md,
disabled = false,
className
}: ButtonProps) {
return (
<button
className={`
${variantStyles[variant]}
${sizeStyles[size]}
rounded-lg font-medium transition
disabled:opacity-50 disabled:cursor-not-allowed
${className || }
`}
disabled={disabled}
>
{children}
</button>
);
}
// Использование
<Button variant="primary" size="lg">Large Button</Button>
<Button variant="danger" size="sm">Small Danger</Button>
<Button className="custom-style">Custom</Button>
2. CSS-переменные для гибкой кастомизации
Это позволяет глубокую кастомизацию через CSS, не меняя компонент.
// components/Card.tsx
interface CardProps {
children: React.ReactNode;
bgColor?: string; // CSS цвет
borderColor?: string;
padding?: string; // rem или px
shadow?: boolean;
}
export function Card({
children,
bgColor = white,
borderColor = #e5e7eb,
padding = 1rem,
shadow = true
}: CardProps) {
return (
<div
style={{
backgroundColor: bgColor,
borderColor: borderColor,
padding: padding,
--shadow: shadow ? 0 1px 3px rgba(0, 0, 0, 0.1) : none
} as React.CSSProperties}
className="border rounded-lg"
style={{
boxShadow: shadow ? 0 1px 3px rgba(0, 0, 0, 0.1) : none
}}
>
{children}
</div>
);
}
// Использование
<Card bgColor="#f3f4f6" padding="2rem" shadow={false}>
Content
</Card>
3. Композиция компонентов (compound components pattern)
Это паттерн позволяет строить гибкие интерфейсы через комбинирование маленьких компонентов.
// components/Tabs/index.tsx
interface TabsProps {
children: React.ReactNode;
defaultTab?: string;
}
const TabsContext = createContext<{
activeTab: string;
setActiveTab: (tab: string) => void;
} | null>(null);
export function Tabs({ children, defaultTab = tab1 }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="flex flex-col">{children}</div>
</TabsContext.Provider>
);
}
export function TabsList({ children }: { children: React.ReactNode }) {
return (
<div className="flex border-b border-gray-300">
{children}
</div>
);
}
export function TabsTrigger({
value,
children
}: {
value: string;
children: React.ReactNode;
}) {
const context = useContext(TabsContext);
if (!context) throw new Error(TabsTrigger must be used in Tabs);
const { activeTab, setActiveTab } = context;
const isActive = activeTab === value;
return (
<button
onClick={() => setActiveTab(value)}
className={`
px-4 py-2 font-medium transition
${isActive ? border-b-2 border-blue-600 text-blue-600 : text-gray-600}
`}
>
{children}
</button>
);
}
export function TabsContent({
value,
children
}: {
value: string;
children: React.ReactNode;
}) {
const context = useContext(TabsContext);
if (!context) throw new Error(TabsContent must be used in Tabs);
const { activeTab } = context;
if (activeTab !== value) return null;
return <div className="py-4">{children}</div>;
}
// Использование
<Tabs defaultTab="tab1">
<TabsList>
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
</TabsList>
<TabsContent value="tab1">Content 1</TabsContent>
<TabsContent value="tab2">Content 2</TabsContent>
</Tabs>
4. Render props паттерн
Дает полный контроль над отрендеренным контентом.
interface FormFieldProps {
name: string;
label: string;
children: (props: { value: string; onChange: (v: string) => void }) => React.ReactNode;
}
export function FormField({ name, label, children }: FormFieldProps) {
const [value, setValue] = useState();
return (
<div className="mb-4">
<label className="block text-sm font-medium mb-2">{label}</label>
{children({ value, onChange: setValue })}
</div>
);
}
// Использование
<FormField name="email" label="Email">
{({ value, onChange }) => (
<input
type="email"
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full px-3 py-2 border rounded"
/>
)}
</FormField>
5. Комбинирование классов с функциями (cn utility)
Исползование помощника cn для объединения условных классов.
// lib/utils.ts
export function cn(...classes: (string | undefined | null | false)[]) {
return classes.filter(Boolean).join( );
}
// components/Badge.tsx
interface BadgeProps {
children: React.ReactNode;
variant?: default | success | warning | error;
className?: string;
}
const variantClasses = {
default: bg-gray-100 text-gray-800,
success: bg-green-100 text-green-800,
warning: bg-yellow-100 text-yellow-800,
error: bg-red-100 text-red-800
};
export function Badge({
children,
variant = default,
className
}: BadgeProps) {
return (
<span
className={cn(
px-2.5 py-0.5 rounded-full text-sm font-medium,
variantClasses[variant],
className
)}
>
{children}
</span>
);
}
6. Абстрактные компоненты для стилей
Создание "style-only" компонентов, которые не добавляют функционала, только стили.
// components/styled/Box.tsx
interface BoxProps extends React.HTMLAttributes<HTMLDivElement> {
as?: React.ElementType;
flex?: boolean;
column?: boolean;
gap?: number;
padding?: number;
}
export const Box = React.forwardRef<HTMLDivElement, BoxProps>((
{ as: Component = div, flex, column, gap, padding, ...props },
ref
) => {
const className = cn(
flex && flex,
column && flex-col,
gap && `gap-${gap}`
);
return (
<Component
ref={ref}
className={className}
style={{
padding: padding ? `${padding}rem` : undefined
}}
{...props}
/>
);
});
// Использование
<Box flex column gap={2} padding={1}>
<h1>Title</h1>
<p>Description</p>
</Box>
7. CSS-in-JS с условными стилями (Emotion)
import styled from @emotion/styled;
interface ContainerProps {
isActive: boolean;
size: small | medium | large;
}
const Container = styled.div<ContainerProps>`
padding: ${(props) => {
switch (props.size) {
case small:
return 0.5rem;
case medium:
return 1rem;
case large:
return 1.5rem;
}
}};
background-color: ${(props) => (props.isActive ? blue : gray)};
transition: all 0.2s ease;
`;
export function MyContainer({ isActive, size }: ContainerProps) {
return (
<Container isActive={isActive} size={size}>
Content
</Container>
);
}
Лучшие практики для гибких стилей
- Используй пропсы для вариаций — не создавай новый компонент для каждого варианта
- Предусмотри className prop — позволяет родителю переопределить стили
- Используй CSS переменные — для глубокой кастомизации без смены компонента
- Composition лучше наследования — строй из маленьких компонентов
- Документируй доступные пропсы — помогает другим разработчикам
- Избегай жестких цветов — используй теменные переменные
- Рассчитывай на переопределения — не делай стили слишком специфичными
В реальной практике я обычно комбинирую несколько подходов: пропсы для основных вариаций, className для переопределений, и CSS переменные для темизации. Это обеспечивает баланс между простотой и гибкостью.