Двусторонняя связь: руна $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:. Это удобный, но опасный при злоупотреблении инструмент — применяйте его точечно.

Проверьте себя
1. Что нужно, чтобы пропс поддерживал двустороннюю связь через bind:?
AНичего — все пропсы привязываемы по умолчанию
BПометить его руной $bindable в дочернем компоненте
CОбернуть его в $derived
DИспользовать createEventDispatcher
2. Почему Svelte сделал привязку пропсов явной (opt-in)?
AИз-за ограничений компилятора
BЧтобы избежать непредсказуемого потока данных при злоупотреблении двусторонней связью
CЧтобы ускорить сборку
DЭто случайное ограничение