Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to access root context from a composition function in Vue Composition API / Vue 3.0 + TypeScript?

I want to create a reusable wrapper function written in TypeScript for triggering a toast notification by using a composition function, as defined in the current specification for Vue 3.0: Composition API RFC.

This example is using BootstrapVue v2.0 toast component. With Vue 2, it would be invoked via the this.$bvToast Vue component instance injection in the root context:

this.$bvToast.toast('Error happened', {
  title: 'Oh no',
  variant: 'danger'
});

This service-like composition function would look much like this:

// File: @/util/notify.ts
export function useNotify() {
  const notifyError = (title: string, msg: string) => {
    // How to access context.root as in a function component, without passing it to this function?
    context.root.$bvToast.toast(msg, {
      title,
      variant: 'danger'
    });
  };

  return { notifyError};
}

export default useNotify;

And would be used much like this:

// Use in your functional component:
import { createComponent } from '@vue/composition-api';

import { useNotify} from '@/util/notify';

export default createComponent({
  name: 'MyFailingComponent',
  setup() {
    const { notifyError } = useNotify();

    notifyError('Request error', 'There was an error processing your request, please try again later.');

    return {};
  }
});
like image 367
ux.engineer Avatar asked Oct 27 '19 13:10

ux.engineer


3 Answers

Well, I soon found out a proper example on that same RFC site. But decided to share my examples here.

The RFC site doesn't include examples in TypeScript at the moment, for clarity's sake I presume. As this new way of writing Vue 3.0 components and composition functions (as a replacement to Mixins) takes a bit of getting used to.

Answer: You can pass the context object directly to the composition function when object-destructuring the needed parts into your component code.

// File: @/util/notify.ts
// import { SetupContext } from '@vue/composition-api';

export function useNotify({ root }) {
  const notifyError = (title: string, msg: string) => {
    root.$bvToast.toast(msg, {
      title,
      variant: 'danger'
    });
  };

  return { notifyError };
}

export default useNotify;
// Use in your functional component:
import { createComponent, SetupContext } from '@vue/composition-api';

import { useNotify} from '@/util/notify';

export default createComponent({
  name: 'MyFailingComponent',
  setup(props: any, context: SetupContext) {
    const { notifyError } = useNotify(context);

    notifyError('Request error', 'There was an error processing your request, please try again later.');

    return {};
  }
});

Same using TypeScript types with complex object destructuring, when passing several function arguments as an object:

// File: @/util/notify.ts
import { SetupContext } from '@vue/composition-api';

export function useNotify({ context, defaultTitle = 'Hey!' }: { context: SetupContext, defaultTitle?: string }) {
  const notifyError = (msg: string, title?: string) => {
    context.root.$bvToast.toast(msg, {
      title: title || defaultTitle,
      variant: 'danger',
    });
  };

  return {
    notifyError,
  };
}

export default useNotify;

// Usage like:
const { notifyError } = useNotify({ context });
// Or
const { notifyError } = useNotify({ context, defaultTitle: 'Hey there' });

Neat syntax, well done Vue community!

like image 145
ux.engineer Avatar answered Nov 12 '22 23:11

ux.engineer


There is also:

import { getCurrentInstance } from 'vue'  // or from '@vue/composition-api'

This will get the calling component's root context from this method.

const root = getCurrentInstance();  // same as ctx.root in component
like image 40
MathGainz Avatar answered Nov 12 '22 21:11

MathGainz


You might end up passing the context to every composable because their dependencies could require the context themselves.

There is an alternative solution for providing the root instance without the need to pass it to every composable you have. This makes their usage in components a bit easier:

You can create a generic useRoot composable and implement it with Vue's provide/inject feature:

// File: @/components/Root.js
// This is the app root
import useRoot from '@/composables/useRoot'

export default {
  setup(props, context) {
    const { provideRoot } = useRoot()
    provideRoot(context.root)
  }
}
// File: @/composables/useFancyStuff
// This is your composable (no arguments needed!)
import useRoot from '@/composables/useRoot'

export default function useNavigation() {
  const { injectRoot } = useRoot()
  const { $router } = injectRoot() // if you want to use the router

  $router.push('/')
}
// File: @/composables/useRoot
// The implementation
import { provide, inject } from '@vue/composition-api'

const ProviderSymbol = Symbol()

export default function useRoot() {
  const provideRoot = root => provide(ProviderSymbol, root)
  const injectRoot = () => inject(ProviderSymbol)

  return {
    provideRoot,
    injectRoot
  }
}
like image 3
nirazul Avatar answered Nov 12 '22 21:11

nirazul