Перейти к содержимому

5. Props

Привет! 👋 Яша снова здесь. Мы уже знаем, как работает реактивность внутри одного компонента. Теперь пора научиться передавать данные между компонентами. В Svelte это делается через export let — и это один из тех моментов, когда ты скажешь “Ого, как просто!” 😄


Props (сокращение от properties) — это способ передать данные сверху вниз: от родительского компонента к дочернему. Это главный механизм переиспользования компонентов.

Представь, что у тебя есть компонент UserCard. Он должен показывать разных пользователей с разными именами, аватарками, биографиями. Вместо того, чтобы создавать UserCard1.svelte, UserCard2.svelte, UserCard3.svelte — ты создаёшь один компонент и передаёшь данные как props.

В React:

function UserCard({ name, age, online }) {
return <div>{name} — {age} лет</div>;
}
<UserCard name="Яша" age={25} online={true} />

В Vue:

<script setup>
const props = defineProps<{ name: string; age: number; online: boolean }>();
</script>

В Svelte — элегантнее всех:

<script>
export let name;
export let age;
export let online = false; // дефолтное значение
</script>
<div>{name} — {age} лет</div>

Ключевое слово export в контексте Svelte-компонента работает не как обычный ES6 export. Svelte перехватывает его и превращает в объявление пропса.

Greeting.svelte
<script>
export let name = 'Мир'; // prop с дефолтным значением
export let emoji = '👋'; // ещё один prop
// Это обычная переменная — НЕ prop
let greeting = 'Привет';
</script>
<p>{greeting}, {name}! {emoji}</p>
<!-- App.svelte — родительский компонент -->
<script>
import Greeting from './Greeting.svelte';
</script>
<Greeting name="Яша" emoji="🚀" />
<!-- Выведет: "Привет, Яша! 🚀" -->
<Greeting name="Мир" />
<!-- Выведет: "Привет, Мир! 👋" (emoji — дефолтное) -->
<Greeting />
<!-- Выведет: "Привет, Мир! 👋" (оба дефолтных) -->

💡 Почему именно export? Svelte умно переиспользует синтаксис ES6. С точки зрения компилятора: то, что компонент “экспортирует” — это то, что он принимает снаружи. Гениально и лаконично!


В Svelte нет встроенной системы валидации пропсов (как PropTypes в React), но есть чёткое правило:

<script>
// ⚠️ Без дефолтного значения — prop "обязательный"
// JS не выбросит ошибку, но TypeScript предупредит
export let userId; // обязательный по духу
export let name; // обязательный по духу
// ✅ С дефолтным значением — необязательный
export let online = false;
export let theme = 'light';
export let count = 0;
export let items = [];
</script>

Если передать компоненту незнакомый prop, Svelte выбросит предупреждение в dev-режиме:

<Unknown prop 'unknownProp' received by 'MyComponent'>

Это помогает отлавливать опечатки! 🎯


С TypeScript работа с пропсами становится ещё удобнее:

<script lang="ts">
// Базовые типы
export let name: string;
export let age: number = 0;
export let online: boolean = false;
export let tags: string[] = [];
// Объект
export let user: { id: number; name: string; avatar: string };
// Enum через union
export let theme: 'light' | 'dark' | 'auto' = 'auto';
export let size: 'sm' | 'md' | 'lg' = 'md';
// Опциональный prop через undefined
export let description: string | undefined = undefined;
// Функция как prop (callback)
export let onClick: (event: MouseEvent) => void = () => {};
export let onValueChange: (value: number) => void = () => {};
</script>

Через интерфейс:

<script lang="ts">
interface Props {
name: string;
age: number;
online?: boolean; // ? — опциональный (есть дефолт)
theme?: 'light' | 'dark';
onClose?: () => void;
}
// В Svelte 4 нельзя деструктурировать из интерфейса напрямую,
// поэтому объявляем каждый prop отдельно:
export let name: Props['name'];
export let age: Props['age'];
export let online: Props['online'] = false;
export let theme: Props['theme'] = 'light';
export let onClose: Props['onClose'] = undefined;
</script>

В Svelte 5 с рунами всё ещё проще:

<script lang="ts">
interface Props {
name: string;
age: number;
online?: boolean;
}
// Svelte 5: деструктурируем из $props()!
const { name, age, online = false }: Props = $props();
</script>

<UserCard name="Яша" age={25} online={true} theme="dark" />
<script>
let currentUser = { name: 'Яша', age: 25 };
let isOnline = true;
</script>
<UserCard
name={currentUser.name}
age={currentUser.age}
online={isOnline}
/>
<!-- ✅ Число -->
<Progress value={75} max={100} />
<!-- ✅ Булево: просто написать имя = true -->
<Button disabled />
<!-- Это то же самое, что: -->
<Button disabled={true} />
<!-- ✅ Объект -->
<Chart data={{ labels: ['янв', 'фев'], values: [10, 20] }} />
<!-- ✅ Функция-колбэк -->
<Button onClick={() => console.log('click!')} />

Если имя переменной совпадает с именем пропса — можно использовать сокращение:

<script>
let name = 'Яша';
let age = 25;
let online = true;
</script>
<!-- Обычный способ: -->
<UserCard name={name} age={age} online={online} />
<!-- Сокращённый способ (shorthand): -->
<UserCard {name} {age} {online} />
<!-- Работает только когда переменная называется так же, как prop! -->

Это мелочь, но когда передаёшь много пропсов — сильно сокращает код. В React такого нет (там нужен spread: <UserCard {...{name, age, online}} />).


Если у тебя есть объект с данными — можно передать его целиком:

<script>
let userProps = {
name: 'Яша',
age: 25,
online: true,
theme: 'dark',
};
</script>
<!-- Обычная передача: -->
<UserCard name={userProps.name} age={userProps.age} online={userProps.online} theme={userProps.theme} />
<!-- Spread: короче и чище! -->
<UserCard {...userProps} />

Можно комбинировать spread с явными пропсами:

<!-- Spread + отдельные пропсы -->
<UserCard {...userProps} theme="light" />
<!-- theme="light" перезапишет userProps.theme -->

Это особенно удобно при переадресации пропсов между компонентами:

<!-- Wrapper.svelte — просто передаёт всё в InnerComponent -->
<script>
import InnerComponent from './InnerComponent.svelte';
// получаем все свои props
export let foo;
export let bar;
</script>
<InnerComponent {foo} {bar} />
<!-- или если много пропсов — через $$props: -->
<InnerComponent {...$$props} />

$$props — это специальная переменная Svelte, которая содержит все переданные пропсы в виде объекта, включая те, которые ты не объявил через export let:

<script>
export let name = '';
export let age = 0;
// $$props содержит { name: 'Яша', age: 25, ...и любые другие }
console.log($$props);
// { name: 'Яша', age: 25, class: 'user-card', id: 'main' }
</script>
<!-- Можно передать все props куда-то ещё -->
<div {...$$props}>
{name} — {age}
</div>

⚠️ Осторожно! Если ты передаёшь $$props в DOM-элемент (<div>, <button> и т.д.), это может передать невалидные HTML-атрибуты в DOM. Лучше использовать $$restProps.


$$restProps — это $$props минус все явно объявленные через export let:

Button.svelte
<script>
export let variant = 'primary';
export let size = 'md';
// $$restProps содержит всё остальное:
// onClick, disabled, type, class, id, aria-* и т.д.
// Всё что угодно, кроме variant и size
</script>
<button
class={'btn btn-' + variant + ' btn-' + size}
{...$$restProps}
>
<slot />
</button>

Использование:

<Button
variant="danger"
size="lg"
onClick={handleClick}
disabled={isLoading}
aria-label="Удалить"
>
Удалить
</Button>
<!-- В итоге в DOM:
<button class="btn btn-danger btn-lg"
onclick="..."
disabled
aria-label="Удалить"
>
Удалить
</button>
-->

$$restProps — незаменимый инструмент для создания UI-библиотек и враппер-компонентов над нативными HTML-элементами. Ты контролируешь свои пропсы, а все остальные атрибуты проходят насквозь.


По умолчанию props — это односторонняя связь: от родителя к дочернему. Но иногда нужна двусторонняя привязка:

NumberInput.svelte
<script>
export let value = 0;
</script>
<input type="number" bind:value />
<!-- value изменяется через пользовательский ввод -->
App.svelte
<script>
import NumberInput from './NumberInput.svelte';
let count = 10;
</script>
<!-- bind: синхронизирует count с value внутри NumberInput -->
<NumberInput bind:value={count} />
<p>Текущее значение: {count}</p>
<!-- count обновится при изменении input! -->

Сокращение (если имена совпадают):

<NumberInput bind:value />
<!-- то же самое, что bind:value={value} -->

💡 bind: — это синтаксический сахар для паттерна “prop + event”. Под капотом Svelte генерирует обработчик события и обновление родительской переменной автоматически.


Иногда нужно рендерить компонент динамически — когда сам компонент определяется в runtime:

<script>
import Icon from './Icon.svelte';
import Badge from './Badge.svelte';
import Label from './Label.svelte';
let selectedComponent = Icon;
let props = { size: 24, color: 'red' };
</script>
<!-- svelte:component — рендерит компонент динамически -->
<svelte:component this={selectedComponent} {...props} />
<!-- Меняем компонент и пропсы: -->
<button on:click={() => { selectedComponent = Badge; props = { text: 'Новый' }; }}>
Переключить на Badge
</button>

Когда this={null} или this={undefined} — ничего не рендерится. Удобно для условного рендера!


Иногда компонент хочет “опубликовать” что-то вовне, но не принимать это как prop:

Timer.svelte
<script>
// НЕ prop — это просто экспорт из модуля компонента
export const reset = () => {
elapsed = 0;
};
let elapsed = 0;
// ...таймер
</script>
<p>Прошло: {elapsed} сек</p>
App.svelte
<script>
import Timer from './Timer.svelte';
let timerRef;
</script>
<Timer bind:this={timerRef} />
<button on:click={() => timerRef.reset()}>Сбросить</button>

export const и export function создают публичное API компонента — методы и значения, доступные родителю через bind:this.


Svelte не имеет встроенного механизма валидации (как PropTypes в React или runtime validators в Vue). Но есть паттерны:

<script>
export let rating = 0;
export let size = 'md';
// Паттерн 1: onMount-валидация (только dev)
import { onMount } from 'svelte';
onMount(() => {
if (rating < 0 || rating > 5) {
console.warn('Rating должен быть от 0 до 5, получено:', rating);
}
if (!['sm', 'md', 'lg'].includes(size)) {
console.warn('Неверный size:', size, '— используем md');
size = 'md';
}
});
// Паттерн 2: реактивная нормализация
$: normalizedRating = Math.max(0, Math.min(5, rating));
// Паттерн 3: TypeScript (лучший подход!)
// export let size: 'sm' | 'md' | 'lg' = 'md';
// TypeScript не даст передать неверное значение на этапе компиляции
</script>

Рекомендация Яши: используй TypeScript — это и есть лучшая валидация. Ошибки находятся в IDE, а не в runtime.


1. Попытаться мутировать prop напрямую:

<script>
export let count = 0;
function increment() {
count++; // ⚠️ Работает, но антипаттерн!
// Svelte предупредит в dev-режиме
// Родитель не узнает об изменении (если нет bind:)
}
</script>

Лучше: использовать bind: для двусторонней привязки или emit событие.

2. Передать объект без распаковки:

<script>
let user = { name: 'Яша', age: 25 };
</script>
<!-- ❌ Передаёт один prop "user" с объектом -->
<UserCard user={user} />
<!-- ✅ Spread — передаёт name и age как отдельные props -->
<UserCard {...user} />

3. Использовать $$props в DOM-элементах без осторожности:

<!-- ⚠️ Может добавить невалидные атрибуты в DOM -->
<div {...$$props}>{content}</div>
<!-- ✅ Используй $$restProps — только нераспознанные props -->
<div {...$$restProps}>{content}</div>

Настраивай пропсы через панель управления и наблюдай, как дочерний компонент обновляется в реальном времени!