DEV Community

The Jared Wilcurt
The Jared Wilcurt

Posted on

Trigger a child component method from parent (Vue)

Vue 3 Approaches:

I've attempted to make this list as comprehensive as possible with code examples.

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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';
};
Enter fullscreen mode Exit fullscreen mode
<template>
  <div>{{ text }}</div>
</template>

<script>
import { text } from './helper.js';

export default {
  name: 'MyChild',
  computed: {
    text: function () {
      return text.value;
    }
  }
};
</script>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

Helper - Script Setup

import { ref } from 'vue';

export const text = ref('Hello');

export const setText = function () {
  text.value = 'World';
};
Enter fullscreen mode Exit fullscreen mode
<template>
  <div>{{ text }}</div>
</template>

<script setup>
import { text } from './text.js';

defineOptions({
  name: 'MyChild'
});
</script>

Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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';
    }
  }
});
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode
<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>

Enter fullscreen mode Exit fullscreen mode

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 };
});
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>
Enter fullscreen mode Exit fullscreen mode

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)