🔥 (#199) Async without await, SSR safe directives, custom directives, and more

Read this on my blog

Hey all!

Tomorrow I'm launching Composable Design Patterns — a collection of 15 patterns on how to use the composition API better.

I've been working on this for a while (it's been in my head for years), and I'm excited to share it with you!

You'll be getting a special launch discount, so make sure to pay attention to your email inbox tomorrow.

Other than that, I've got a new podcast episode for you, and some links to check out, as well as some tips.

Enjoy your week!

— Michael

🔥 Async Without Await

Using async logic with the composition API can be tricky at times.

We need to put things in the correct order, or the await keyword will mess things up with our reactivity.

But with the Async Without Await pattern, we don't need to worry about all of this:

const title = ref('Basic Title');  // We can place this async function wherever we want  const { state } = useAsyncState(fetchData());  const betterTitle = computed(() => `${title.value}!`);

Here's how this works:

  • We hook up all of our refs synchronously
  • Updates happen asynchronously in the background
  • Because of reactivity, everything "just works"

Here's a basic sketch of what the useAsyncState composable from VueUse is doing to implement this:

export default useAsyncState(promise) {    // 1. Create state ref synchronously    const state = ref(null);      const execute = async () => {      // 3. Reactivity will update this when it resolves      state.value = await promise;    }      // 2. Execute promise asynchronously in the background    execute();    return state;  }

🔥 SSR Safe Directives

In many cases, we need to generate unique IDs for elements dynamically.

But we want this to be stable through SSR so we don't get any hydration errors.

And while we're at it, why don't we make it a directive so we can easily add it to any element we want?

Here's a stripped-down version of this directive:

const generateID = () => Math.floor(Math.random() * 1000);    const directive = {    getSSRProps() {      return { id: generateID() };    },  }

When using it with Nuxt, we need to create a plugin so we can register the custom directive:

// ~/plugins/dynamic-id.ts  const generateID = () => Math.floor(Math.random() * 1000);    export default defineNuxtPlugin((nuxtApp) => {    nuxtApp.vueApp.directive("id", {      getSSRProps() {        return { id: generateID() };      },    });  });

In Nuxt 3.10+, you can also use the useId composable instead:

<template>    <div :id="id" />  </template>    <script setup>  const id = useId();  </script>

Normally, custom directives are ignored by Vue during SSR because they typically are there to manipulate the DOM. Since SSR only renders the initial DOM state, there's no need to run them, so they're skipped.

But there are some cases where we actually need the directives to be run on the server, such as with our dynamic ID directive.

That's where getSSRProps comes in.

It's a special function on our directives that is only called during SSR, and the object returned from it is applied directly to the element, with each property becoming a new attribute of the element:

getSSRProps(binding, vnode) {    // ...      return {      attribute,      anotherAttribute,    };  }

🔥 Custom Directives

In script setup you can define a custom directive just by giving it a camelCase name that starts with v:

<script setup>  const vRedBackground = {    mounted: (el) => el.style.background = 'red',  }  </script>    <template>    <input v-red-background />  </template>

With the Options API:

export default {    setup() {      // ...    },    directives: {      redBackground: {        mounted: (el) => el.style.background = 'red',      },    },  }

Registering a directive globally:

const app = createApp({})    // make v-focus usable in all components  app.directive('redBackground', {    mounted: (el) => el.style.background = 'red',  })

And since a very common use case is to have the same logic for the mounted and updated hooks, we can supply a function instead of an object that will be run for both of them:

<script setup>  const vRedBackground = (el) => el.style.background = 'red';  </script>    <template>    <input v-red-background />  </template>

You can find more info on custom directives in the docs.

🔥 The Controller Components Pattern

Controller Components bridge the gap between UI (Humble Components) and business logic (composables).

They manage the state and interactions, orchestrating the overall behavior of the application.

<!-- TaskController.vue -->  <script setup>    import useTasks from './composables/useTasks';      // Composables contain the business logic    const { tasks, addTask, removeTask } = useTasks();  </script>    <template>    <!-- Humble Components provide the UI -->    <TaskInput @add-task="addTask" />    <TaskList :tasks @remove-task="removeTask" />  </template>

They're useful because they allow us to to push complex logic into composables, and push complex UI into Humble Components.

By doing this, we can keep our components simple and focused on their purpose, while still being able to handle complex logic and state.

To learn more about these 3 patterns and how they work together, check out Clean Components Toolkit.

🎙️ #041 — The Quadruple Migration (with Rijk van Zanten)

To start the year light and fun, Michael and Alex are joined by Rijk van Zanten, the creator of Directus.

Of course we talk about his journey into web development, the Vue.js ecosystem, what Directus is and why he chose Vue over other frameworks for it's extensible frontend.

Further, Rijk shares his thoughts on the Vue.js job market and how his "Quadruple Migration", over to the Composition API, Pinia, Vue 3 and Vite, went. As a cherry on top - this all started very early in the development cycle of Vue 3!

But the fun doesn't and there because Rijk comes with the one or the other hot take on topics like TypeScript and whether our libraries will be worse for JavaScript developers, testing, and many many other scenarios.

Watch on YouTube or listen on your favorite podcast platform.

Chapters:

In case you missed them:

📜 Client-Side Error Handling in Nuxt

It may not be as fun as shipping the next feature, but making sure our apps are rock-solid and can recover from errors is crucial.

Without good error-handling, our UX suffers and our applications can be hard to use.

In this article I explore handling client-side errors using the NuxtErrorBoundary component.

Check it out here: Client-Side Error Handling in Nuxt

📜 Using OAuth in Nuxt

OAuth is tricky to understand, so I wanted to write something to help explain how it works.

Luckily, we can use auth without knowing OAuth, but it's still good to understand how it works.

This one is based on the Mastering Nuxt course I created with Vue School.

Check it out here: Using OAuth in Nuxt

📅 Upcoming Events

Here are some upcoming events you might be interested in. Let me know if I've missed any!

Vuejs Amsterdam 2025 — (March 12, 2025 to March 13, 2025)

The biggest Vue conference in the world! A two-day event with workshops, speakers from around the world, and socializing.

Check it out here

VueConf US 2025 — (May 13, 2025 to May 15, 2025)

A great Vue conference, this year held in Tampa. Two days of conference talks, plus a day for workshops.

Check it out here

💬 Programmers and designers

"If you make a general statement, a programmer says, 'Yes, but...'while a designer says, 'Yes, and...'." — André Bensoussan

🧠 Spaced-repetition: Special CSS pseudo-selectors in Vue

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.

If you want some styles to apply specifically to slot content, you can do that with the :slotted pseudo-selector:

<style scoped>    /* Add margin to <p> tags within the slot */    :slotted(p) {      margin: 15px 5px;    }  </style>

You can also use :global to have styles apply to global scope, even within the

<style scoped>    :global(body) {      margin: 0;      padding: 0;      font-family: sans-serif;    }  </style>

Of course, if you have lots of global styles you want to add, it's probably easier to just add a second

<style scoped>    /* Add margin to <p> tags within the slot */    :slotted(p) {      margin: 15px 5px;    }  </style>    <style>    body {      margin: 0;      padding: 0;      font-family: sans-serif;    }  </style>

Check out the docs for more info.

🔗 Want more Vue and Nuxt links?

Michael Hoffman curates a fantastic weekly newsletter with the best Vue and Nuxt links.

Sign up for it here.

p.s. I also have a bunch of products/courses:

Unsubscribe

评论

此博客中的热门博文

🔥 (#155) A Vue podcast?

Scripting News: Tuesday, February 13, 2024