In the previous part, we learned how to work with components and pages. If a certain part of a template is repeated, it's a prime candidate for extraction into a separate new component, in accordance with the programmer's DRY principle. But what if the methods for working with data in the <script> section are repeated? Or what if we want to organize our application better and avoid scary long component files that become difficult to navigate?
We certainly can extract the logic from JavaScript (TypeScript) code as well. Nuxt provides automatic imports across the application if we place the relevant files in two additional special folders - /app/utils and /app/composables.
Note: In Nuxt v4, these folders are now located inside the /app directory, whereas previously you would find them directly in the project root directory.
Export and Import in JavaScript
A JavaScript function or constant can be made visible to other files through the export keyword. Generally speaking, the definition must then be "imported" on the other side - import { foo } from ./foo.js. You can learn more about modules (including nuances with "default" exports) HERE, for example. This "ESM" system is the de-facto standard today, although you may still encounter the older approach (CommonJS) in some places. We will stick to ESM.
This works well in Nuxt too, but in many cases Nuxt can go further and shield us from the requirement of writing explicit imports. Files in the /app/utils and /app/composables folders are automatically scanned and linked in the background so that elements exported from there are accessible everywhere without having to do anything else. This mechanism is called auto-import. Truth be told, the /app/components folder we talked about last time also works on the same principle (additionally registering the component within the Vue.js application). Furthermore, all Vue.js and Nuxt API methods and components are automatically accessible thanks to this.
Of course, you're not required to store your own files only in the defined folders - you can place them anywhere, but explicit imports are needed from other directories. Conversely, some people prefer to see exactly what they're using from elsewhere in their code. For such cases, it's possible and not wrong to use imports the way you're used to - from a relative file path or from a depending package. However, it's even better to use aliases - #imports for auto-imported functions and #components for components. For example:
// computed comes from Vue, myFunction from a custom file in /app/utils
import { computed, myFunction } from '#imports'
I like not having to care about them. It should be my Java origins, where imports of classes often take more place than you would like. Therefore, let's show how the auto-imports work in action.
/app/utils
The /app/utils folder is recommended for placing so-called stateless logic - that is, helper functions that work only with declared inputs and whose output does not depend in any way on the current state of the application.
During build, Nuxt goes through all files in the folder and makes anything prefixed with the export keyword available throughout the rest of the application. Everywhere else, you simply use the exported element.
TIP: Auto-import will be available only after a new build. If you have the dev server running, hot-reload will handle it within a few moments, but if the server is currently off, the function/constant will show up in your IDE as unknown. Sometimes the IDE may tend to insert an import at the beginning of the <script> section for you. This doesn't hurt anything, but it's unnecessary. And it's good to understand why it happens. Just run pnpm dev, which will perform the auto-scan and re-create the necessary .d.ts linking files. The IDE will understand it shortly after and stop reporting an error (if not, refreshing the project's window or in extreme cases restarting the IDE helps).
There is one small caveat: By default, this implicit content exposure is performed only at the top level of the /app/utils directory; it doesn't go recursively into subdirectories. I assume this is due to potential performance overhead and processing time. You can change this with configuration, but the recommended approach - if you decide to organize your reusable logic into multiple subdirectories - is rather to create a /utils/index.ts file, perform explicit imports of functions/constants in it, and then "re-export" them. It looks something like this:
// /app/utils/subdir/helper.ts
export function helper () {
// ...
}
// /app/utils/index.ts
export { helper } from '@/utils/subdir/helpers'
/app/composables
The /app/composables folder behaves exactly the same as /app/utils, but is intended for stateful functions. "Composables" should be placed here in the sense that Vue.js defines this term. There's an unwritten (actually written in the documentation) rule that a Vue.js composable name is ALWAYS prefixed with use.
As I've discovered - strictly technically speaking it doesn't matter. Nuxt doesn't enforce how the content of each directory should look. You can easily mix them up and have stateless functions in /app/composables and call composables from within /app/utils (as I "managed" to do in my first projects). However, from a quality and long-term maintainability perspective, it would be better to always follow this separation.
Case Study
On my personal blog site, I don't have any significant use for /app/utils. I've only placed a types.ts file holding data type definitions inside.
However, I use several /app/composables:
- useArticleLinkStore - loads article metadata from the internal database created by the Nuxt Content module. It allows the rest of the application to get their list and filter them in various ways. It happens to be a data store implementation from the Pinia library. Don't worry, I will cover both technologies in more detail in later tutorials.
-
useFunStore - loads and provides metadata for images displayed in the
Humorsection from the/data/fun.tsfile. This isn't really a typical use case, but I didn't want to introduce a more complex database solution for this simple scenario. -
usePageMeta - here I extracted the function that inserts SEO meta tags into each page so I don't have to repeat them on every page (it's a composable because it calls another composable -
useSeoMeta).
Demo Project
You can find the source code for the sample implementation here:
nuxt-utils @ GitHub
The project extends nuxt-pages @ GitHub from the Components & Pages tutorial.
The demonstration of using /app/utils is represented by the isPrime() function, which determines whether a given number is a prime. The implementation is on the home page /app/pages/index.vue. To make it more interesting, the source data (numbers 1-9) is provided by a JS generator generateSequence(). Like the isPrime() function, it is defined in /app/utils/utils.ts. In older versions of Nuxt 3, generator function* couldn't be auto-imported, but this has been resolved since then.
The example of /app/composables is useCounter - it's defined as a function that provides data about the click count and 3 methods - get the current count, add a specified amount, and reset. These functions are used by the new component /app/components/ClickCounter.vue, which obtains them from a simple useCounter() call and uses them in the template. The component is then inserted into the page through the default layout in the /app/app.vue file template. Notice the way how the composable returns data - as an object wrapping the parts we want to make accessible from the outside. This is how most Vue.js composables are designed, because it allows the return object to be destructured on the caller's side only to the parts they're interested in.
TIP: Because the "counter" is defined within the shared layout, you may notice that its value persists even after clicking to another link in the menu. This is again because I'm not using classic HTML <a> links, which would refresh the entire page - and thus the state of the counter component, which is not saved anywhere in this simple demo application. Instead, there's the <NuxtLink> component we already met, which only changes the "inside" of <NuxtPage>. However, using the generator in /app/components/TheMenu.vue implies one peculiarity - the link to / (index) has the property :external="true" set, which conversely forces a reload of the entire page (we're telling the application that it's going to a foreign - external - link). If this didn't happen, the way the function is currently written, it wouldn't run again (the generator would finish and stop) and nothing would be rendered on the home page.
Summary
Nuxt has two dedicated directories /app/utils and /app/composables, over which it automatically scans all export keywords and makes them accessible across the entire application without the need to perform explicit import where we want to use them. The /app/utils directory should be used for stateless functions, the /app/composables directory is for stateful logic and working with other composables from Vue.js, Nuxt, and other libraries.
The next installment of the tutorial will describe the server-side part of the Nuxt framework.
Top comments (0)