Join 13,567+ other Vue devs and get exclusive tips and insights delivered straight to your inbox, every week.
👋Hey friend! I work hard to send you amazing stuff each week.
— Michael
Hey!
I've got a few new Nuxt articles for you (I've been busy):
If you're interested in learning more about component design patterns, Clean Components Toolkit is 35% off until May 22! You can grab it here: Clean Components Toolkit
Every week new lessons are being dropped for Mastering Nuxt! Yesterday, we started on the data fetching chapter where we wrap up all basic functionality on the app.
Have a fantastic week!
— Michael
Slots in Vue can have default content, which allows you to make components that are much easier to use:
<button class="button" @click="$emit('click')"><slot><!-- Used if no slot is provided -->Click me</slot></button>
My favourite use for default slots is using them to create extension points.
Basically, you take any part of a component, wrap it in a slot, and now you can override that part of the component with whatever you want. By default, it'll still work the way it always has, but now you have more options:
<template><button class="button" @click="$emit('click')"><!-- Adding in the slot tag does nothing at first --><!-- We can override this by providing content to the slot --><slot><div class="formatting">{{ text }}</div></slot></button></template>
Now you can use this component in many different ways. The easy, default way, or your own, custom way:
<!-- Uses default functionality of the component --><ButtonWithExtensionPoint text="Formatted text" /><!-- Use the extension point to create custom behaviour --><ButtonWithExtensionPoint><div class="different-formatting">Do something a little different here</div></ButtonWithExtensionPoint>
In Nuxt we can get detailed information on how our page is loading with the useLoadingIndicator
composable:
const {progress,isLoading,} = useLoadingIndicator();console.log(`Loaded ${progress.value}%`); // 34%
It’s used internally by the <NuxtLoadingIndicator>
component, and can be triggered through the page:loading:start
and page:loading:end
hooks (if you’re writing a plugin).
But we have lots of control over how the loading indicator operates:
const {progress,isLoading,start, // Start from 0set, // Overwrite progressfinish, // Finish and cleanupclear // Clean up all timers and reset} = useLoadingIndicator({duration: 1000, // Defaults to 2000throttle: 300, // Defaults to 200});
We’re able to specifically set the duration
, which is needed so we can calculate the progress
as a percentage. The throttle
value controls how quickly the progress
value will update — useful if you have lots of interactions that you want to smooth out.
The difference between finish
and clear
is important. While clear
resets all internal timers, it doesn’t reset any values.
The finish
method is needed for that, and makes for more graceful UX. It sets the progress
to 100
, isLoading
to true
, and then waits half a second (500ms). After that, it will reset all values back to their initial state.
We can make our composables more reusable by passing in an object that contains all of the configuration options for how we want the composable to behave:
const state = ref({ email: '' });const { history, undo, redo } = useRefHistory(state, {// Track history recursivelydeep: true,// Limit how many changes we savecapacity: 10,});
We use an object here instead of a long list of parameters:
const { history, undo, redo } = useRefHistory(state, true, 10));
Using an options object instead of parameters gives us several benefits.
First, it’s self-documenting. We have the name of the parameter right beside the value, so we never forget what each value is doing.
We can also create a type for the entire options object:
export type RefHistoryOptions {deep?: boolean;capacity?: number;};export type RefHistoryReturn {history: Ref;undo: () => void;redo: () => void;};export function useRefHistory(ref: Ref,options: RefHistoryOptions): RefHistoryReturn {};
Second, we don’t need to worry about ordering or unused options. The more potential edge cases we cover with a composable, the more options we’ll have. But we usually only need to worry about a couple of them at one time — they’re all optional.
Third, it’s much easier to add new options. Because the order doesn’t matter and none of the options are required, adding a new capability to our composable won’t break anything. We simply add it to the list of possible options and carry on.
The pattern doesn’t require a lot of work to implement, either:
export function useRefHistory(ref, options) {const {deep = false,capacity = Infinity,} = options;// ...};
First, we pass in the options object as the last parameter. This makes it possible to have the options object itself as an optional parameter.
The required params come first. Typically, there will only be one or two. More parameters is a code smell, and likely means that your composable is trying to do too much.
The required parameter (or parameters) is very often a Ref
, or a MaybeRef
if we’re also implementing the Flexible Arguments Pattern.
We then access the options by destructuring.
Doing this gives us a really clean and readable way of providing defaults. Remember, these are options so they should all have defaults. If the values are required they should likely have
This helps to clarify what options are being used in this composable. It’s not uncommon for one composable to use another composable, and in that case some of the options are simply passed along to the inner composable:
export function useRefHistory(ref, options) {const {deep = false,capacity = Infinity,...otherOptions,} = options;// Pass along some options we're not using directlyuseSomeOtherComposable(otherOptions);};
If you want to learn more patterns for composables, check out Composable Design Patterns.
One of Vue's core features is the use of props. Props are how we pass data around in Vue, from parent to child components.
But not all props are created equal.
There are three main kinds:
Check it out here: 3 Kinds of Props in Vue
The NuxtLink component may seem simple at first glance, but there’s a lot going on beneath the surface.
It’s one of the easiest Nuxt components to use, while giving our apps a big performance boost.
In this article we see some things about NuxtLink you may not have known.
Check it out here: Better Navigation with NuxtLink
Here are some upcoming events you might be interested in. Let me know if I've missed any!
VueConf US 2025 is a great Vue conference, this year held in Tampa from May 19–21, with two days of talks and a day of workshops. Unfortunately, I am no longer able to make it to the conference this year. I hope everyone attending has an amazing time and I look forward to joining in the future!
It's time to get together in Madrid. Join for a full day of talks, activities, and networking with the Vue.js community and ecosystem.
"You're bound to be unhappy if you optimize everything." — Donald Knuth
The best way to commit something to long-term memory is to periodically review it, gradually increasing the time between reviews 👨🔬
Actually remembering these tips is much more useful than just a quick distraction, so here's a tip from a couple weeks ago to jog your memory.
Testing is important to do, but it can be hard to do.
In my experience, good architecture lends itself to easy-to-write tests (or at least, easier-to-write). The inverse is also true, that difficult-to-write tests are typically a symptom of poor architecture.
Of course, sometimes tests are just hard to write, and there’s no way around it.
The best thing we can do is borrow a tool from mathematics and science, and transform a difficult problem into an easier but equivalent one:
Michael Hoffman curates a fantastic weekly newsletter with the best Vue and Nuxt links.
p.s. I also have a bunch of products/courses: