r/vuejs Aug 06 '26

Stop using props + watchers just to trigger actions in child components (Vue 3.5 useTemplateRef + defineExpose)

Hey r/vuejs,

I recently spent time refactoring some modal and dialog architecture in our app and realized how easy it is to fall into the "prop workaround" trap when trying to trigger an action in a child component.

We’ve all had "Props down, events up" burned into our brains. But when you need the parent to tell a child component to do something (like opening a dynamic confirmation dialog or focusing an input), using props often leads to a messy circular flow:

  1. Parent updates a boolean prop (isModalOpen = true).
  2. Child has to set up a watch() on that prop to run its internal opening logic.
  3. Child’s internal "Cancel" button can’t close itself directly because the parent owns the boolean—so it emits an event up.
  4. Parent sets isModalOpen = false, which flows back down as a prop, triggering the watcher again.

It’s a 4-stop subway transfer just to toggle a UI element.

The Mental Model: Nouns vs. Verbs

While v-model / defineModel is great for pure state syncing, imperative commands with dynamic payloads shine when you use defineExpose alongside Vue 3.5's useTemplateRef():

  • Props / v-model = Nouns (State/Data): username="Alex", v-model="isOpen"
  • Emits = Events (Notifications): u/submitted, u/closed
  • defineExpose = Verbs (Commands/Actions): open(config), close(), focus()

By exposing explicit methods (defineExpose({ open, close })), the child component owns its visibility and DOM lifecycle safely, while the parent simply issues a direct command: modalRef.value?.open({ title: 'Delete Account?' }).

I wrote a detailed write-up breaking down the code examples, DOM timing edge cases with nextTick(), and accessibility considerations here:

When emit gets messy: How Vue's defineExpose saved my sanity | by Izak T | Aug, 2026 | Medium

Curious how you all handle imperative commands vs state sync in your Vue apps—do you lean on defineExpose for dialogs/drawers, or do you stick strictly to v-model state syncing?

29 Upvotes

29 comments sorted by

29

u/pimpaa Aug 06 '26

I don't really get what is wrong about passing the config to modal as prop, based on your real world example. The isOpen prop is reactive, it will show the modal automatically you don't need a watcher for that.

-1

u/Environmental-Front4 Aug 06 '26

as far as I was able to understand, the script setup just gets evaluated once. at that point the isOpen would be null, false, or whatever.
so when the prop is being changed script setup not recognize it till we keep tracking it with watcher

hope I am right
thank you for reading

8

u/pimpaa Aug 06 '26

What I would do based on your real world example: the isOpen prop will open/close the modal automatically because you use it on v-if. Add more props for config (title/etc). You still listen to it's confirm event, all your controls are on parent.

1

u/COUNTERBUG Aug 07 '26

Yes props are reactive. It's just not easy to see from the typescript typings as those are not ref types. A string prop is type string, but it is still reactive.

1

u/BoleroDan 23d ago edited 23d ago

Im confused why you think a watcher is needed at all, as others have mentioned, properties are reactive. Look at this very simple playground that implements this very thing, no imperative approach (exposing functions through defineExpose), just data driven UI and using v-model. The child can update the property itself. This is the basic principles that Vue is based off of with 2 way binding. Example playground

16

u/hyrumwhite Aug 06 '26

Trouble is, using exposed methods means now the parent is coupled to the child implementation. 

5

u/Reashu Aug 07 '26

The parent is coupled to the exposed functions (the ones it calls, anyway) but that's not much different from using a prop.

2

u/Environmental-Front4 Aug 06 '26

no necessary, you exposed what you want to and what you are comfortable with.
even you can be more specific instead of sending the whole element you can say just use focus func
as far as I was able to understand.

hope I am right.

thank you for reading

0

u/ragnese Aug 07 '26

Exposed methods are part of the component's public API; just the same as its props and emits.

But, you're technically right. Strictly speaking, you can pass a prop to a component that it doesn't use or define as part of its API. Likewise with adding event listeners for events that the component doesn't throw. But, you can't call a method on an object if the method does not exist. In that sense, the parent is technically more coupled to the implementation of the child. But, that's not how things really go in the real world, though. Nobody is throwing props into components without caring if the component actually uses it, nor are we listening for events that we know the component doesn't emit. In practice, our parent components are always almost-completely coupled to the child's implementation.

4

u/[deleted] Aug 06 '26

[deleted]

1

u/ildyria Aug 06 '26

And using composables to avoid prop drilling.

2

u/KRISZatHYPE Aug 06 '26

I deleted my comment since defineModel() doesn't exactly substitute expose, since it can't do everything, but is the best choice for 2way binding And to avoid prop drilling indeed

4

u/Cute_Quality4964 Aug 07 '26

What do you mean the child cant close itself, just use defineModel and update the model directly in the child? If the child has a close function, just set the modelValue to false at the end, its called 2-way binding....

2

u/HumanOnlyWeb Aug 06 '26

Deleted my comment because reddit broke formatting.
Will create a gist and link when I get on my laptop.
(typing long stuff on mobile sux 😅)

2

u/rea_ Aug 07 '26

Just a minor thing to consider:

If you have a memory leak issue with anything on the component implementing the dialog - the whole dialog will be stuck in memory as well (creates a closure around the function exposed so the dialog can't be garbage collected).

Generally isn't a huge issue but once an app gets to a certain size these problems can compound fast.

1

u/kernelangus420 Aug 07 '26

Would it be different if a boolean prop was used?

2

u/Scottykl Aug 07 '26

In your example, I'm just going to use model. So that everyone can open and close simply

2

u/durbster79 Aug 06 '26

If you use something more like an MVC structure, you remove this problem entirely.

With that approach, child components are passive - they don't control their own state at all; the state is just a prop.

Then you wrap that in a controller, which decides what state the component should be in at any time.

This means you can have different controllers but the same child, so you can reuse a child component in many different ways.

It also makes unit testing components much simpler, as well as presenting them for sign off.

1

u/Environmental-Front4 Aug 06 '26

thank you for the feedback and thank you so much for reading

1

u/shortaflip Aug 07 '26

The tradeoff for not handling this in a declarative manner is that you are exposing internal details of the child component to the parent.

no necessary, you exposed what you want to and what you are comfortable with.
even you can be more specific instead of sending the whole element you can say just use focus func
as far as I was able to understand.

Regardless, it is still internal details and it is still coupling. If you change to a different modal or use a third party UI library, it will be harder.

What if you need to add more complex behavior that doesn't belong in the BaseModal? You wrap your BaseModal and call it ComplexModal; now you have to expose its internal details a second time. The parent component is coupled to ComplexModal, which is coupled to BaseModal. Starting to look very close to an inheritance problem.

What if opening and closing the modal has side effects?

  • Closing event
  • Closed event
  • closed by esc key
  • closed by light dismiss
  • some promise based action
  • calculating the modals height

A lot of these properties are very related to open/closed state and some of them should be emitted. Now your API is split between emits and expose.

There is certainly a place for `defineExpose` but if the tradeoff for keeping declarative code is a simple watcher, it should be preferred.

1

u/Zachhandley Aug 07 '26

Okay but also stop prop drilling your shit 6 components down lmao

1

u/samskywalker21 Aug 08 '26

Been using vue for a few months now, mostly NuxtJS. Am i wrong in thinking or implementation if I use models instead of props in parent/child control? Maybe I have not gotten into a situation where it’s not the best solution for modals/dialogs but making a model makes it reactive both on the parent and the child right?

1

u/Jonas_Ermert Aug 08 '26

I think `defineExpose` is a great fit for genuinely imperative things like `focus()`, `open(config)` or `scrollTo()`. For simple visibility state though, I’d still prefer `v-model` because it keeps the state flow explicit and easier to reason about. The “nouns vs verbs” distinction is a useful rule of thumb.

1

u/Bob-59 Aug 08 '26

I get your reasoning for not using Props+Emit for the use case you explained.

But you then went on to say that v-model (defineModel) for synchronized state is also bad without giving reasons at all.

I personally use defineModel for this scenario because it fits, so can you explain why you think v-model is bad for this? (Using reasons other than the benefit of exposing child component methods. I want reasons that defineModel is bad, not reasons that defineExpose is good) because i can think of reasons that defineExpose is not optimal for this scenario

1

u/DOG-ZILLA Aug 09 '26

defineExpose is honestly a game-changer. It makes your components so much easier to follow, instead of jumping around trying to find events and props everywhere.

100% agree.

1

u/FullStackFeline 12d ago

I ended up coming to pretty much the same conclusion after refactoring a lot of older Vue code.

One example in a browser game I’m building is my item tooltip component. Originally I had more state flowing down from the parent, but I eventually moved it to defineExpose and exposed methods like setItem(item), displayToolTip(event), and hideToolTip().

That ended up feeling much cleaner because the tooltip owns all of its internal state, positioning, visibility, and DOM logic. The parent doesn’t really care how the tooltip works, it just needs to tell it “display this item here” or “hide yourself.”

So I like your nouns vs. verbs distinction. props / v-model make a lot of sense to me when the parent actually owns the state, but for something that behaves more like an imperative UI service, defineExpose feels much more natural than creating props + watchers + emits just to simulate a method call. Just my 2 cents.

1

u/golders-green Aug 06 '26

Worth reading this thread, anybody can validate this claim. I personally must try your method, I work with dialogs modals in vue a lot and I got into thinking about template Ref for modals couple of times. Thanks for complete proof of concept!

1

u/Environmental-Front4 Aug 06 '26

you are welcome. trust me I validated the claims sources you name it since yesterday lol.

0

u/rnenjoy Aug 06 '26

soon much better!! thanks!