Vue 3 Approaches:
I've attempted to make this list as comprehensive as possible with code examples.
-
A. Use
ref
s - B. Prop trigger
- C. Module file instantiation
- D. Composable
- E. Pinia
- F. Emit Callback
- G. Event Bus
Approach A: Ref
Add a Ref to the child and access its internals from the parent.
Ref - Options API
<template>
<div>{{ text }}</div>
</template>
<script>
export default {
name: 'MyChild',
data: function () {
return {
text: 'Hello'
};
},
methods: {
setText: function () {
this.text = 'World';
}
}
};
</script>
<template>
<MyChild ref="myChild" />
<button @click="$refs.myChild.setText">Set text</button>
</template>
<script>
import MyChild from './MyChild.vue';
export default {
name: 'MyParent',
components: { MyChild }
};
</script>
Ref - Script Setup
<template>
<MyChild ref="myChild" />
<button @click="setChildText">Set text</button>
</template>
<script setup>
import { useTemplateRef } from 'vue';
import MyChild from './MyChild.vue';
defineOptions({
name: 'MyParent'
});
function useChildDoThing () {
const MyChildRef = useTemplateRef('myChild');
function setChildText () {
MyChildRef.value.setText();
}
return { setChildText };
}
const { setChildText } = useChildDoThing();
</script>
<template>
<div>{{ text }}</div>
</template>
<script setup>
import { ref } from 'vue';
defineOptions({
name: 'MyChild'
});
function useText () {
const text = ref('Hello');
const setText = function () {
text.value = 'World';
};
return { text, setText };
}
const { text, setText } = useText();
defineExpose({
setText
});
</script>
Approach B: Prop Trigger
Flip a boolean prop on the child from the parent. The child watches the prop and runs the method. This requires deeper coupling of a boolean prop tied to a method, and a watcher connecting them. Becase of this, it is not advised. Also unit testing components that use watchers is always harder.
Trigger - Options API
<template>
<div>{{ text }}</div>
</template>
<script>
export default {
name: 'MyChild',
props: {
triggerSetText: Boolean
},
data: function () {
return { text: 'Hello' };
},
methods: {
setText: function () {
this.text = 'World';
}
},
watch: {
triggerSetText: function () {
this.setText();
}
}
};
</script>
<template>
<MyChild :triggerSetText="setTextSwitch" />
<button @click="setChildText">Set text</button>
</template>
<script>
import MyChild from './MyChild.vue';
export default {
name: 'MyParent',
components: { MyChild },
data: function () {
return {
setTextSwitch: true
};
},
methods: {
setChildText: function () {
this.setTextSwitch = !this.setTextSwitch;
}
}
};
</script>
Trigger - Script Setup
<template>
<div>{{ text }}</div>
</template>
<script setup>
import { ref, watch } from 'vue';
defineOptions({
name: 'MyChild'
});
const props = defineProps({
triggerSetText: Boolean
});
function useText (props) {
const text = ref('Hello');
function setText () {
text.value = 'World';
}
watch(
() => { return props.triggerSetText; },
() => { setText(); }
);
return { text };
}
const { text } = useText(props);
</script>
<template>
<MyChild :triggerSetText="childTextTrigger" />
<button @click="setChildText">Set text</button>
</template>
<script setup>
import { ref } from 'vue';
import MyChild from './MyChild.vue';
defineOptions({
name: 'MyParent'
});
function useSetChildText () {
const childTextTrigger = ref(true);
function setChildText () {
childTextTrigger.value = !childTextTrigger.value;
}
return {
childTextTrigger,
setChildText
};
}
const {
childTextTrigger,
setChildText
} = useSetChildText();
</script>
Approach C: Module Helper File
Initialize your shared reactive state in a module, export the state and logic as needed.
Helper - Options API
import { ref } from 'vue';
export const text = ref('Hello');
export const setText = function () {
text.value = 'World';
};
<template>
<div>{{ text }}</div>
</template>
<script>
import { text } from './helper.js';
export default {
name: 'MyChild',
computed: {
text: function () {
return text.value;
}
}
};
</script>
<template>
<MyChild />
<button @click="setText">Set text</button>
</template>
<script>
import MyChild from './MyChild.vue';
import { setText } from './helper.js';
export default {
name: 'MyParent',
components: { MyChild },
methods: { setText }
};
</script>
Helper - Script Setup
import { ref } from 'vue';
export const text = ref('Hello');
export const setText = function () {
text.value = 'World';
};
<template>
<div>{{ text }}</div>
</template>
<script setup>
import { text } from './text.js';
defineOptions({
name: 'MyChild'
});
</script>
<template>
<MyChild />
<button @click="setText">Set text</button>
</template>
<script setup>
import MyChild from './MyChild.vue';
import { setText } from './text.js';
defineOptions({
name: 'MyParent'
});
</script>
Approach D - Composable
Similar to the above, but instead of instantiating in the helper module, your wrap the logic in a function call as a composable. This allows shared instantiation, like above, but also for seperate instances of state.
import { ref } from 'vue';
// For new context
export const useText = function () {
const text = ref('Hello');
const setText = function () {
text.value = 'World';
};
return { text, setText };
};
// For shared context
export const { text, setText } = useText();
Approach E - Pinia
Move the shared component state to a Pinia store. Move the shared logic to a Pinia action. Import them as needed from the store in each component.
Pinia - Options
import { defineStore } from 'pinia';
export const textStore = defineStore('text', {
state: function () {
return {
text: 'Hello'
};
},
actions: {
setText: function () {
this.text = 'World';
}
}
});
<template>
<div>{{ text }}</div>
</template>
<script>
import { mapState } from 'pinia';
import { textStore } from '../stores/text.js';
export default {
name: 'MyChild',
computed: {
...mapState(textStore, [
'text'
])
}
};
</script>
<template>
<MyChild />
<button @click="setText">Set text</button>
</template>
<script>
import { mapActions } from 'pinia';
import MyChild from './MyChild.vue';
import { textStore } from '../stores/text.js';
export default {
name: 'MyParent',
components: { MyChild },
methods: {
...mapActions(textStore, [
'setText'
])
}
};
</script>
Pinia - Script Setup
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const textStore = defineStore('text', () => {
const text = ref('Hello');
const setText = function () {
text.value = 'World';
};
return { setText, text };
});
<template>
<div>{{ text }}</div>
</template>
<script setup>
import { computed } from 'vue';
import { textStore } from '../stores/text.js';
defineOptions({
name: 'MyChild'
});
const text = computed(() => {
return textStore().text;
});
</script>
<template>
<MyChild />
<button @click="setText">Set text</button>
</template>
<script setup>
import MyChild from './MyChild.vue';
import { textStore } from '../stores/text.js';
defineOptions({
name: 'MyParent'
});
const { setText } = textStore();
</script>
Approach F - Emit a callback
The child component could emit an object with a reference to a function in the child, that can then be called by the parent. Gonna be honest, I've been doing Vue for 8 years and never once seen anyone do this. I did test it and shockingly it works. Probably has some performance downsides though. If you use this approach you're probably doing something very wrong. I don't see any advantage to using this over just using a ref
which is officially recommended and simpler.
Emit Callback - Options API
<template>
<div>{{ text }}</div>
</template>
<script>
export default {
name: 'MyChild',
data: function () {
return {
text: 'Hello'
};
},
methods: {
setText: function () {
this.text = 'World';
}
},
created: function () {
this.$emit('innerMethods', {
setText: this.setText
});
}
};
</script>
<template>
<MyChild @innerMethods="childMethods = $event" />
<button @click="childMethods.setText()">Set text</button>
</template>
<script>
import MyChild from './MyChild.vue';
export default {
name: 'MyParent',
components: {
MyChild
},
data: function () {
return {
childMethods: {}
};
}
};
</script>
Emit Callback - Script Setup
<template>
<div>{{ text }}</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
defineOptions({
name: 'MyChild'
});
const emit = defineEmits(['innerMethods']);
function useText () {
const text = ref('Hello');
const setText = function () {
text.value = 'World';
};
onMounted(() => {
emit('innerMethods', { setText });
});
return { text };
}
const { text } = useText();
</script>
<template>
<MyChild @innerMethods="childMethods = $event" />
<button @click="childMethods.setText()">Set text</button>
</template>
<script setup>
import { ref } from 'vue';
import MyChild from './MyChild.vue';
defineOptions({
name: 'MyParent'
});
const childMethods = ref({});
</script>
Approach G: Event Bus
Vue 2 had an event bus pattern. It sucked, and you should not have ever used it. They told people not to, but they did anyway. In Vue 3, the entire concept was abandoned and removed. But just because it's not possible, and was always a bad idea, doesn't mean you can't still do it. It would require pulling in an additional dependency (npm i --save mitt
). But I'm not gonna put a code example for this one because it's a dumb idea. Don't do it. Even dumber than the callback example above, because it involves maintaining another dependency.
Editor's Note: This was originally on Stack Overflow, but I deleted it from there after the 9th time the mods tried to edit it. Now it lives here. Where it will still be used for AI training data, without my permission, but at least StackOverflow won't profit off of it, and the psycho mods over there can't fuck with it to make it inaccurate.
Top comments (0)