Storybook — это инструмент для разработки UI-компонентов в изоляции от приложения. Вместо того чтобы запускать всё приложение для тестирования одной кнопки, вы открываете Storybook и видите все варианты этой кнопки на одной странице.
Зачем нужен Storybook:
Story — это функция, которая возвращает компонент с определёнными пропсами. Каждая история = один конкретный случай использования:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
// Мета: настройки для всех историй компонента
const meta: Meta<typeof Button> = {
title: 'UI/Button', // путь в сайдбаре Storybook
component: Button,
tags: ['autodocs'], // автогенерация документации
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: { control: 'radio', options: ['sm', 'md', 'lg'] },
disabled: { control: 'boolean' },
onClick: { action: 'clicked' }, // логирует клики
},
}
export default meta
type Story = StoryObj<typeof meta>
// Истории — именованные экспорты
export const Primary: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Нажми меня',
},
}
export const Danger: Story = {
args: {
variant: 'danger',
children: 'Удалить',
},
}
export const Disabled: Story = {
args: {
disabled: true,
children: 'Недоступно',
},
}
export const LongText: Story = {
args: {
children: 'Очень длинный текст кнопки который может не влезть',
},
}Args — это пропсы истории. В UI Storybook они становятся Controls — интерактивными элементами управления:
// Можно задавать args на разных уровнях:
// 1. Глобальные (в preview.ts)
export const globalArgs = { theme: 'light' }
// 2. На уровне компонента (в meta)
const meta = {
args: { size: 'md' } // дефолтные args для всех историй
}
// 3. На уровне истории
export const Large: Story = {
args: { size: 'lg' } // переопределяет мета args
}Decorators оборачивают истории в дополнительный контекст (провайдеры, стили):
// В meta — для всех историй компонента
const meta = {
decorators: [
(Story) => (
<ThemeProvider theme="dark">
<div style={{ padding: 20 }}>
<Story />
</div>
</ThemeProvider>
),
],
}
// В preview.ts — глобально для всех историй
export const decorators = [
(Story) => (
<ReduxProvider store={store}>
<RouterProvider>
<Story />
</RouterProvider>
</ReduxProvider>
),
]Storybook расширяется через аддоны:
| Аддон | Что даёт |
|---|---|
| @storybook/addon-essentials | Controls, Actions, Docs, Viewport |
| @storybook/addon-a11y | Проверка доступности |
| @storybook/addon-interactions | Тесты взаимодействий |
| chromatic | Визуальное тестирование |
| @storybook/addon-themes | Переключение тем |
// .storybook/main.ts
const config = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
}
// .storybook/preview.ts — глобальные настройки
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, // авто-логирование onXxx пропсов
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
}Философия разработки снизу вверх:
Атомы (Button, Input, Icon)
↓
Молекулы (SearchBar = Input + Button)
↓
Организмы (Header = Logo + Nav + SearchBar)
↓
Шаблоны (PageLayout = Header + Content + Footer)
↓
Страницы (HomePage)Storybook позволяет разрабатывать каждый уровень изолированно, а потом собирать из готовых кирпичиков.
Симуляция реестра Storybook на ванильном JS: регистрация историй, работа с args, рендеринг нужных вариантов
// Симулируем работу Storybook: регистрацию историй,
// хранение args и рендеринг компонентов с разными пропсами.
// --- Реестр историй ---
class StorybookRegistry {
constructor() {
this.registry = new Map() // component -> { meta, stories }
}
// Регистрация компонента и его историй
register(meta, stories) {
const componentName = meta.title
this.registry.set(componentName, {
meta,
stories: new Map(Object.entries(stories))
})
console.log('Зарегистрирован компонент:', componentName)
console.log(' Историй:', Object.keys(stories).length)
return this
}
// Получить все истории компонента
getStories(componentTitle) {
const entry = this.registry.get(componentTitle)
if (!entry) return []
return Array.from(entry.stories.entries()).map(([name, story]) => ({
name,
// Мерджим args: мета -> история (история переопределяет)
args: { ...entry.meta.args, ...story.args }
}))
}
// Рендеринг истории — вызов компонента с нужными args
render(componentTitle, storyName) {
const entry = this.registry.get(componentTitle)
if (!entry) throw new Error('Компонент не найден: ' + componentTitle)
const story = entry.stories.get(storyName)
if (!story) throw new Error('История не найдена: ' + storyName)
const mergedArgs = { ...entry.meta.args, ...story.args }
const result = entry.meta.component(mergedArgs)
console.log('Рендер [' + componentTitle + ' / ' + storyName + ']:')
console.log(' Args:', JSON.stringify(mergedArgs))
console.log(' Результат:', result)
return result
}
// Показать документацию компонента
docs(componentTitle) {
const entry = this.registry.get(componentTitle)
if (!entry) return
console.log('
=== Документация:', componentTitle, '===')
console.log('Компонент:', entry.meta.component.name)
if (entry.meta.argTypes) {
console.log('Пропсы:')
for (const [prop, config] of Object.entries(entry.meta.argTypes)) {
console.log(' -', prop + ':', config.description || config.control || 'any')
}
}
console.log('Истории:')
for (const [name, story] of entry.stories) {
console.log(' -', name, story.args ? JSON.stringify(story.args) : '(нет args)')
}
}
}
// --- Пример компонента ---
function Button({ variant = 'primary', size = 'md', children = 'Кнопка', disabled = false }) {
const variantStyle = { primary: 'синяя', secondary: 'серая', danger: 'красная' }
const sizeStyle = { sm: 'маленькая', md: 'средняя', lg: 'большая' }
return '[КНОПКА: ' + children + ' | ' + variantStyle[variant] + ' | ' + sizeStyle[size] + (disabled ? ' | disabled' : '') + ']'
}
// --- Регистрация историй ---
const storybook = new StorybookRegistry()
storybook.register(
{
title: 'UI/Button',
component: Button,
args: { size: 'md', variant: 'primary' }, // дефолтные args
argTypes: {
variant: { control: 'select', description: 'Вариант кнопки' },
size: { control: 'radio', description: 'Размер кнопки' },
disabled: { control: 'boolean', description: 'Отключена ли кнопка' },
},
},
{
Default: { args: { children: 'Нажми меня' } },
Danger: { args: { variant: 'danger', children: 'Удалить' } },
Large: { args: { size: 'lg', children: 'Большая кнопка' } },
Disabled: { args: { disabled: true, children: 'Недоступно' } },
}
)
// --- Использование ---
console.log('=== Список историй ===')
storybook.getStories('UI/Button').forEach(s => {
console.log(' -', s.name, '| args:', JSON.stringify(s.args))
})
console.log('
=== Рендеринг историй ===')
storybook.render('UI/Button', 'Default')
storybook.render('UI/Button', 'Danger')
storybook.render('UI/Button', 'Disabled')
storybook.docs('UI/Button')Storybook — это инструмент для разработки UI-компонентов в изоляции от приложения. Вместо того чтобы запускать всё приложение для тестирования одной кнопки, вы открываете Storybook и видите все варианты этой кнопки на одной странице.
Зачем нужен Storybook:
Story — это функция, которая возвращает компонент с определёнными пропсами. Каждая история = один конкретный случай использования:
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'
// Мета: настройки для всех историй компонента
const meta: Meta<typeof Button> = {
title: 'UI/Button', // путь в сайдбаре Storybook
component: Button,
tags: ['autodocs'], // автогенерация документации
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: { control: 'radio', options: ['sm', 'md', 'lg'] },
disabled: { control: 'boolean' },
onClick: { action: 'clicked' }, // логирует клики
},
}
export default meta
type Story = StoryObj<typeof meta>
// Истории — именованные экспорты
export const Primary: Story = {
args: {
variant: 'primary',
size: 'md',
children: 'Нажми меня',
},
}
export const Danger: Story = {
args: {
variant: 'danger',
children: 'Удалить',
},
}
export const Disabled: Story = {
args: {
disabled: true,
children: 'Недоступно',
},
}
export const LongText: Story = {
args: {
children: 'Очень длинный текст кнопки который может не влезть',
},
}Args — это пропсы истории. В UI Storybook они становятся Controls — интерактивными элементами управления:
// Можно задавать args на разных уровнях:
// 1. Глобальные (в preview.ts)
export const globalArgs = { theme: 'light' }
// 2. На уровне компонента (в meta)
const meta = {
args: { size: 'md' } // дефолтные args для всех историй
}
// 3. На уровне истории
export const Large: Story = {
args: { size: 'lg' } // переопределяет мета args
}Decorators оборачивают истории в дополнительный контекст (провайдеры, стили):
// В meta — для всех историй компонента
const meta = {
decorators: [
(Story) => (
<ThemeProvider theme="dark">
<div style={{ padding: 20 }}>
<Story />
</div>
</ThemeProvider>
),
],
}
// В preview.ts — глобально для всех историй
export const decorators = [
(Story) => (
<ReduxProvider store={store}>
<RouterProvider>
<Story />
</RouterProvider>
</ReduxProvider>
),
]Storybook расширяется через аддоны:
| Аддон | Что даёт |
|---|---|
| @storybook/addon-essentials | Controls, Actions, Docs, Viewport |
| @storybook/addon-a11y | Проверка доступности |
| @storybook/addon-interactions | Тесты взаимодействий |
| chromatic | Визуальное тестирование |
| @storybook/addon-themes | Переключение тем |
// .storybook/main.ts
const config = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
}
// .storybook/preview.ts — глобальные настройки
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, // авто-логирование onXxx пропсов
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
}Философия разработки снизу вверх:
Атомы (Button, Input, Icon)
↓
Молекулы (SearchBar = Input + Button)
↓
Организмы (Header = Logo + Nav + SearchBar)
↓
Шаблоны (PageLayout = Header + Content + Footer)
↓
Страницы (HomePage)Storybook позволяет разрабатывать каждый уровень изолированно, а потом собирать из готовых кирпичиков.
Симуляция реестра Storybook на ванильном JS: регистрация историй, работа с args, рендеринг нужных вариантов
// Симулируем работу Storybook: регистрацию историй,
// хранение args и рендеринг компонентов с разными пропсами.
// --- Реестр историй ---
class StorybookRegistry {
constructor() {
this.registry = new Map() // component -> { meta, stories }
}
// Регистрация компонента и его историй
register(meta, stories) {
const componentName = meta.title
this.registry.set(componentName, {
meta,
stories: new Map(Object.entries(stories))
})
console.log('Зарегистрирован компонент:', componentName)
console.log(' Историй:', Object.keys(stories).length)
return this
}
// Получить все истории компонента
getStories(componentTitle) {
const entry = this.registry.get(componentTitle)
if (!entry) return []
return Array.from(entry.stories.entries()).map(([name, story]) => ({
name,
// Мерджим args: мета -> история (история переопределяет)
args: { ...entry.meta.args, ...story.args }
}))
}
// Рендеринг истории — вызов компонента с нужными args
render(componentTitle, storyName) {
const entry = this.registry.get(componentTitle)
if (!entry) throw new Error('Компонент не найден: ' + componentTitle)
const story = entry.stories.get(storyName)
if (!story) throw new Error('История не найдена: ' + storyName)
const mergedArgs = { ...entry.meta.args, ...story.args }
const result = entry.meta.component(mergedArgs)
console.log('Рендер [' + componentTitle + ' / ' + storyName + ']:')
console.log(' Args:', JSON.stringify(mergedArgs))
console.log(' Результат:', result)
return result
}
// Показать документацию компонента
docs(componentTitle) {
const entry = this.registry.get(componentTitle)
if (!entry) return
console.log('
=== Документация:', componentTitle, '===')
console.log('Компонент:', entry.meta.component.name)
if (entry.meta.argTypes) {
console.log('Пропсы:')
for (const [prop, config] of Object.entries(entry.meta.argTypes)) {
console.log(' -', prop + ':', config.description || config.control || 'any')
}
}
console.log('Истории:')
for (const [name, story] of entry.stories) {
console.log(' -', name, story.args ? JSON.stringify(story.args) : '(нет args)')
}
}
}
// --- Пример компонента ---
function Button({ variant = 'primary', size = 'md', children = 'Кнопка', disabled = false }) {
const variantStyle = { primary: 'синяя', secondary: 'серая', danger: 'красная' }
const sizeStyle = { sm: 'маленькая', md: 'средняя', lg: 'большая' }
return '[КНОПКА: ' + children + ' | ' + variantStyle[variant] + ' | ' + sizeStyle[size] + (disabled ? ' | disabled' : '') + ']'
}
// --- Регистрация историй ---
const storybook = new StorybookRegistry()
storybook.register(
{
title: 'UI/Button',
component: Button,
args: { size: 'md', variant: 'primary' }, // дефолтные args
argTypes: {
variant: { control: 'select', description: 'Вариант кнопки' },
size: { control: 'radio', description: 'Размер кнопки' },
disabled: { control: 'boolean', description: 'Отключена ли кнопка' },
},
},
{
Default: { args: { children: 'Нажми меня' } },
Danger: { args: { variant: 'danger', children: 'Удалить' } },
Large: { args: { size: 'lg', children: 'Большая кнопка' } },
Disabled: { args: { disabled: true, children: 'Недоступно' } },
}
)
// --- Использование ---
console.log('=== Список историй ===')
storybook.getStories('UI/Button').forEach(s => {
console.log(' -', s.name, '| args:', JSON.stringify(s.args))
})
console.log('
=== Рендеринг историй ===')
storybook.render('UI/Button', 'Default')
storybook.render('UI/Button', 'Danger')
storybook.render('UI/Button', 'Disabled')
storybook.docs('UI/Button')Создай компонент Button с вариантами (variant: primary/secondary/danger) и размерами (size: sm/md/lg). Затем создай компонент StorybookDemo, который отображает все комбинации кнопки как каталог вариантов — подобно тому, как это делает Storybook.
В Button используй variants[variant] и sizes[size] для применения стилей. В StorybookDemo передавай variant={variant} и size={size} из циклов map. Для disabled состояния передай disabled={true}.