Globally Accessible Component Instance

553 views Asked by At

In our production applications with Vue 2.x, we have a toast component. This toast component is mounted once via a plugin (code below) and is then added to the Vue prototype making it accessible in every component instance.

This makes life a lot easier instead of having to add the toast to everywhere we use.

Vue 2.x plugin

export default {
install(vue: any, _: any) {
    const root = new Vue({ render: (createElement) => createElement(Toast) });
    root.$mount(document.body.appendChild(document.createElement("div")));

    const toastInstance: Toast = root.$children[0] as Toast;
    vue.prototype.$toast = {
        show: (state: ToastState, text: string) => { toastInstance.show(state, text); },
        hide: () => { toastInstance.hide(); }
    };
}

Which can then be called in any component like:

this.$toast.show(ToastStates.SUCCESS, "Some success message");

I have recently started another project and would like to do something similar, except using Vue 3. Because we don't have access to this in the setup function, I can't use the same approach as before.

I have been looking into a few things, and have found a few ways of doing it, but none as a definitive best practice.

Provide / Inject: This seems the most promising, where I can use

export const appInstance = createApp(App);

then

appInstance.provide("toast", toastComponentInstance)

which I can then inject in any components. The problem with this, is that to get it available in every component, it needs to be attached to the initial app instance, where it hasn't been created yet. Maybe I could manually mount it and pass it in (but that seems like a hack).

Composition: I have also looked at this issue here: How to access root context from a composition function in Vue Composition API / Vue 3.0 + TypeScript? but didn't find that very useful and I had to do all types of hacks to actually gain access to the plugin. Gross code below..

export function useToast() {

    const root = getCurrentInstance();

    const openToast: (options: ToastOptions) => void = (options: ToastOptions) => {
        root.ctz.$toast.open(options);
    }

    const closeToast: () => void = () => {
        root.ctx.$toast.close();
    }

    return {
        openToast,
        closeToast
    }
}

I have other ideas but they seem far fetched an hacky. Keen to hear peoples thoughts on other solutions. I just want a simple way to have 1 instance of a toast, that I can call two functions on to open / close it when and where I want.

1

There are 1 answers

0
Daniel On

This is roughly how I'd do it...

I'd use Composition API, because it makes passing around internals easy

(I'm using popup instead of toast for simplicity)

myPopup.vue


// internal
const popupMessage = Vue.ref('');
const popupVisible = Vue.ref(true);

// external
export const popUpShow = function(message) {
    popupMessage.value = message
  popupVisible.value = true
}
export const popupHide = function () {
    popupVisible.value = false
}

export default {
    setup(){
        return {
        popupMessage, popupVisible, popupHide
    }   
  }
}

Some component, anywhere, composition or class based...

import { popUpShow } from "./myPopup";

export default {
  methods: {
    myTriggeredEvent() {
      popUpShow("I am your Liter")
    }
  }
}

By exposing popUpShow, which acts as a singleton, you can import that from anywhere, and not have to worry about context.

There the drawback in using this kind of setup/architecture is that it doesn't scale well. The problem happens if your architecture reaches a certain size, and you have multiple triggers coming from various sources that the component needs to have complex logic to handle its state (not likely for this example though). In that case, a managed global store, ie. Vuex, might be a better choice.