24 KiB
title | head.title | description | navigation.icon |
---|---|---|---|
components | components/ | The components/ directory is where you put all your Vue components. | i-ph-folder |
Nuxt automatically imports any components in this directory (along with components that are registered by any modules you may be using).
-| components/
---| AppHeader.vue
---| AppFooter.vue
<template>
<div>
<AppHeader />
<NuxtPage />
<AppFooter />
</div>
</template>
Component Names
If you have a component in nested directories such as:
-| components/
---| base/
-----| foo/
-------| Button.vue
... then the component's name will be based on its own path directory and filename, with duplicate segments being removed. Therefore, the component's name will be:
<BaseFooButton />
::note
For clarity, we recommend that the component's filename matches its name. So, in the example above, you could rename Button.vue
to be BaseFooButton.vue
.
::
If you want to auto-import components based only on its name, not path, then you need to set pathPrefix
option to false
using extended form of the configuration object:
export default defineNuxtConfig({
components: [
{
path: '~/components',
pathPrefix: false, // [!code ++]
},
],
});
This registers the components using the same strategy as used in Nuxt 2. For example, ~/components/Some/MyComponent.vue
will be usable as <MyComponent>
and not <SomeMyComponent>
.
Dynamic Components
If you want to use the Vue <component :is="someComputedComponent">
{lang=vue} syntax, you need to use the resolveComponent
helper provided by Vue or import the component directly from #components
and pass it into is
prop.
For example:
<script setup lang="ts">
import { SomeComponent } from '#components'
const MyButton = resolveComponent('MyButton')
</script>
<template>
<component :is="clickable ? MyButton : 'div'" />
<component :is="SomeComponent" />
</template>
::important
If you are using resolveComponent
to handle dynamic components, make sure not to insert anything but the name of the component, which must be a string and not a variable.
::
::tip{icon="i-ph-video" to="https://www.youtube.com/watch?v=4kq8E5IUM2U" target="_blank"}
Watch Daniel Roe's short video about resolveComponent
.
::
Alternatively, though not recommended, you can register all your components globally, which will create async chunks for all your components and make them available throughout your application.
export default defineNuxtConfig({
components: {
+ global: true,
+ dirs: ['~/components']
},
})
You can also selectively register some components globally by placing them in a ~/components/global
directory, or by using a .global.vue
suffix in the filename. As noted above, each global component is rendered in a separate chunk, so be careful not to overuse this feature.
::note
The global
option can also be set per component directory.
::
Dynamic Imports
To dynamically import a component (also known as lazy-loading a component) all you need to do is add the Lazy
prefix to the component's name. This is particularly useful if the component is not always needed.
By using the Lazy
prefix you can delay loading the component code until the right moment, which can be helpful for optimizing your JavaScript bundle size.
<script setup lang="ts">
const show = ref(false)
</script>
<template>
<div>
<h1>Mountains</h1>
<LazyMountainsList v-if="show" />
<button v-if="!show" @click="show = true">Show List</button>
</div>
</template>
Delayed Hydration
Lazy components are great for controlling the chunk sizes in your app, but they don't enhance runtime performance, as they still load eagerly unless conditionally rendered. In real world applications, some pages may include a lot of content and a lot of components, and most of the time not all of them need to be interactive as soon as the page is loaded. Having them all load eagerly can negatively impact performance and increase bundle size.
In order to optimize the page, you may want to delay the hydration of some components until they're visible, or until the browser is done with more important tasks for example. Delaying the hydration of components will ensure it is only loaded when necessary, which is great for making a good user experience and a performant app. Nuxt has first class support for delayed hydration to help with that, without requiring you to write all the related boilerplate.
In order to use delayed hydration, you first need to enable it in your experimental config in nuxt.config
export default defineNuxtConfig({
experimental: {
delayedHydration: true
}
})
Nuxt has reserved component prefixes that will handle this delayed hydration for you, that extend dynamic imports. By prefixing your component with LazyVisible
, Nuxt will automatically handle your component and delay its hydration until it will be on screen.
<template>
<div>
<LazyVisibleMyComponent />
</div>
</template>
If you need the component to load as soon as possible, but not block the critical rendering path, you can use the LazyIdle
prefix, which would handle your component's hydration whenever the browser goes idle.
<template>
<div>
<LazyIdleMyComponent />
</div>
</template>
If you would like the component to load after certain events occur, like a click or a mouse over, you can use the LazyEvent
prefix, which would only trigger the hydration when those events occur.
<template>
<div>
<LazyEventMyComponent />
</div>
</template>
If you would like to load the component when the window matches a media query, you can use the LazyMedia
prefix:
<template>
<div>
<LazyMediaMyComponent />
</div>
</template>
If you would like to never hydrate a component, use the LazyNever
prefix:
<template>
<div>
<LazyNeverMyComponent />
</div>
</template>
If you would like to hydrate a component after a certain amount of time, use the LazyTime
prefix:
<template>
<div>
<LazyTimeMyComponent />
</div>
</template>
If you would like to hydrate a component once a promise is fulfilled, use the LazyPromise
prefix:
<template>
<div>
<LazyPromiseMyComponent />
</div>
</template>
Nuxt's delayed hydration system is highly flexible, allowing each developer to build upon it and implement their own hydration strategy.
If you have highly specific hydration triggers that aren't covered by the default strategies, or you want to have conditional hydration, you can use the general purpose LazyIf
prefix:
<template>
<div>
<button @click="myFunction">Click me to start the custom hydration strategy</button>
<LazyIfMyComponent :hydrate="myCondition" />
</div>
</template>
<script setup lang="ts">
const myCondition = ref(false)
function myFunction() {
// trigger custom hydration strategy...
myCondition.value = true
}
</script>
Custom hydration triggers
If you would like to override the default hydration triggers when dealing with delayed hydration, like changing the timeout, the options for the intersection observer, or the events to trigger the hydration, you can do so by supplying a hydrate
prop to your lazy components.
<template>
<div>
<LazyIdleMyComponent :hydrate="3000" />
<LazyVisibleMyComponent :hydrate="{threshold: 0.2}" />
<LazyEventMyComponent :hydrate="['click','mouseover']" />
<LazyMediaMyComponent hydrate="(max-width: 500px)" />
<LazyIfMyComponent :hydrate="someCondition" />
<LazyTimeMyComponent :hydrate="3000" />
<LazyPromiseMyComponent :hydrate="promise" />
</div>
</template>
<script setup lang="ts">
const someCondition = ref(true)
const promise = Promise.resolve(42)
</script>
::read-more{to="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia"}
Read more about using LazyMedia
components and the accepted values.
::
::important If your components begin with a reserved delayed hydration prefix like Visible/Idle/Event, they will not have delayed hydration by default. This is made to ensure you have full control over all your components and prevent breaking dynamic imports for those components.
This also means you would need to explicitly add the prefix to those components for if you'd like for them to have delayed hydration.
For example, if you have a component named IdleBar
and you'd like it to be delayed based on network idle time, you would need to use it like <LazyIdleIdleBar>
and not <LazyIdleBar>
to make it a delayed hydration component. Otherwise, it would be treated as a regular dynamic import
::
Listening to hydration events
All delayed hydration components have a @hydrated
event that is fired whenever they are hydrated. You can listen to this event to trigger some action that depends on the component:
<template>
<div>
<LazyVisibleMyComponent @hydrated="onHydrate" />
</div>
</template>
<script setup lang="ts">
function onHydrate() {
console.log("Component has been hydrated!")
}
</script>
Caveats and best practices
Delayed hydration has many performance benefits, but in order to gain the most out of it, it's important to use it correctly:
-
Avoid delayed hydration components as much as possible for in-viewport content - delayed hydration is best for content that is not immediately available and requires some interaction to get to. If it is present on screen and is meant to be available for use immediately, using it as a normal component would provide better performance and loading times. Use this feature sparingly to avoid hurting the user experience, as there are only a few cases that warrant delayed hydration for on-screen content.
-
Delayed hydration with conditional rendering - when using
v-if
with delayed hydration components, note thatv-if
takes precedence. That means, the component will be hydrated when thev-if
is truthy, as that will render exclusively on the client. If you need to render the component only when the condition is true, use a regular async component (<LazyMyComponent />
) with av-if
. If you need it to hydrate when the condition is fulfilled, use a delayed hydration prefix with thehydrate
prop. -
Delayed hydration with a shared state - when using multiple components (for example, in a
v-for
) with the samev-model
, where some components might get hydrated before others (for example, progressively increasing media queries), if one of the components updates the model, note that it will trigger hydration for all components with that same model. That is because Vue's reactivity system triggers an update for all the parts of the app that depend on that state, forcing hydration in the process. Props are unaffected by this. Try to avoid multiple components with the same model if that is not an intended side effect. -
Use each hydration strategy for its intended use case - each hydration strategy has built-in optimizations specifically designed for that strategy's purpose. Using them incorrectly could hurt performance and user experience. Examples include:
-
Using
LazyIf
for always/never hydrated components (:hydrate="true"
/:hydrate="false"
) - you can use a regular component/LazyNever
respectively, which would provide better performance for each use case. KeepLazyIf
for components that could get hydrated, but might not get hydrated immediately. -
Using
LazyTime
as an alternative toLazyIdle
- while these strategies share similarities, they are meant for different purposes.LazyTime
is specifically designed to hydrate a component immediately after a certain amount of time has passed.LazyIdle
, on the other hand, is meant to provide a limit for the browser to handle the hydration whenever it's idle. If you useLazyTime
for idle-based hydration, the browser might handle the component's hydration while handling other, potentially more important components at the same time. This could slow down the hydration for all components being handled. -
ADVANCED
For example, in stead of handling promises manually and setting a boolean indicator for when the promise was fulfilled, which would then get passed to LazyIf
, using LazyPromise
directly would handle it without requiring another ref, reducing the complexity and the amount of work Vue's reactivity system would need to handle and track.
Always remember that while LazyIf
allows for implementation of custom, highly-tailored hydration strategies, it should mainly be used when either pure conditional hydration is required (for example, hydration of a component when a separate button is clicked), or when no built-in strategy matches your specific use case, due to the internal optimizations each existing hydration strategy has.
Direct Imports
You can also explicitly import components from #components
if you want or need to bypass Nuxt's auto-importing functionality.
<script setup lang="ts">
import { NuxtLink, LazyMountainsList } from '#components'
const show = ref(false)
</script>
<template>
<div>
<h1>Mountains</h1>
<LazyMountainsList v-if="show" />
<button v-if="!show" @click="show = true">Show List</button>
<NuxtLink to="/">Home</NuxtLink>
</div>
</template>
Custom Directories
By default, only the ~/components
directory is scanned. If you want to add other directories, or change how the components are scanned within a subfolder of this directory, you can add additional directories to the configuration:
export default defineNuxtConfig({
components: [
// ~/calendar-module/components/event/Update.vue => <EventUpdate />
{ path: '~/calendar-module/components' },
// ~/user-module/components/account/UserDeleteDialog.vue => <UserDeleteDialog />
{ path: '~/user-module/components', pathPrefix: false },
// ~/components/special-components/Btn.vue => <SpecialBtn />
{ path: '~/components/special-components', prefix: 'Special' },
// It's important that this comes last if you have overrides you wish to apply
// to sub-directories of `~/components`.
//
// ~/components/Btn.vue => <Btn />
// ~/components/base/Btn.vue => <BaseBtn />
'~/components'
]
})
::note Any nested directories need to be added first as they are scanned in order. ::
npm Packages
If you want to auto-import components from an npm package, you can use addComponent
in a local module to register them.
::code-group
import { addComponent, defineNuxtModule } from '@nuxt/kit'
export default defineNuxtModule({
setup() {
// import { MyComponent as MyAutoImportedComponent } from 'my-npm-package'
addComponent({
name: 'MyAutoImportedComponent',
export: 'MyComponent',
filePath: 'my-npm-package',
})
},
})
<template>
<div>
<!-- the component uses the name we specified and is auto-imported -->
<MyAutoImportedComponent />
</div>
</template>
::
Component Extensions
By default, any file with an extension specified in the extensions key of nuxt.config.ts
is treated as a component.
If you need to restrict the file extensions that should be registered as components, you can use the extended form of the components directory declaration and its extensions
key:
export default defineNuxtConfig({
components: [
{
path: '~/components',
extensions: ['.vue'], // [!code ++]
}
]
})
Client Components
If a component is meant to be rendered only client-side, you can add the .client
suffix to your component.
| components/
--| Comments.client.vue
<template>
<div>
<!-- this component will only be rendered on client side -->
<Comments />
</div>
</template>
::note
This feature only works with Nuxt auto-imports and #components
imports. Explicitly importing these components from their real paths does not convert them into client-only components.
::
::important
.client
components are rendered only after being mounted. To access the rendered template using onMounted()
, add await nextTick()
in the callback of the onMounted()
hook.
::
::read-more{to="/docs/api/components/client-only"}
You can also achieve a similar result with the <ClientOnly>
component.
::
Server Components
Server components allow server-rendering individual components within your client-side apps. It's possible to use server components within Nuxt, even if you are generating a static site. That makes it possible to build complex sites that mix dynamic components, server-rendered HTML and even static chunks of markup.
Server components can either be used on their own or paired with a client component.
::tip{icon="i-ph-video" to="https://www.youtube.com/watch?v=u1yyXe86xJM" target="_blank"} Watch Learn Vue video about Nuxt Server Components. ::
::tip{icon="i-ph-article" to="https://roe.dev/blog/nuxt-server-components" target="_blank"} Read Daniel Roe's guide to Nuxt Server Components. ::
Standalone server components
Standalone server components will always be rendered on the server, also known as Islands components.
When their props update, this will result in a network request that will update the rendered HTML in-place.
Server components are currently experimental and in order to use them, you need to enable the 'component islands' feature in your nuxt.config:
export default defineNuxtConfig({
experimental: {
componentIslands: true
}
})
Now you can register server-only components with the .server
suffix and use them anywhere in your application automatically.
-| components/
---| HighlightedMarkdown.server.vue
<template>
<div>
<!--
this will automatically be rendered on the server, meaning your markdown parsing + highlighting
libraries are not included in your client bundle.
-->
<HighlightedMarkdown markdown="# Headline" />
</div>
</template>
Server-only components use <NuxtIsland>
under the hood, meaning that lazy
prop and #fallback
slot are both passed down to it.
::alert{type=warning} Server components (and islands) must have a single root element. (HTML comments are considered elements as well.) ::
::alert{type=warning} Be careful when nesting islands within other islands as each island adds some extra overhead. ::
::alert{type=warning} Most features for server-only components and island components, such as slots and client components, are only available for single file components. ::
Client components within server components
::alert{type=info}
This feature needs experimental.componentIslands.selectiveClient
within your configuration to be true.
::
You can partially hydrate a component by setting a nuxt-client
attribute on the component you wish to be loaded client-side.
<template>
<div>
<HighlightedMarkdown markdown="# Headline" />
<!-- Counter will be loaded and hydrated client-side -->
<Counter nuxt-client :count="5" />
</div>
</template>
::alert{type=info}
This only works within a server component. Slots for client components are working only with experimental.componentIsland.selectiveClient
set to 'deep'
and since they are rendered server-side, they are not interactive once client-side.
::
Server Component Context
When rendering a server-only or island component, <NuxtIsland>
makes a fetch request which comes back with a NuxtIslandResponse
. (This is an internal request if rendered on the server, or a request that you can see in the network tab if it's rendering on client-side navigation.)
This means:
- A new Vue app will be created server-side to create the
NuxtIslandResponse
. - A new 'island context' will be created while rendering the component.
- You can't access the 'island context' from the rest of your app and you can't access the context of the rest of your app from the island component. In other words, the server component or island is isolated from the rest of your app.
- Your plugins will run again when rendering the island, unless they have
env: { islands: false }
set (which you can do in an object-syntax plugin).
Within an island component, you can access its island context through nuxtApp.ssrContext.islandContext
. Note that while island components are still marked as experimental, the format of this context may change.
::note
Slots can be interactive and are wrapped within a <div>
with display: contents;
::
Paired with a Client component
In this case, the .server
+ .client
components are two 'halves' of a component and can be used in advanced use cases for separate implementations of a component on server and client side.
-| components/
---| Comments.client.vue
---| Comments.server.vue
<template>
<div>
<!-- this component will render Comments.server on the server then Comments.client once mounted in the browser -->
<Comments />
</div>
</template>
Built-In Nuxt Components
There are a number of components that Nuxt provides, including <ClientOnly>
and <DevOnly>
. You can read more about them in the API documentation.
::read-more{to="/docs/api"} ::
Library Authors
Making Vue component libraries with automatic tree-shaking and component registration is super easy. ✨
You can use the components:dirs
hook to extend the directory list without requiring user configuration in your Nuxt module.
Imagine a directory structure like this:
-| node_modules/
---| awesome-ui/
-----| components/
-------| Alert.vue
-------| Button.vue
-----| nuxt.js
-| pages/
---| index.vue
-| nuxt.config.js
Then in awesome-ui/nuxt.js
you can use the components:dirs
hook:
import { defineNuxtModule, createResolver } from '@nuxt/kit'
export default defineNuxtModule({
hooks: {
'components:dirs': (dirs) => {
const { resolve } = createResolver(import.meta.url)
// Add ./components dir to the list
dirs.push({
path: resolve('./components'),
prefix: 'awesome'
})
}
}
})
That's it! Now in your project, you can import your UI library as a Nuxt module in your nuxt.config
file:
export default defineNuxtConfig({
modules: ['awesome-ui/nuxt']
})
... and directly use the module components (prefixed with awesome-
) in our pages/index.vue
:
<template>
<div>
My <AwesomeButton>UI button</AwesomeButton>!
<awesome-alert>Here's an alert!</awesome-alert>
</div>
</template>
It will automatically import the components only if used and also support HMR when updating your components in node_modules/awesome-ui/components/
.
:link-example{to="/docs/examples/features/auto-imports"}