How many times have you seen (or written) code where a parent component passes a boolean prop to a child component solely to trigger a method inside that child?
<!-- ❌ Parent passing a trigger flag -->
<ChildComponent :shouldReset="isResetting"/>
Enter fullscreen mode Exit fullscreen mode
Inside the child component, you end up writing an artificial watcher just to catch that state flip:
<!-- ❌ Child watching a prop just to run a method -->
<script setup>
import { watch } from 'vue'
const props = defineProps<{ shouldReset: boolean }>()
watch(() => props.shouldReset, (newVal) => {
if (newVal) {
resetForm() // Running internal logic based on prop change
}
})
</script>
Enter fullscreen mode Exit fullscreen mode
This is a classic Vue code smell. Passing state down when you actually mean to send a command creates awkward state-toggling hacks, extra reactivity cycles, and unnecessary watcher logic.
Here is how to clean up this anti-pattern using defineExpose and Vue 3.5’s useTemplateRef().
Why “Props + Watchers” as Action Triggers is a Smell
-
Artificial State Pollution: You have to manage boolean flags in the parent (
isResetting = true, then back tofalse) just to fire a one-time event. - Reactivity Overhead: Vue has to track the prop dependency, queue the watcher, evaluate the callback, and schedule component re-renders—all to invoke a simple function.
- Cluttered Child Logic: The child component needs boilerplate code to watch a prop, handle boolean states, and reset the flag.
Vue is primarily declarative, but UI actions are inherently imperative. Focusing an input, resetting a form, playing an animation, or stepping through a wizard are actions, not state.
The Better Pattern: defineExpose + useTemplateRef
Instead of forcing declarative props onto an imperative action, expose the child’s internal function directly to the parent.
Step 1: Explicitly Expose the Method in the Child
By default, components using <script setup> are closed. External components cannot access internal variables or functions unless you explicitly expose them using defineExpose.
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const formState = ref({ name: '', email: '' })
// The internal action
const resetForm = () => {
formState.value = { name: '', email: '' }
console.log('Form reset executed!')
}
// Explicitly grant access to external parents
defineExpose({
resetForm
})
</script>
<template>
<form>
<input v-model="formState.name" placeholder="Name" />
<input v-model="formState.email" placeholder="Email" />
</form>
</template>
Enter fullscreen mode Exit fullscreen mode
Step 2: Call the Exposed Method in the Parent (Vue 3.5)
In Vue 3.5+, use useTemplateRef() to obtain a strongly-typed reference to the child component instance, then call the method directly.
<!-- ParentComponent.vue -->
<script setup>
import { useTemplateRef } from 'vue'
import ChildComponent from './ChildComponent.vue'
// Obtain the reference using Vue 3.5's useTemplateRef
const childRef = useTemplateRef('childRef')
const handleResetButtonClick = () => {
// Directly invoke the exposed method on the child instance
childRef.value?.resetForm()
}
</script>
<template>
<div>
<ChildComponent ref="childRef"/>
<button @click="handleResetButtonClick">Reset Child Form</button>
</div>
</template>
Enter fullscreen mode Exit fullscreen mode
The Architectural Balance: When to Use What
A common rule in Vue is “Props Down, Events Up”. Reaching into child components with refs should not replace standard data flow, but knowing when to use each pattern keeps your architecture clean:
Scenario Recommended Pattern Why Passing data into a child Props (:data="...")
Declarative state rendering
Notifying a parent about child actions
Emits (@submit="...")
Decoupled event notification
Triggering imperative child UI actions
defineExpose + `useTemplateRef`
Direct command execution without state hacks
Summary
If your component requires a prop and a watch solely to trigger an internal function, you are using reactivity as a workaround for command execution.
Leverage defineExpose alongside Vue 3.5’s useTemplateRef() to keep component logic clean, imperative, and free of artificial state flags.
답글 남기기