Getting a grip on Nuxt's auto-import functionality
Understanding when to use and when to avoid the auto-import magic
Intro
One of Nuxt 3’s stand-out features is its auto-import (opens new window) functionality which promises to reduce developer friction by removing the burden of managing imports, and even having to worry about the source of any particular dependency!
But perhaps you – like me – find the auto-import experience doesn’t quite deliver, with supposed time savings eclipsed by new problems such as poor IDE integration, difficulty locating files, or understanding your application structure.
I wanted to get to the bottom of this paradox so after my original complaint in my Nuxt Layers article (opens new window), I decided to spend some time getting to know auto-import; when it was useful, when less so, and what workarounds there might be.
I’ve tried to keep the article as short as I can, but there’s quite a lot to cover!
Overview
So let’s reacquaint ourselves with how auto-import works, or if you were fuzzy on the specifics, shine a light on them.
Background
Nuxt leverages unjs/unimport (opens new window) (which in turn leverages unjs/unplugin (opens new window)) to supply the auto-import magic.
It’s a build process which scans configured folders, hoists discovered dependencies into the global scope, injects relevant import statements into the build, and connects your IDE via TypeScript declarations (opens new window) (check your .nuxt/components.d.ts
and .nuxt/imports.d.ts
).
For a little more detail, here’s how Chat GPT describes it (opens new window).
Implementation
Whilst “auto-import” is a catch-all term which covers both components and code, there are actually significant differences and subtle ambiguity across their naming, documentation, behaviour, and defaults:
Behaviour | Components | Code |
---|---|---|
Documentation | Components (opens new window) | Composables (opens new window) |
Default folders | ~/components | ~/composables , ~/utils , ~/server/utils |
Direct import | #components (opens new window) | #imports (opens new window) |
Configuration option | components (opens new window) | imports (opens new window) |
Heavy-lifting done by | Framework code (opens new window) | unjs/unimport (opens new window) |
Folder scanning defaults | Nested | Top-level |
Path format | Abs path, rel path, aliases | Abs path, rel path, aliases, globs |
Notes | Auto-prefixes nested folders |
Did you spot the potential footgun?
It’s that Nuxt 3 – by default – will auto-prefix nested components.
It’s important to understand the ramifications of this, as it directly affects the nature of your codebase, including:
- component organisation
- component usage
- IDE integration
- refactoring
Component specifics
So how does auto-prefixing play a part with auto-importing components?
Given that auto-imports are global by definition, and your project could have potentially many 100s of components, Nuxt looks to sidestep global namespace collisions by prefixing component auto-imports with their folder path.
As such, Nuxt’s out-of-the-box component “auto-importing” is also component auto-renaming:
Folder | Component name | Auto-import name |
---|---|---|
components | Dropdown.vue | Dropdown.vue |
components/form | Dropdown.vue | FormDropdown.vue |
components/form/options | Dropdown.vue | FormOptionsDropdown.vue |
components/form/options | DropdownItem.vue | FormOptionsDropdownItem.vue |
Unfortunately, the docs mainly skip over this fundamental choice:
- component prefixing is not mentioned in the Key Concepts / Auto-imports (opens new window) part of the guide
- on the Components page, manual renaming is suggested only “for clarity (opens new window)”
- the docs search for “auto-import” does not reference the main Components (opens new window) page
- the behaviour is different to, and not mentioned directly in, the Nuxt 3 Migration / Auto Imports (opens new window) guide
The upshot is:
- if you didn’t know about the new defaults, you may already have found auto-imports mysteriously “broken”
- even if you did, you may not be aware of the indirect ramifications path-prefixing may impose upon you
Configuration
Overview
Of course, like most things in Nuxt, the defaults can be reconfigured.
On first glance it seems that components
and imports
are configured quite similarly:
export default defineNuxtConfig({
[option]: {
...
dirs: [
'path/to/dir',
]
},
})
However, it’s good to know there are some subtle variations between them:
components
has a top-levelobject
configuration as well as anarray
shorthandcomponents.dirs
can be more specifically-configured usingobjects
imports.dirs
supports only paths or globs (opens new window)
The config documentation (opens new window) is somewhat scattered and incomplete, so it doesn’t hurt to check the actual source code:
Note, that whilst only some of the config options are documented (and I can’t tell you the reason for this) reading through the doc comments in these files provides additional insight into the decisions the framework makes to create your application from the raw source code you write.
Components
If you decide to configure (or, reconfigure) component
auto-prefixing, your primary options (opens new window) are:
// src/nuxt.config.ts
export default defineNuxtConfig({
components: [
// use defaults: use path prefix
'~/core/components',
// override defaults: no path prefix
{ path: '~/layers/site/components', pathPrefix: false },
// override defaults: no path prefix, register all globally (for Nuxt Content)
{ path: '~/layers/blog/components', pathPrefix: false, global: true },
]
})
Note that I’m using the array shorthand above, because I’m not supplying any root options (opens new window).
You can even completely disable component auto-importing per project, and per layer:
// src/nuxt.config.ts or src/layer/nuxt.config.ts
export default defineNuxtConfig({
components: []
})
Imports
For imports
– for which the defaults are generally fine – the options (opens new window) are much simpler:
// src/nuxt.config.ts
export default defineNuxtConfig({
imports: {
// optionally disable
autoImport: false,
// load all composables at all depths
dirs: [
'~/**/composables/**',
]
}
})
Demo
If you want to see the impact of the above, check this sample repo:
Navigate to the nuxt.config.ts
file where you can quickly re-configure the components
option:
export default defineNuxtConfig({
components: config.arr.default // <-- change to something like config.obj.noPrefix
})
You’ll be able to compare the array
, object
and pathPrefix
settings, and see how they combine to import – or in some cases not import – the components you might expect.
Project size considerations
Approach
Now you’re up to speed on configuration, let’s cover opting in, opting out, or sitting somewhere between.
On anything other than a quick demo, you may want to either:
- settle on a folder strategy, such as 2-levels deep, singular-names (i.e.
components/dropdown/DropdownItem.vue
), or - turn off path-prefixing in favour of explicitly-named components, or
- turn off auto-importing in favour of explicit imports, or
- a mixture of the last two approaches
The motivation for each may depend on:
- how large the project is
- how well the team knows the codebase
- how comfortable the developers are with magic
- your style of development
- your IDE choice
Small to medium projects
In a small or medium projects, it’s reasonably simple to keep track of component auto-importing:
+- src
+- components
+- account <-- nested folders; prefixed component names
| +- AccountSettings.vue
| +- AccountLogin.vue
+- article
| +- ArticleList.vue
| +- ArticleItem.vue
+- dropdown
| +- Dropdown.vue
| +- DropdownOption.vue
+- site
| +- Footer.vue
| +- Header.vue
|
+- Button.vue <-- root level; no prefixing
+- Dropdown.vue
However, note:
- the mix of top-level contexts, i.e. core (
dropdown
), global (site
) and domain (account
,article
) concerns - the mix of physical prefixes and auto-prefixes
site/Footer
,Dropdown
(which some IDEs may fail to reference) - that top-level imports are not renamed, but nested components are
If your project is small and everything is reasonably accessible in the Project Explorer, maybe that’s fine.
But, two questions regarding organisation:
- are you sure your small project won’t eventually become a large project?
- as your project grows, are you happy with enforced path-prefixing (opens new window) constraints?
If your project does expand and you need to organise further, expect longer, concatenated naming such as:
<FormDropdown>
<FormDropdownOption />
</FormDropdown>
Or perhaps:
<AccountSettingsDropdownOption />
You get the idea.
Large projects
So let’s take a large project, such as Elk (opens new window), a Mastodon client written by some of the core Nuxt team, and a great showcase for what Nuxt can do.
The app has about 50 routes (opens new window), with 24 top-level components (opens new window) folders and around 180 component files. They’ve stuck mainly to a 2-level structure with occasional strategic nesting:
+- src
+- components
+- account
+- aria
+- ...
+- common
+- ...
+- status
| +- edit
| | +- StatusEditHistory.vue
| | +- StatusEditHistorySkeleton.vue
| | +- ...
| +- ...
+- ...
You could argue at least this is reasonably organised and mainly consistent, but let’s say I’m a new team member, and I’m trying to understand how StatusEditHistorySkeleton
(opens new window) fits into the overall site.
The actual import hierarchy is:
+- components/status/edit/StatusEditHistorySkeleton.vue
+- components/status/edit/StatusEditHistory.vue
+- components/status/edit/StatusEditIndicator.vue
+- components/status/StatusDetails.vue
+- pages/[[server]]/@[account]/[status].vue
But with auto-imports there are no direct links between the files, so your options are:
- hope your IDE will determine the real component source or find usages
- which may only work if the component filename matches the full auto-import
- plus, this can take significant time if an auto-import or usages have not already been found and cached
- go via the
.nuxt/components.d.ts
file- which will have ~4x the number of entries as components, plus globals
- perform a manual search using the component’s full name
Note that I’m not taking sides here, I’m merely highlighting:
- the ratio of ease of importing to ease of locating diminishes as the project grows larger
- you should understand the mechanisms in case you later decide to refactor
Very large projects
I’m currently working with Forgd (opens new window) on their web app, which has about 75 routes, 34 component folders (not much organisation yet), ~280 components, and additional .story.vue
files.
We recently refactored to layers (making it much easier to locate files) and are currently reviewing auto-imports.
Here’s an example of both core and domain files in our folder structure:
+- core
| +- components
| | +- chart
| | +- ui
| | | +- UiButton.vue
| | +- ...
| +- ...
+- layers
+- ...
+- token-designer
⋮ +- components
| +- td
| ⋮ +- adjust
| ⋮ +- tab
| ⋮ +- TdAdjustTabPriceAndMarketCapPerformance.vue
+- pages
⋮ +- token-designer
⋮ +- adjust
⋮ +- simulating-post-tge-pops.vue
Things to note about the above:
- a
core
layer contains all global concerns - we rely on prefixes (such as
Td
) to keep naming sane - we use aliases (such as
#td
) to target layers
What’s interesting regarding the deep nesting above, is that any of the following are valid auto-import locations if path-prefixing is turned on – but IDE tooling may only would likely only locate 4 of these files:
components --> TdAdjustTabPriceAndMarketCapPerformance
components/td --> TdAdjustTabPriceAndMarketCapPerformance
AdjustTabPriceAndMarketCapPerformance
components/td/adjust --> TdAdjustTabPriceAndMarketCapPerformance
AdjustTabPriceAndMarketCapPerformance
TabPriceAndMarketCapPerformance
components/td/adjust/tab --> TdAdjustTabPriceAndMarketCapPerformance
AdjustTabPriceAndMarketCapPerformance
TabPriceAndMarketCapPerformance
PriceAndMarketCapPerformance
Note also, without short prefixes, we could be typing component names like the following!
td: <TokenDesignerAdjustTabPriceAndMarketCapPerformance />
ad: <AutoDistributionConfiguratorStrategyDetails />
amm: <AutomatedMarketMakingStrategyCompleteCta />
We are currently considering:
- auto-importing only core-level components, and without auto-prefixing, i.e.
core/forms/UiButton
- moving away from auto-importing domain-level components, i.e.
PriceAndMarketCapPerformance
- moving towards local index files to export related sets of components
If that was the case, domain-level imports / usage may look like this:
<script >
import {
PriceAndMarketCapPerformance,
...
} from '#td/components/adjust'
</script>
<template>
<PriceAndMarketCapPerformance />
<UiButton />
...
</template>
It’s a very small amount of extra code, but:
- the major IDEs add imports automatically
- a
Cmd-Click
is guaranteed to take you directly to the component - VSCode can be coerced into supporting
.vue
file refactoring
IDE integration
Preparation
Note that before you can even begin to see the benefits of type completion you will need to either build the project or run nuxi prepare
to generate the stub files in Nuxt’s build folder .nuxt/
.
If you don’t do that, code-completion is not going to work at all!
Overview
So how do the two main IDEs, VSCode and WebStorm work with these magically auto-imported files?
Well, not all IDEs are not created equal, and frameworks authors cater differently to different IDEs.
In the case of auto-imports, both VsCode and WebStorm:
- can instantly resolve explicit imports
- can guess at implicit imports, or go via
.nuxt/components.d.ts
(can take time to search the codebase)
Also:
- WebStorm has the edge on refactoring, but VS code can be coerced into playing nice
- Volar under WebStorm is fairly hit-and-miss; sometimes it works, sometimes it doesn’t
Support
Navigation
Navigate to source from usage in .vue
file:
Cmd-Click
component tag- WebStorm – navigates to
.nuxt/components.ts
, then you need to manually click the<Component>.vue
filename- strangely, PHPStorm navigates direct to the component!
- VSCode – navigates to the component, via
.nuxt/components.ts
- WebStorm – navigates to
Right-Click
- WebStorm – Go To > Declaration or Usages
- VSCode – Go to Definition
Find all usages from usage in .vue
file:
- WebStorm –
Right-Click
> Find Usages; shows in popup - VSCode –
Right-Click
> Go to References; shows inline (opens new window)
Find usages from Project Explorer:
- WebStorm –
Right-Click
> Find Usages; shows “Nothing found in ‘All Places’” - VSCode –
Right-Click
> Find File References; shows.nuxt/components.d.ts
Refactoring
Implicit import refactoring:
- TBC
Explicit import refactoring:
- WebStorm – updates any combination of
.vue
or.ts
renames or moves - VSCode doesn’t update
.vue
to.vue
renames or moves
To work around VSCode’s limitations, you can re-export .vue
components from an index.ts
file, and it will update .vue
imports if the index.ts
file or containing folders are renamed or moved:
// components/somewhere/index.ts
import SomeComponent from './SomeComponent'
...
export {
SomeComponent,
...
}
<script>
import { SomeComponent } from '~/components/somewhere'
</script>
Summary
Trade-offs
Nuxt’s auto-import defaults bring with them some subtle tradeoffs – which can be magnified as the project grows:
Situation | Issues |
---|---|
Root vs nested folders | Inconsistencies between root and nested components |
Missed the documentation on naming | Only same-named components auto-import |
Small number of components | Easy to find |
Large number of components | More nesting required / harder to find |
Low nesting | Simple names |
Deep nesting | Highly-concatenated names |
Long names | Hard to locate / multiple possible locations |
Auto-importing | No direct link in IDE / will need to search |
Not-named the same as the path prefix | IDE may not find file / or may take a long time to find file |
Moving existing prefixed components | Will break your app if you don’t understand prefixing |
Want to refactor component path | Will need to manually search and rename component names |
How much typing is saved | Adding shorter imports once vs typing longer component names often |
Leveraging import context | Limited local imports vs filtering all globally available imports |
Using layers | Local imports may be in some cases be simpler than unwieldy auto-imports |
Want to leverage IDE tooling | Lack of explicit imports may break tools around usage |
And the larger your application gets, the less magic you want and the more safety you need, so it’s important to understand the pros and cons, so you can make the right choices for your project and team.
Thoughts
For small or medium projects, auto-imports are fine. But you should consider what might happen if your project grows.
I generally prefer to turn off path-prefixing, as then you’re free to decide on your own prefixing strategy, and it makes it easier to refactor entire subtrees of code should you decide to.
For larger projects I feel that global concerns (opens new window) (such as UI components, or site furniture) and some 3rd-party code absolutely benefit from being auto-imported, but it’s clearer if domain-level concerns are imported explicitly.
Not only are the relationships between the domain entities clearer, but IDE and tooling support is guaranteed (you can use index
files to simplify imports and improve VSCode refactoring), and it’s significantly easier to get to grips with a new project – or an old project you haven’t looked at in a while!
And finally, some additional opinions from Redditors (opens new window):
- Getting a grip on Nuxt’s auto-import functionality (opens new window)
- What do you think about auto-imports? (opens new window)
Last words
Maybe you’re happy with auto-imports and don’t feel the need to change. Or maybe auto-imports never quite worked for you, but at least you now understand them better. Or maybe it’s somewhere between – which is ironically where you might end up if your project gets large enough, and you push the boundaries of auto-importing.