Hi, everybody! This is my first article in Dev.to and I'm really happy to be able to share my recent experiences using Nuxt.js and Vue Composition API.
I'm currently working on a small toy project based on Nuxt. This project uses the following skill base.
- Nuxt.js
- Typescript
- Vuetify
- Storybook
In addtion, I added the Vue Composition API that will be used in Vue3. However, there ware some problems in the environment using Nuxt and Typescript.
So let's get started! What problems were there and how to solve them.
Nuxt ComponentOptions
A Nuxt.js provides a variety of component options and if you using Typescript, you can find component options in @nuxt/types
// node_modules/@nuxt/types/app/vue.d.ts
/**
* Extends interfaces in Vue.js
*/
import Vue from 'vue'
import { MetaInfo } from 'vue-meta'
import { Route } from 'vue-router'
import { Context, Middleware, Transition, NuxtApp } from './index'
import { NuxtRuntimeConfig } from '../config/runtime'
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
asyncData?(ctx: Context): Promise<object | void> | object | void
fetch?(ctx: Context): Promise<void> | void
fetchDelay?: number
fetchOnServer?: boolean | (() => boolean)
head?: MetaInfo | (() => MetaInfo)
key?: string | ((to: Route) => string)
layout?: string | ((ctx: Context) => string)
loading?: boolean
middleware?: Middleware | Middleware[]
scrollToTop?: boolean
transition?: string | Transition | ((to: Route, from: Route | undefined) => string | Transition)
validate?(ctx: Context): Promise<boolean> | boolean
watchQuery?: boolean | string[] | ((newQuery: Route['query'], oldQuery: Route['query']) => boolean)
meta?: { [key: string]: any }
}
}
declare module 'vue/types/vue' {
interface Vue {
$config: NuxtRuntimeConfig
$nuxt: NuxtApp
$fetch(): void
$fetchState: {
error: Error | null
pending: boolean
timestamp: number
}
}
}
But, when you using the Vue Composition API in Nuxt components, the basic Types scope chagnes from @nuxt/types
to @vue/composition-api
Therefore, we can not use the types for some component options that only nuxt has like the layout
, middleware
, fetch
Let's see an example.
<template>
<div>Hello Vue Composition API!</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api'
export default defineComponent({
layout: 'some-layout' // Error: No overload matches this call
})
</script>
Basically, to use composition-api in the Typescript environment, we declare the definedComponent
.
If we want to use the layout
property, we have to declare it in definedComponent
, but you will see an error(or warning) that the type cannot be found in the IDE or Editor.
In this situation we can infer why the layout
is not avaliable.
// node_modules/@vue/composition-api/dist/index.d.ts
import Vue$1, { VueConstructor, ComponentOptions, VNode, CreateElement } from 'vue';
...
interface ComponentOptionsBase<Props, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}> extends Omit<ComponentOptions<Vue, D, M, C, Props>, 'data' | 'computed' | 'method' | 'setup' | 'props'> {
data?: (this: Props, vm: Props) => D;
computed?: C;
methods?: M;
}
...
declare type ComponentOptionsWithoutProps<Props = unknown, RawBindings = Data, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}> = ComponentOptionsBase<Props, D, C, M> & {
props?: undefined;
setup?: SetupFunction<Props, RawBindings>;
} & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>;
...
declare function defineComponent<RawBindings, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}>(options: ComponentOptionsWithoutProps<unknown, RawBindings, D, C, M>): VueProxy<unknown, RawBindings, D, C, M>;
declare function defineComponent<PropNames extends string, RawBindings = Data, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, PropsOptions extends ComponentPropsOptions = ComponentPropsOptions>(options: ComponentOptionsWithArrayProps<PropNames, RawBindings, D, C, M>): VueProxy<Readonly<{
[key in PropNames]?: any;
}>, RawBindings, D, C, M>;
declare function defineComponent<Props, RawBindings = Data, D = Data, C extends ComputedOptions = {}, M extends MethodOptions = {}, PropsOptions extends ComponentPropsOptions = ComponentPropsOptions>(options: HasDefined<Props> extends true ? ComponentOptionsWithProps<PropsOptions, RawBindings, D, C, M, Props> : ComponentOptionsWithProps<PropsOptions, RawBindings, D, C, M>): VueProxy<PropsOptions, RawBindings, D, C, M>;
Yes! We found it! The problem is that definedComponent
only support default Vue ComponentOptions
types. So, How can we solve this problem?
vue-shims.d.ts
First, create a file vue-shim.d.ts
in the @types
folder at project root. (If you have seen this documentation, vue-shim.d.ts
will already exist.)
import Vue from 'vue'
import { Context, Middleware } from '@nuxt/types'
...
declare module 'vue/types/options' {
interface ComponentOptions<V extends Vue> {
fetch?(ctx: Context): Promise<void> | void
layout?: string | ((ctx: Context) => string)
middleware?: Middleware | Middleware[]
}
}
And, Like the above code, declare ComponentOptions
interface as extends Vue in the 'vue/types/options'
module.
Internally, this declaration has the following meaning::
- vue-shim.d.ts extends a default ComponentOptions of Vue
- definedComponent extends the new ComponentOptions interface declared in step1
- We can use newly added types in definedComponent.
Good! Now we can use the types of Nuxt.js ComponentOptions!
$vuetify
Vuetify is a Material Design component framework for Vue.js
Vuetify has similar issues with types like ComponentOptions in Nuxt and Composition environment. That is, we can not access the type of this.$vuetify
in the definedComponent
.
Maybe, If you use Vueitfy in Nuxt.js, you will be using the @nuxtjs/vuetify
@nuxtjs/vuetify
provides type of $vuetify
in Nuxt Context as follows:
// node_modules/@nuxtjs/vuetify/dist/index.d.ts
import { Module } from '@nuxt/types';
import { Framework } from 'vuetify';
import { Options, TreeShakeOptions, VuetifyLoaderOptions } from './options';
declare module '@nuxt/types' {
interface Configuration {
vuetify?: Options;
}
interface Context {
$vuetify: Framework;
}
}
declare const vuetifyModule: Module<Options>;
export { Options, TreeShakeOptions, VuetifyLoaderOptions };
export default vuetifyModule;
This problem can also be solved by declaring a new type like the above problem.
// vue-shim.d.ts
import { Framework } from 'vuetify'
...
declare module 'vue/types/vue' {
interface Vue {
$vuetify: Framework
}
}
Now the $vuetify
type is also available like this!
<script lang="ts">
import { defineComponent } from '@vue/composition-api'
export default defineComponent({
setup(_, context) {
...
const { width } = context.root.$vuetify.breakpoint
...
}
})
</script>
Conclusion
Using the Nuxt.js
and Vue Composition API
together can be a very good option. However, the Composition API is not yet fully supported for Nuxt (especially TypeScript).
Of course, the content of this article is not all, but I hope it helps people who want to use Nuxt and Composition API in a Typescript environment.
If you are more interested in this topic, check out nuxt-community/composition-api project!
Thank you!
Top comments (1)
For updates regarding nuxt composition api, you now have the other properties available on defineComponent() like layout, middleware, etc
Also for accessing $vuetify, just use the useContext() method and you will have access to $vuetify from it
const context = useContext()
const breakpoint = ({
breakpoint: computed(() => context.$vuetify.breakpoint)
})
It worked for me