Emit doesn't seem to work on input field in Vue 3
I want to add in the form the possibility to retrieve information on country, post code, city etc. by entering the address. I found a tutorial for doing this and I wanted to adapt the code to my needs, but the main problem is that this code doesn’t seem to work, at least for me.
Basically when I look for an address it should automatically fill in all the other fields, but nothing happens. Google Geocoding works and making various consoles.log I can retrieve the information I need, the problem is that the various inputs are not filled.
This is the complete code I am using in a .vue view for testing.
I have recently approached vue and I cannot understand what the problem is, reasoning I believe that I cannot identify which fields to update, but I have no errors, so debugging is even more complex.
Есть много способов сделать это: Vue 3 и взаимодействие компонентов
Vue 3 принёс в жизнь разработчиков возможность организации более гибкой структуры приложений. Всё чаще я стал замечать, что разные команды, а порой и разработчики внутри одной, используют целый зоопарк сомнительных подходов для организации взаимодействия между компонентами. Применяются какие-то крайности, либо всё в state manager, либо в composable (composition API), либо мутация props внутри дочерних компонентов!
Хотелось бы поднять эту тему и рассмотреть варианты взаимодействия компонентов доступные нам во Vue 3.

Немного теории
Стоит отметить, что я твердо убеждён, что каждый из способов описанных ниже не представляет из себя серебряной пули, так как имеет те или иные ограничения.
Цель, которой мы должны придерживаться — это разработка самодостаточных отделимых друг от друга компонентов, представляющих из себя некий черный ящик, принимающий в себя что-то, выполняющий что-то внутри себя и, при необходимости, сообщающий окружающим о каких-либо событиях!
Способов взаимодействия компонентов не так много:
composable (composition API)
state manager (Vuex/Pinia)
props/emits — прямое взаимодействие между компонентами! Такой подход обеспечивает прямой поток данных и строгое их изменение в определенном компоненте (при правильном применении).
Может быть неудобен при большой вложенности (“props drilling”) и в каких-то нетиповых ситуациях.
provide/inject — при правильном подходе позволит избежать “props drilling” и сохранить изменения непосредственно в родительском компоненте.
Можно легко ошибиться и потерять реактивность/мутировать данные внутри дочерних компонентов/запутаться, кто из дочерних вызывает функцию для мутирования. Так же “привязывает” дочерние компоненты к родительскому.
composable — добавляет гибкости и позволяет использовать одну логику с состоянием в нескольких компонентах.
Здесь так же легко потерять “виновника” изменений и “соблазниться” удобством вынесения всего в composable файлы.
Vuex/Pinia — практически аналогичен с composable. Для нашего вопроса уж точно.
Здесь важно понимать, что любая неочевидная связь приносит с собой возможные скрытые эффекты, о которых будет знать и помнить (пока) тот разработчик, который это всё разрабатывал. Разработчику, которому посчастливится разобрать компоненты, которые связаны только по смыслу и что-то где-то внутри себя меняют, придётся потратить гораздо больше времени, чем с прямой связью между компонентами.
Так или иначе, все это лишь моё субъективное мнение, которое не является неоспоримой истиной. Отмечу, что неоспоримым остаётся тот факт, что применение того или иного подхода должно быть задокументировано на каждом проекте, а разработчики должны отдавать себе отчет в применении этих подходов.
С картинками нагляднее.
props/emits — дочерний компонент не должен изменять (мутировать) props напрямую, он должен сообщать родительскому компоненту, что такой-то props следует изменить на такое значение. Сообщает он об этом, используя emit(). Да “props drilling” здесь присутствует, но такую вложенность я обычно игнорирую.

Взаимодействие компонентов напрямую через Props/Emits
provide/inject — картина будет совершенно другой.

Взаимодействие компонентов напрямую через Provide/Inject
Как и в случае с props/emits данные не изменяются в дочернем компоненте! Для изменения данных используется функция, переданная через provide вместе с значением. Эта функция уже вызывается внутри дочернего компонента.
Vuex/Pinia (state managers).

Взаимодействие компонентов напрямую через Vuex/Pinia
На самом деле, хранить всё состояние в state manager — слишком избыточно. Давайте на секунду представим, что мы всё, что только можем, выносим. Получаем структуру компонентов, где каждый дочерний/родительский компонент общается через “что-то” третье, имея возможность изменять данные напрямую в этом “чём-то”. Скорее всего, рано или поздно, у нас будет много неиспользуемых данных в этом “чем-то”, а в добавок начнут всплывать скрытые эффекты по всему приложению.
Для себя я выработал стратегию, что в state manager стоит выносить только общее состояние между несколькими компонентами. От прямой передачи через Props/Emits отказываться не стоит.

Взаимодействие компонентов напрямую через Vuex/Pinia + Props/Emits
А что же с Composable ( Composition API)? Подход с вынесением состояния и функционала в отдельные файлы и использование use… функций позволяет отказаться от использования Vuex/Pinia.
Правда, здесь есть одно но. Vuex/Pinia — это инструменты, которые имеют хорошую документацию и поддержку сообщества. Использование Vuex/Pinia позволяет сократить проектные знания, так как использование этих технологий в любом проекте будет практически одинаковым. С собственной реализацией Composable такого не будет. То есть, каждый новый разработчик на проекте будет вынужден изучать вашу конкретную реализацию общих функций и тем более переменных, которые могут быть глобальными и локальными.
Принципиально, схема не будет отличаться от той, что выше. Да и поленился я её рисовать.
Взаимодействие через Props/Emits (примитивы)
Прямой props и emits
Самая простая задача — это взаимодействие родительского компонента через v-model. Здесь нет ничего сложного. Достаточно написать несколько строк кода внутри дочернего компонента и взаимодействие с ним станет доступным через v-model.
В родительском компоненте (App.vue) добавим переменную и кнопку, чтобы изменять значение принудительно. Передадим переменную в Form.vue через v-model:
В компоненте Form.vue укажем props и emits:
Вот и всё, мы научились использовать v-model на собственных компонентах.
Writable computed
Чтобы использовать v-model на <input> можно использовать writable computed (get/set).
App.vue не изменится, а вот Form.vue необходимо изменить
Мы сохранили реактивность данных в обе стороны. То есть, если значение изменится в родительском компоненте (App.vue), то оно будет изменено и внутри дочернего компонента (Form.vue).
Такого результата можно добиться, сохранив props в локальную переменную (через ref()) и добавить 2 watch.
Добавим watchers в Form.vue:
Последний вариант кажется избыточным, но остаётся возможным. ‼️Отмечу, что он работает только из-за того, что это примитивный тип данных. Обратите внимание на watchers, чувствуете рекурсию и переполнение callstack? Его не происходит только потому, что watch сравнил значение, которое не изменилось, но новое присваивание произошло.
Provide/Inject
При использовании provide/inject важно понимать с чем вы его используете — с реактивной или нет переменной, не забывая, что изменять значение нужно именно в provider. Всё это более подробно описано в документации. В примере в качестве ключа я передаю строковый литерал, но в производственном коде рекомендуется передавать Symbol, вынесенный в отдельный файл.
Если вы по какой-либо причине используете Options Api во Vue 3 и/или не внимательно читали документацию, но хотите использовать provide/inject, то однозначно есть смысл прочитать несколько разделов.
Внимание! Нет реактивности!
Внесем некоторые правки в App.vue и реактивность появится.
Взаимодействие через Vuex/Pinia в этой статье мы рассматривать не будем. Большую часть можно посмотреть в их документациях.
Остановимся более подробно на моменте, когда у нас будет не 1 input, а, скажем, 10 или 15. А что если таких Form.vue на странице будет 2-3? Вот здесь обычно и возникают сложности. Передавать 1, 2 ,5 ,10 v-model можно, но будет выглядеть ужасно! Попробуем взаимодействовать с объектами в v-model.
Взаимодействие через Props/Emits (объекты)
Частный случай с итерацией по массиву
При наличии массива объектов, каждый из которых содержит в себе данные для одной формы, на ум приходит идея с итерацией и v-model. Попробуем это реализовать.
Это очень простой пример. Его цель, показать тот момент, что нельзя в v-model передать весь текущий объект в итерации. Получается, что так бы мы пилили сук, на котором сидим. Остаётся передавать v-model на каждое поле или выполнять итерацию через что-то другое.
Прямой props и emit
Попробуем реализовать v-model с Object через прямой props и emit (смотри первый пример). Однако, здесь мы сталкиваемся с проблемой избыточного кода в template, так как изменяется одно поле, а в emit вторым параметрам передается весь объект. Решим такую проблему с помощью функции, в качестве аргументов принимающую ключ и значение.
Что-то мне не нравится. Ах да, хотелось бы использовать v-model на <input>! Мы уже умеем делать это через writable computed. Стоит попробовать.
Writable computed
Внимание! Плохой код! Не повторять увиденное!
Проверяем… Работает! А что, если мы не будем вызывать emit()…
Эх, оно и так работает! Магии здесь никакой нет. Это старый добрый JavaScript с cсылочными типами данных (Object). То есть, мы просто мутируем объект напрямую.
Внимание! Код лучше, но плохой!
Эврика, теперь все работает ожидаемо.
Хотя код далек от идеала. Помните, что у нас 2, 3, 5 таких <input> (и не только), где мы меняем данные! Ок, нет проблем, повторим computed!
Так работает, мало того, сохраняется реактивность props для компонента. Но это всего 2 <input>, а для 10 напишем 10 computed? Хм, плохая идея.
Прежде чем перейти к более оптимальному решению, стоит отметить, что внимательный читатель должен был заметить, что переменные, объявленные в App.vue, объявляется при помощи ref(), а не reactive(). Стоит выяснить, есть ли между ними разница при решении через writable computed.
Изменим немного кода в App.vue:
И вот здесь всё сломалось. Вот одно из главных отличий между ref и reactive. Хотя и здесь есть обходной путь. Можно прослушивать событие @update:model-value и через Object.assign изменять переменную obj. Мой подход прост — если мне нужен Object, который я буду переписывать, то ref, иначе reactive. Хотя я встречал проекты, где принципиально отказываются от reactive из-за возможности забыть .value. Немного похоже на излишнюю осторожность, но имеет место быть.
Watch
В предыдущей главе мы остановились на не самом оптимальном решении с writable computed. Не оптимальное оно по причине повторения кода для каждого input в нашей маленькой форме.
Оптимальное решение есть и оно довольно простое, хотя и не является серебряной пулей. Но начнем мы с плохой реализации.
Как и в случае с computed всё это не будет работать с reactive в качестве props.
Создадим локальную переменную.
Внимание! Плохой код! Не повторять увиденное!
Проверяем… Это работает! Реактивность сохранилась, но мы же не вызываем emit()?! Да и vue/eslint не будет “ругаться” на мутацию props. Всё дело в том, что localObj ссылается на тот же объект и на самом деле выполняется именно прямая мутация входных параметров!
С toRef/toRefs будет так же.
Решим проблему при помощи копирования, глубокого или поверхностного — зависит от ситуация. В данном случаем нам хватит поверхностного.
Этого мало, работать не будет. Осталось лишь повесить watch и в нём вызывать emit().
Внимание! Плохой код! Не повторять увиденное!
Такой код будет работать, но он скрывает ошибку.
Попробуйте изменить данные в родителе, нажав на кнопку. Ничего не изменилось? Хорошо, это ожидаемо, так как мы не отслеживаем изменения props.
Введите что-то в <input>. В родительском компоненте данные изменились? Да, изменились.
Снова нажимаем на кнопку, чтобы изменить данные в родителе… И они теперь поменялись и в дочернем!
Так, стоп! Что произошло?! Да всё очень просто. Наш watch отследил первое изменение в localObj и передал его наверх, а v-model в родителе перезаписал значение. Так как это объект, то присваивание произошло по ссылке, как результат — скрытая связь между родительским компонентом и дочерним.
Небольшие изменения исправят ситуацию.
Одну проблему мы решили, теперь изменения в дочернем компоненте вызывают emit и в родительском изменяются данные. Однако, мы потеряли реактивность для props. Нет нет, сам props по прежнему реактивный и изменяется после emit(), но вот мы этого не видим. Причина проста, script отработал один раз, создав копию для входных параметров, и сохранил эту копию в переменную localObj. Дальнейшие изменения props никак не влияют на localObj.
Прежде чем пойти дальше, стоит немного остановиться на работе watch.
При такой записи newValue всегда будет равно oldValue. Но, если они равны, то как срабатывает watch без опции deep: true?
Когда в watch первым аргументом передаётся реактивная переменная, созданная с помощью ref(), то происходит неявное разворачивание, то есть работает ровно так же, как и в блоке template.
Наша переменная localObj хранит в себе ссылку на proxy (созданный с помощью reactive()). Если в watch передать localObj, то произойдет разворачивание и так как ссылка на proxy не изменяется, то и watch не сработает.
Но мы передаем localObj.value, где так же хранится объект, который не изменяется, тем более мы видим, что newValue === oldValue. Что здесь происходит? Да всё просто, передав localObj.value мы передали reactive объект (proxy), который автоматически переключил watch в состояние глубокого отслеживания!
Это на самом деле одно и тоже!
С последним будьте внимательны, если из функции вернуть localObj, то в newValue будет приходить неразвернутый объект.
Вернемся к оставшейся проблеме и посмотрим, сможем ли мы её решить. На текущий момент мы получаем данные от родительского компонента, копируем их в переменную в дочернем компоненте, используем эту переменную в v-model на <input> и при изменениях наш watch вызывает emit() и данные в родителе меняются. Не хватает только отслеживания изменений для props. Ведь вполне может быть ситуация, что данные в родители кто-то/что-то изменит?! Тогда наш дочерний компонент их не получит и никак не отреагирует (точнее сам компонент их получит, а вот локальная переменная — нет).
Первое, что приходит на ум — это добавить watch для props.modelValue. Попробуем?!
Так как props являются реактивным объектом (reactive), но сами ключи нет, нам следует передать в watch функцию getter, чтобы watch реагировал на изменения
И вот в этот момент уже должна быть очевидна проблема. Посмотрите внимательно, мы внутри дочернего компонент изменяем данные, которые передаём родительскому компоненту и хотим реагировать на их изменения внутри дочернего. Таким watch мы должны попасть в рекурсию и переполнить callstack. Попробуем?
Пробуем… Но оно не работает, мы не проваливаемся в рекурсию, да и данные в родителе изменяются ровно один раз! Это законно? Да, давайте посмотрим внимательнее. Первый watch наблюдает за localObj.value, который автоматически стал глубоким. Переписав его, мы “сломали” watch. Убедимся в этом, добавив следующий код:
И как результат наблюдаем такое же поведение.
Ситуация исправится, если мы будем следить за функцией getter
Да! Вот она! Долгожданная ошибка!
[Vue warn]: Maximum recursive updates exceeded in component <Repl>. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.
Но и это не все. Если нажать на кнопку, которая изменяет данные в родителе, то мы не увидим изменений в <input>! Проблема в том, что мы следим за изменением всего props.modelValue, а не его содержимым. Вот так заработает:
Стоит отметить, что вместо первого watch мы вполне можем использовать watchEffect, результат будет такой же — ожидаемая рекурсия и переполнение callstack.
Всё это безобразие не решает нашей проблемы с реагированием на изменения входных данных. Возможно “костыльно”, но одно из самых простых и прямых решений — это добавление ещё одного props, в качестве триггера, когда данные нужно обновить!
Выглядит это всё совсем не изящно и просит изменений! Попробуем это всё безобразие изменить.
Composable подход
Vue 3 позволяет нам многое. Мы можем отказаться от пресловутых mixins, которые вносят много “тайной магии” в наш код и затрудняют его чтение, а так же выносить целые блоки логики, используемой в нескольких компонента или даже в других блоках!
Работа с Composable тянет на отдельную статью, которые уже есть (Документация, VueSchool). Мы же рассматриваем совершенно другие вещи, поэтому как-нибудь в другой раз .
Прежде чем использовать подход, показанный ниже, следует понимать риски, которые он с собой несет. Такое использование “внешних” переменных приводит к тому, что компоненты могут менять их состояние напрямую. Связанность компонентов становится только “смысловой” и совершенно не читается в коде! Я предпочитаю написать несколько комментариев для коллег, при подобных решениях.
Добавим один файл:
Работает. Но посмотрите внимательно на App.vue. Что мы здесь видим? Импорт чего-то, изменение его свойств и компонент Form.vue, который будто завис в воздухе.
С компонентом нам нечего делать, а вот с импортом чего-то мы можем немного “поколдовать”.
Изменили мы совсем немного, но читабельность увеличилась! А React-разработчики увидят здесь что-то знакомое.
В завершении
Ни один поход не является серебряной пулей! Каждый из подходов, включая не рассмотренные Vuex/Pinia должен применяться в зависимости от задачи. Не стоит совершать ошибку, вынося весь код в utils.js, Vuex/Pinia или composable (use… функции).
Отличными примерами, в которых я так или иначе “ковырялся”, могут быть исходники Quasar и Vuetify.
Debouncing User Input with Vue: Delay the input event until the user stops typing
Lukas Hermann
- HTML
- JS
- Vue
Sometimes I want to persist user input to the server without the need for “Submit” or “Save” buttons. The user types into the text field and the app takes care of everything else without any further necessary action.
So-called “debouncing” can achieve this by waiting until the user stopped typing before sending the HTTP request to the server. This article shows how I achieved this with a handy custom Vue component.
# The Problem
I was working on my app stagetimer.io. It allows multiple participants to connect with their devices to view the same countdown timer. Each user can enter their name into a field so the controller can see who is connected. The app saves the name as it is typed, but the server can also update it. This posed two problems:
- Every keystroke fires an input event. Therefore it is necessary to debounce this event, that is, wait until the user stopped typing before saving it to the server, so it doesn’t trigger too many HTTP requests.
- A server response can also update the text of the input field. But while the user is typing it is relevant to prevent updating the value so the user doesn’t experience “jumping” of the text.
# The Solution
This Vue component solves both problems. It is designed to be used as a drop-in replacement for the native <input> tag.
First, here is the component in full. Underneath I explain what each part does and how it works.
# 1–7 <template>
<input> is the only HTML element inside the template. I use a copy of the passed value prop, called internalValue , for reasons explained later. The type prop is just a passthrough, more can be added as required.
I am not using v-model to keep track of user input with the touched variable, see line 31.
# 13–16 Props
Besides the mandatory value prop I am giving default values to all others. This way I can safely omit them when using the component.
# 17–22 Data
I could use value directly and pass it to the input element, but this can lead to a race condition. If the app sends the value to the server it is common for it to respond with the same value causing an app update. Had the user continued typing, this update would reset the value removing the last typed characters. Therefore I added a decoupled internalValue to keep track of user input with the touched variable.
# 23–27 Watcher
Here I am watching the value prop for changes and update the internalValue only if the user hasn’t touched the input, implying he hasn’t typed anything else in the meantime.
# 29–32 updateInternalValue() Method
Every keystroke triggers this method. With touched = true I can keep track of it. It then calls the updateValue() method with that magic debouncing mechanic.
# 33–37 updateValue() Method
This method is the heart of the entire component. It uses lodash’s debounce method. updateValue() can be called multiple times with the same parameters and only executes the callback function the delay of 600 ms has passed after the last call.
It is essential to use an anonymous function here, not an arrow function, to preserve Vue’s this context.
I set touched = false once the callback is executed, signifying that at this time the value is passed to the parent component. Changes to the value prop can now be updated safely to my internalValue until the user starts typing again.
Afterward, I emit two events. The input event enables the use of v-model with this component, and the update:value event ensures that Vue’s two-way binding value.sync also works.
Note: This guide is for Vue version 2. For Vue 3 it is necessary to rename the prop value to modelValue , see https://v3.vuejs.org/guide/migration/v-model.html#v-model.
Liked this article?
© Lukas Hermann · Brenzstrasse 6 · 74405 Gaildorf · Germany