🔥 (#180) Self-healing URLs

Hey all!

I'm finally selling the hardcover of Vue Tips Collection 2 again!

It's also a great price — $77 with shipping included, until Friday.

Get the hardcover of Vue Tips Collection 2 here

We've also got a new podcast and tips.

I've also got a short newsletter-only riff on self-healing URLs. Let me know if you're liking this new format!

Have a great week,

— Michael

📝 Self Healing URLs

Recently, I've been thinking about self-healing URLs. It's a pretty cool concept, but I have yet to implement it in any project.

A lot of the time we need to stick an id or some sort of unique hash into our URL.

But this isn't exactly a great user experience — the hash isn't human readable or descriptive, and hashes don't exactly help your SEO.

However, we need some sort of unique hash or id. If we just use the title of the blog post, for example, what happens when we update that title?

/my-great-article/ -> /my-awesome-article/

You may have seen these "self-healing" URLs in the wild before: they combine the best of both solutions.

Here's an example from Amazon. The full URL for this microphone is quite long:

https://www.amazon.ca/Condenser-Microphone-One-Channel-Audio-Interface/dp/B077Y5C6HZ/

But, the entire human readable part, "Condenser-Microphone-One-Channel-Audio-Interface", doesn't matter at all. If you remove bits, or remove that section entirely, it will load the correct page.

This is the only part that matters:

https://www.amazon.ca/dp/B077Y5C6HZ/

Here's how we can implement a simple version in our own apps.

First, we append a hash at the end of the URL like this: /my-great-article-b33f123/

Then, any of these will be redirected to the correct URL:

  • /my-great-article-b33f123/
  • /my-horrible-article-b33f123/
  • /b33f123/

We can achieve this using logic that's something like this:

// Get the param from the route  const slugHash = route.params.slugHash;  const lastIndex = slugHash.lastIndexOf('-');  // Extract slug and hash  const slug = slugHash.slice(0, lastIndex);  const hash = slugHash.slice(lastIndex + 1);  // Find the post by hash  const foundPost = posts.find((p) => p.id === hash);  if (foundPost) {    post.value = foundPost;    // If the slug is incorrect, redirect to the correct URL    if (foundPost.slug !== slug) {      router.replace(`/${foundPost.slug}-${foundPost.id}`);    }  }

We find the article based on the hash, and nothing else. This makes it robust against any other part of the URL being changed or incorrect.

This makes it possible to have the human readable part of the URL change over time, as the title of our blog post changes, or the name of the product we're selling.

The key part is making sure that we have a URL format that allows for this — we need to always be able to correctly identify the hash. In this example, the hash always follows the last hypen (if there is any). This means the hash itself cannot contain a hyphen, or we may only pull out the last part of the hash.

Once we've found the correct item based on the hash, we can then redirect to the correct URL.

So, this idea is simple in theory, but will definitely have some tricky pieces in practice.

Have you ever implemented anything like this in an app?

Also, let me know if you're enjoying this short articles in the emails!

🔥 Nesting Reactive Objects

Here's a nuance to nesting reactive objects that's tripped me up.

Nesting a ref in a reactive array keeps everything reactive as expected:

const arr = reactive([]);  arr.push(ref('hello'));  arr.push(ref('world'));  setTimeout(() => (arr[0].value = 'nothing'), 1000);

But putting this ref inside a non-reactive object breaks this reactivity:

const arr = reactive([]);  arr.push({    text: ref('hello'),  });  arr.push({    text: ref('world'),  });  setTimeout(() => (arr[0].value = 'nothing'), 1000);

I gave a hint in that last sentence — this is because it's being wrapped in a non-reactive object. The trail of reactivity goes cold once we hit this object, but only because we're accessing the text property through the non-reactive object.

If we instead access the ref directly, we're able to trigger a reactive update as expected:

const arr = reactive([]);  const one = ref('hello');  const two = ref('world');  arr.push({    text: one,  });  arr.push({    text: two,  });  // This triggers the update correctly  setTimeout(() => (one.value = 'nothing'), 1000);

Of course, this isn't about refs in particular. All we need is to keep the reactivity alive, which we can also achieve using reactive.

🔥 Components are Functions

Underneath it all, components are just functions that return some HTML.

It's a huge simplification, and if you've ever looked at the complexity of the Vue codebase you know this isn't actually true. But, fundamentally, this is what Vue is doing for us — plus a million other amazing things.

Take a look at this component:

<template>    <div>      <h1></h1>      <p>Some words that describe this thing</p>      <button>Clickity click!</button>    </div>  </template>

Now, here is some Javascript that does essentially the same thing:

function component(title) {    let html = '';    html += '<div>';    html += `<h1>${title}</h1>`;    html += '<p>Some words that describe this thing</p>';    html += '<button>Clickity click!</button>';    html += '</div>';    return html;  }

This code constructs the HTML in much the same way that the Vue component would.

Granted, we don't get reactivity, event handling, or a bunch of other features with this, but the HTML that gets output is the same thing.

This means that we can apply all of the patterns and experience from Javascript (and other languages) to our components. Things like breaking components down into smaller pieces, naming them well, and avoiding over-abstraction are all examples.

Of course, there are many more!

🔥 Master computed refs (and computed props)

When a function does more than just return a value, it complicates your code.

These are called side effects, and you should never have them inside of a computed ref:

const searchString = ref('tacos');  const requiredIngredients = ref(['chicken', 'cilantro']);  const searchResults = ref([]);  const getData = async () => {    searchResults.value = await fetch(`/api/search`, {      method: 'POST',      body: searchQueryObject,    });  };  // Combine together to create a search query object  const searchQueryObject = computed(() => {    // 🚫 Ew, side effect! :/    getData();    return {      type: 'recipe',      string: searchString.value,      ingredients: requiredIngredients.value,    };  });

However, fixing this is quite straightforward. We can just move that side effect into a watcher that is triggered whenever the computed ref updates:

const searchString = ref('tacos');  const requiredIngredients = ref(['chicken', 'cilantro']);  const searchResults = ref([]);  // Combine together to create a search query object  const searchQueryObject = computed(() => ({    type: 'recipe',    string: searchString.value,    ingredients: requiredIngredients.value,  }));  // Fetch new data every time the query changes  watchEffect(async () => {    searchResults.value = await fetch(`/api/search`, {      method: 'POST',      body: searchQueryObject,    });  });

This applies equally to the Options API, although the syntax is slightly different:

export default {    computed: {      searchQuery() {        return {          type: 'recipe',          string: this.searchString,          ingredients: this.requiredIngredients,        };      },    },    watch: {      async searchQuery(query) {        this.searchResults = await fetch(`/api/search`, {          method: 'POST',          body: query,        });      },    },  };

At first glance, this may seem like we made the code more complicated. But it's actually easier to understand the flow of data here.

🎙️ #022 — Signals and Vue

In this episode, Michael Thiessen and Alexander Lichter dive into the current hype surrounding "Signals" in frontend development. They explore the history of Signals, how they compare to Vue's reactivity system, and what the future might hold if Signals become part of the ECMAScript standard.

Watch on YouTube or listen on your favourite podcast platform.

Chapters:

In case you missed them:

📜 Adding Drag and Drop (video)

In this video from LearnVue, we see how easy it can be to add a powerful drag and drop system in to your app.

Check it out here: Adding Drag and Drop (video)

📜 Why you should be using Vue 3's Composition API

This is a guest post by Andrew Schmelyun.

Go check out his blog for more awesome content like this!

Check it out here: Why you should be using Vue 3's Composition API

📅 Upcoming Events

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

PragVue 2024 — (September 17, 2024)

The first Czech Vue.js conference, taking place in Cinema City - Nový Smíchov

Check it out here

Vuejs.de Conf — (October 8, 2024 to October 9, 2024)

A community-driven Vue conference in Germany. Listen to great talks from great speakers and meet the wonderful VueJS Community.

Check it out here

Vue Fes Japan 2024 — (October 19, 2024)

Check it out here

VueConf Toronto 2024 — (November 18, 2024 to November 20, 2024)

My favourite Vue conference, in my own backyard! A three-day event with workshops, speakers from around the world, and socializing.

Check it out here

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

💬 The Goal

"The goal of software development is to build something that lasts at least until we've finished building it." — Anonymous

🧠 Spaced-repetition: Aria roles you didn't know you needed

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.

Aria roles are used to tell a screenreader what an element is for.

This is really important when the native HTML element just doesn't exist (eg. roles like toolbar and alert) or when you're using a different HTML element for design or technical reasons (eg. wrapping a radio button to style it).

But please, remember that you should always use the semantic element where you can. This is always the best and most effective solution.

There are six different categories of aria roles:

  • Widget - roles like button, checkbox, separator, tab, or scrollbar
  • Composite - roles like combobox and listbox (these are for dropdown menus), radiogroup, or tree
  • Document structure - this includes article, presentation, figure, feed, and directory
  • Landmark - banner, main, navigation, and region are roles in this category
  • Live region - alert, log, marquee, and status are roles that might update with real-time information
  • Window - alertdialog and dialog are the only two roles in this category

You can check out the full list here: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles

🔗 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 four products/courses: Clean Components Toolkit, Vue Tips Collection 2, Mastering Nuxt 3, and Reusable Components

Unsubscribe

评论

此博客中的热门博文

Scripting News: Tuesday, June 11, 2024

Scripting News: Tuesday, February 13, 2024