🔥 (#139) Creating Web Components with Vue
Hey!
A week from now on November 22, Clean Components Toolkit will be released!
You'll learn tools to help solve every day problems ranging from organizing business logic, breaking up components intelligently, and managing state between components.
Each tool also comes with:
- A discussion of edge cases and common mistakes
- An in-depth refactoring example so you can see exactly how it's used
- A quiz to check your understanding
- A video going through the quiz answers and explaining my reasoning behind each
To celebrate the launch, I wanted to give you a sneak peek at some of the content — it's really good btw.
Last week I did a workshop on this content at VueConf Toronto and it went very well, and I can't wait for you to get your hands on this!
(You can get the slides for my talk here)
And of course, I've also included your regular weekly tips.
While you read those, I'll be busy putting the final touches on these tools for next week.
Have a great week!
— Michael
🔥 Clean Components Toolkit Preview
I wanted to give you a sneak peek at some of the tools included in Clean Components Toolkit before it's released next week.
Here's an overview of a few that you'll get access to.
Thin Composables
Thin composables introduce an additional layer of abstraction, separating the reactivity management from the core business logic. Here we use plain JavaScript or TypeScript for business logic, represented as pure functions, with a thin layer of reactivity on top.
import { ref, watch } from 'vue'; import { convertToFahrenheit } from './temperatureConversion'; export function useTemperatureConverter(celsiusRef: Ref<number>) { const fahrenheit = ref(0); watch(celsiusRef, (newCelsius) => { // Actual logic is contained within a pure function fahrenheit.value = convertToFahrenheit(newCelsius); }); return { fahrenheit }; }
Humble Components Pattern
Humble Components are designed for simplicity, focusing on presentation and user input, keeping business logic elsewhere. Following the "Props down, events up" principle, these components ensure clear, predictable data flow, making them easy to reuse, test, and maintain.
<template> <div class="max-w-sm rounded overflow-hidden shadow-lg"> <img class="w-full" :src="userData.image" alt="User Image" /> <div class="px-6 py-4"> <div class="font-bold text-xl mb-2"> {{ userData.name }} </div> <p class="text-gray-700 text-base"> {{ userData.bio }} </p> </div> <div class="px-6 pt-4 pb-2"> <button @click="emitEditProfile" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" > Edit Profile </button> </div> </div> </template> <script setup> defineProps({ userData: Object, }); const emitEditProfile = () => { emit('edit-profile'); }; </script>
Extract Conditional
To simplify templates with multiple conditional branches, we extract each branch's content into separate components. This improves readability and maintainability of the code.
<!-- Before --> <template> <div v-if="condition"> <!-- Lots of code here for the true condition --> </div> <div v-else> <!-- Lots of other code for the false condition --> </div> </template> <!-- After --> <template> <TrueConditionComponent v-if="condition" /> <FalseConditionComponent v-else /> </template>
Extract Composable
Extracting logic into composables, even for single-use cases. Composables simplify components, making them easier to understand and maintain. They also facilitate adding related methods and state, such as undo and redo features. This helps us keep logic separate from UI.
import { ref, watch } from 'vue'; export function useExampleLogic(initialValue: number) { const count = ref(initialValue); const increment = () => { count.value++; }; const decrement = () => { count.value--; }; watch(count, (newValue, oldValue) => { console.log(`Count changed from ${oldValue} to ${newValue}`); }); return { count, increment, decrement }; }
<template> <div class="flex flex-col items-center justify-center"> <button @click="decrement" class="bg-blue-500 text-white p-2 rounded" > Decrement </button> <p class="text-lg my-4">Count: {{ count }}</p> <button @click="increment" class="bg-green-500 text-white p-2 rounded" > Increment </button> </div> </template> <script setup lang="ts"> import { useExampleLogic } from './useExampleLogic'; const { count, increment, decrement } = useExampleLogic(0); </script>
List Component Pattern
Large lists in components can lead to cluttered and unwieldy templates. The solution is to abstract the v-for loop logic into a child component. This simplifies the parent and encapsulates the iteration logic in a dedicated list component, keeping things nice and tidy.
<!-- Before: Direct v-for in the parent component --> <template> <div v-for="item in list" :key="item.id"> <!-- Lots of code specific to each item --> </div> </template> <!-- After: Abstracting v-for into a child component --> <template> <NewComponentList :list="list" /> </template>
Data Store Pattern
In growing applications, prop drilling and event frothing increase complexity, as state and events are passed through many component layers. The Data Store Pattern solves this by creating a global state singleton, exposing parts of this state, and including methods to modify it.
<script setup lang="ts"> import useUserSettings from '~/composables/useUserState'; const { theme, changeTheme } = useUserSettings(); </script>
// useUserState.ts import { reactive, toRefs, readonly } from 'vue'; import { themes } from './utils'; const state = reactive({ darkMode: false, sidebarCollapsed: false, theme: 'nord', }); export default () => { const { darkMode, sidebarCollapsed, theme } = toRefs(state); const changeTheme = (newTheme) => { if (themes.includes(newTheme)) { state.theme = newTheme; } }; return { darkMode, sidebarCollapsed, theme: readonly(theme), changeTheme, }; };
🔥 Vue to Web Component in 3 Easy Steps
Here's how you can create web components in Vue.
First, create the custom element from a Vue component using defineCustomElement
:
import { defineCustomElement } from 'vue'; import MyVueComponent from './MyVueComponent.vue'; const customElement = defineCustomElement(MyVueComponent);
Second, register the custom element with the DOM:
customElements.define('my-vue-component', customElement);
Third, use the custom element in your HTML:
<html> <head></head> <body> <my-vue-component></my-vue-component> </body> </html>
Now you've got a custom web component that doesn't need a framework and can run natively in the browser!
Check out the docs for more details on how this works.
🔥 Forcing a Component to Update
What do you do if a component isn't updating the way it should?
Likely, this is caused by a misunderstanding and misuse of the reactivity system.
But let's look at a quick solution using forceUpdate
:
import { getCurrentInstance } from 'vue'; const methodThatForcesUpdate = () => { // ... const instance = getCurrentInstance(); instance.proxy.forceUpdate(); // ... };
Using the Options API instead:
export default { methods: { methodThatForcesUpdate() { // ... this.$forceUpdate(); // Notice we have to use a $ here // ... } } }
Now, here comes the sledgehammer if the previous approach doesn't work.
I do not recommend using this approach. However, sometimes you just need to get your code to work so you can ship and move on.
But please, if you do this, keep in mind this is almost always the wrong way, and you're adding tech debt in to your project.
We can update a componentKey
in order to force Vue to destroy and re-render a component:
<template> <MyComponent :key="componentKey" /> </template> <script setup> import { ref } from 'vue'; const componentKey = ref(0); const forceRerender = () => { componentKey.value += 1; }; </script>
The process is similar with the Options API:
export default { data() { return { componentKey: 0, }; }, methods: { forceRerender() { this.componentKey += 1; } } }
You can find a deeper explanation here: https://michaelnthiessen.com/force-re-render/
🔥 The picture element
The <picture>
element lets us provide many image options for the browser, which will then decide what the best choice is:
<picture> <!-- You can have as many source tags as you want --> <!-- (or none at all!) --> <source srcset="big-image.png" media="(min-width: 1024px)"> <source srcset="bigger-image.png" media="(min-width: 1440px)"> <source srcset="biggest-image.png" media="(min-width: 2048px)"> <!-- One img tag is required to actually display the image --> <!-- and is used as the default choice --> <img src="regular-image.png"> </picture>
You can provide different options based on screen size, resolution, and supported image formats.
The mdn docs have more info on this element.
📜 Prisma with Nuxt 3: Seeding the Database with Dummy Data (3 of 5)
A database is useless without any data.
But with Prisma, adding in seed data (or "dummy" data) is extremely easy.
In this article we'll cover:
- How to generate our Prisma client
- How to update our Supabase database with migrations
- How to add in dummy data
Check it out here: Prisma with Nuxt 3: Seeding the Database with Dummy Data (3 of 5)
💬 One way street
"A good programmer is someone who always looks both ways before crossing a one-way street." — Doug Linder
🧠 Spaced-repetition: v-once
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've got large chunks of static or mostly static content, you can tell Vue to (mostly) ignore it using the v-once
directive:
<template> <!-- These elements never change --> <div v-once> <h1 class="text-center">Bananas for sale</h1> <p> Come get this wonderful fruit! </p> <p> Our bananas are always the same price — ${{ banana.price }} each! </p> <div class="rounded p-4 bg-yellow-200 text-black"> <h2> Number of bananas in stock: as many as you need </h2> <p> That's right, we never run out of bananas! </p> </div> <p> Some people might say that we're... bananas about bananas! </p> </div> </template>
This can be a helpful performance optimization if you need it.
The v-once
directive tells Vue to evaluate it once and never update it again. After the initial update it's treated as fully static content.
Here are the docs for v-once.
p.s. I also have four courses: Vue Tips Collection, Mastering Nuxt 3, Reusable Components and Clean Components
评论
发表评论