"Review this component" is a favourite senior exercise because it tests judgement rather than recall. The code compiles and works; the question is what will hurt in six months. This page collects the patterns worth naming in that conversation and the anti-patterns you should be able to spot in under a minute each.
useDataTable), the component is a thin view over it. The composable is unit-tested without a DOM; the component test only checks wiring. This is the pattern behind Nuxt UI and VueUse.<UseClipboard v-slot="{ copy, copied }">. Useful when the consumer must own the markup entirely; a composable is usually simpler unless you need it in a template.Tabs/Tab, Menu/MenuItem) share state through provide/inject with a typed key, so the consumer composes them freely in the template and the parent still coordinates.defineModel's local fallback; the same component works with or without v-model.#footer, #actions, #empty. Props configure behaviour; slots configure rendering.inheritAttrs: false and forward $attrs to the meaningful element so class, aria-* and native listeners keep working. Nuxt UI's ui prop pattern goes further and lets consumers target internal slots with classes.<Form :key="userId"> remounts the form when the user changes, which is cleaner than a watcher that clears twelve fields.useState; everything else stays local and travels by props and events.watch that writes total from items is a computed. Symptoms: stale values before the flush, forgotten second sources, immediate: true bolted on.v-model on props.form.name. It works and nobody can trace the write.setup. Extract composables by feature; the component should read like a table of contents.v-if and v-for on the same element (in Vue 3 v-if wins and cannot see the loop variable); v-for without a key.computed (fetching, writing to storage, emitting). Computed getters run when Vue decides.useState or provide from the app root.document.querySelector inside components where a template ref belongs; setTimeout(fn, 0) where nextTick belongs.mitt as a global) for parent–child communication. Vue 3 removed $on for a reason; use emits, provide/inject or a store.BaseTable with forty props to cover every table in the app. Two similar components are cheaper than one wrong abstraction.any at the boundaries: untyped emits, Record<string, any> props, as casts around $refs. The types are the API documentation.The review exercise in one file: three smells and their fixes.
<script setup lang="ts">
import type { CartItem } from '#shared/types/cart'
const props = defineProps<{ items: CartItem[], currency: string }>()
const emit = defineEmits<{ remove: [id: string] }>()
// ✗ Smell 1: a watcher computing derived state.
// const total = ref(0)
// watch(() => props.items, (items) => { total.value = items.reduce((n, i) => n + i.price * i.qty, 0) }, { immediate: true, deep: true })
// ✓ A computed is lazy, cached, and never stale.
const total = computed(() => props.items.reduce((n, i) => n + i.price * i.qty, 0))
// ✗ Smell 2: mutating a prop.
// function remove(id: string) { props.items.splice(props.items.findIndex(i => i.id === id), 1) }
// ✓ Ask the owner of the state to change it.
function remove(id: string) {
emit('remove', id)
}
// ✗ Smell 3: formatting in the template with a global helper and a locale from a store.
// ✓ A memoised formatter next to the data it formats.
const format = computed(() => new Intl.NumberFormat(undefined, { style: 'currency', currency: props.currency }))
</script>
<template>
<ul>
<!-- ✗ :key="index" would move row state when an item is removed -->
<li v-for="item in items" :key="item.id">
{{ item.name }} × {{ item.qty }}
<button type="button" @click="remove(item.id)">Remove</button>
</li>
</ul>
<p>Total: {{ format.format(total) }}</p>
</template>
A renderless component and the composable it wraps, to show that one is a thin shell over the other:
<script setup lang="ts">
const { copy, copied, isSupported } = useClipboard({ copiedDuring: 1500 })
defineSlots<{ default(props: { copy: (text: string) => Promise<void>, copied: boolean, isSupported: boolean }): any }>()
</script>
<template>
<slot :copy="copy" :copied="copied" :is-supported="isSupported" />
</template>
A component that grew a variant, then a mode, then hideHeader, then customFooterSlotName, is telling you it is two components. Splitting it is a refactor; keeping it is a permanent tax on every consumer. Interviewers like hearing that you have deleted an abstraction.
DialogRoot/DialogContent (or Nuxt UI's UModal) and compare the keyboard and focus behaviour with your original.Cart/Summary.vue above out loud as if in an interview: name each smell, the failure it causes and the fix, in under two minutes."Whether state lives in the right place and flows in one direction: props down, events up, no mutated props, derived values as computeds rather than watchers. Whether the component is thin over a composable so the logic is testable without a DOM. Whether the list keys are stable ids. Whether the API is typed at the edges: emits, slots, and no any. Whether the component is trying to be two components, which shows up as booleans that toggle chunks of markup where slots belong. And for SSR, whether anything touches window outside onMounted or keeps state at module level."
"Almost never for new code: a composable does the same job with less ceremony and works outside templates. The exceptions are when the consumer has to use the behaviour purely from a template, for example in a low-code or CMS-driven setup, or when the behaviour needs to render something itself, like a portal or a focus trap around slot content. Headless primitives such as Reka UI are the useful middle ground: they render minimal semantic markup with the accessibility handled, and leave styling to the consumer."
Nothing yet. Notes filed here show up on this page and the section overview.
Rendering and lifecycle
Lifecycle order on server and client, nextTick, Suspense, Teleport, KeepAlive and Transition, async components, template compilation, keys and render functions.
API styles and migrations
Options API versus Composition API and why the difference is architectural, the Vue 2 to 3 breaking changes worth remembering, Nuxt 3 to 4 to 5, and how a senior runs a migration.