Двусторонняя связь: руна $bindable
Руна $bindable разрешает двустороннюю связь пропса — данные текут и вниз, и вверх через bind:.
«Двусторонняя связь удобна, но как соль: щепотка улучшает блюдо, ложка — портит.»
Обычно пропсы однонаправлены: вниз. Но иногда удобнее двусторонняя связь — например, кастомное поле ввода, чьё значение должно синхронизироваться с родителем в обе стороны. Для таких случаев Svelte 5 даёт руну $bindable. По умолчанию пропсы не привязываемы; вы явно помечаете нужный пропс как bindable, и тогда родитель может использовать директиву bind:.
Явность здесь не случайна. Команда Svelte намеренно сделала привязку opt-in, потому что чрезмерное использование двусторонней связи делает поток данных непредсказуемым: становится трудно понять, кто и когда меняет значение. Поэтому $bindable — инструмент для узких случаев, а не способ по умолчанию.
<!-- TextInput.svelte -->
<script>
let { value = $bindable('') } = $props(); // привязываемый пропс
</script>
<input bind:value />Теперь родитель может связать своё состояние с этим полем двусторонне:
<!-- родитель -->
<script>
import TextInput from './TextInput.svelte';
let name = $state('');
</script>
<TextInput bind:value={name} />
<p>Привет, {name}!</p>Когда пользователь печатает в поле, name у родителя обновляется. И наоборот: если родитель изменит name, поле отразит это. Связь работает в обе стороны. Без bind: вы получили бы обычный однонаправленный пропс.
Как это работает под капотом
Двусторонняя связь — это пара «геттер плюс сеттер», проходящая сквозь компонент. Родитель отдаёт ребёнку доступ и на чтение, и на запись. Смоделируем.
// Двусторонняя связь как пара чтение/запись
function bindable(getParent, setParent) {
return {
get value() { return getParent(); },
set value(v) { setParent(v); } // запись уходит наверх
};
}
let parentName = 'Аня';
const input = bindable(() => parentName, (v) => parentName = v);
console.log('поле видит:', input.value); // Аня
input.value = 'Борис'; // пользователь ввёл новое
console.log('родитель стал:', parentName); // Борис (ушло наверх)Попробуй сам ▶ — вставь код в консоль браузера (F12 → Console) и нажми Enter, чтобы увидеть вывод.
Родитель (name) <==bind:value==> Ребёнок (value $bindable)
^ |
| запись из поля v
+--------- <input bind:value> ------+
данные текут в ОБЕ стороныЧастые ошибки
- Ждать, что любой пропс привязываем. Нужен явный
$bindableв дочернем компоненте. - Злоупотреблять двусторонней связью. Это запутывает поток данных; чаще достаточно колбэка.
- Забывать
bind:у родителя. Без директивы пропс остаётся однонаправленным.
Best practices
- Используйте
$bindableтолько для очевидно двунаправленных вещей — полей ввода, переключателей. - В сомнительных случаях предпочитайте пропс вниз плюс колбэк вверх — поток нагляднее.
- Документируйте, какие пропсы привязываемы, чтобы команда не гадала.
Когда двусторонняя связь оправдана
Сделав привязку явной, команда Svelte фактически подталкивает вас задуматься каждый раз, когда вы тянетесь к двусторонней связи. И это правильно: в большинстве случаев пара «пропс вниз плюс колбэк вверх» даёт более прозрачный поток данных, где видно, кто именно и в ответ на что меняет значение. Двусторонняя связь по-настоящему окупается лишь в узком классе случаев — когда вы пишете обёртку над элементом ввода: кастомное текстовое поле, слайдер, переключатель, выпадающий список. Там семантика bind: естественна и ожидаема, а альтернатива с колбэком была бы многословной без выигрыша в ясности. Если же вы ловите себя на том, что биндите сложные объекты состояния между компонентами в обе стороны, остановитесь: скорее всего, поток данных стоит перепроектировать. Двусторонняя связь — точечный инструмент, а не способ организации архитектуры по умолчанию.
Итог: $bindable включает двустороннюю связь пропса, позволяя родителю использовать bind:. Это удобный, но опасный при злоупотреблении инструмент — применяйте его точечно.