systematic quality assurance for Vue 3 projects
Code reviews in Vue teams rarely fail because of a lack of willingness, they fail because of a missing framework. Without clear criteria, reviews turn into subjective style debates instead of real quality checks. This article shows which review patterns actually matter, from component design through reactivity mistakes to Pinia store quality and TypeScript integration.
Table of Contents
- 1. Why review patterns are indispensable for Vue teams
- 2. Component design: single responsibility and props contracts
- 3. Reactivity mistakes: the most common Vue code review findings
- 4. Composables: quality criteria for reusable logic
- 5. Pinia stores: what has to be checked in review
- 6. TypeScript integration: types as a review quality marker
- 7. Template quality: v-if, v-for and key handling
- 8. Performance checks in the Vue code review
- 9. Review criteria at a glance
- 10. Summary
- 11. FAQ
1. Why review patterns are indispensable for Vue teams
A Vue code review without defined review criteria is a conversation about taste. One reviewer asks about naming conventions, another argues about tab indentation, while critical reactivity mistakes or broken prop validation slip unnoticed into the main branch. Structured Vue code review patterns shift the conversation away from style questions toward measurable quality attributes: Is the component testable? Are the props typed? Is reactivity used correctly? These are questions with clear answers, not a matter of taste.
The second reason for systematic Vue code review patterns is consistency across time and reviewers. In a team with three developers, person A reviews today, person B tomorrow and person C next week. Without a shared checklist, the bar varies considerably. A documented review checklist is not a sign of distrust toward developers, it is a tool that decouples review quality from the reviewer's experience and mood on a given day. New team members learn the project standards at the same time by working through the checklist.
Vue 3 and the Composition API have changed the attack surface for typical review findings. With the Options API the problem was often missing this binding or the wrong mixin usage. With the Composition API it is reactivity loss on destructuring, missing cleanup functions in watchEffect and uncontrolled side effects in composables. Anyone whose Vue code review process is still geared toward Options API patterns misses the new sources of error introduced by the Composition API.
2. Component design: single responsibility and props contracts
The first quality attribute in the Vue code review is component size and responsibility. A component with more than one screen of code that handles data fetching, business logic and rendering all at once is a clear review signal. The single responsibility question: can you describe in one sentence what this component does? If not, it belongs split up. The container/presentational pattern separates data acquisition (container component with store access) from rendering (presentational component that only receives props).
Props contracts are the second important review criterion. Every prop must have an explicit type, in TypeScript projects via the defineProps<{ propName: Type }>() pattern, in JavaScript projects via the object syntax with type and required. A Vue code review should treat untyped props as a review blocker, not as an optional recommendation. Emits must be declared just as strictly: defineEmits<{ eventName: [payload: Type] }>(). Undocumented emits are a hidden API that creates invisible breakage during refactoring.
// WRONG: untyped props, undeclared emits, mixes concerns
// ProductCard.vue, no props types, implicit emits, fetches own data
const props = defineProps(['id', 'title']) // no types, no required
const emit = defineEmits(['click']) // no payload type
const data = await fetch(`/api/products/${props.id}`) // fetches own data
// RIGHT: typed props, declared emits, presentational component only
// ProductCard.vue, receives fully typed data, emits typed events
interface Props {
productId: string
title: string
price: number
inStock: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
addToCart: [productId: string, quantity: number]
viewDetails: [productId: string]
}>()
// No data fetching, parent or store provides data
// Component only renders what it receives
3. Reactivity mistakes: the most common Vue code review findings
Reactivity loss through destructuring is the most common mistake in the Vue code review of Composition API code. const { count } = useCounterStore(), now count is a plain number, not a ref. Changes to the store no longer show up in the component. The correct pattern: const { count } = storeToRefs(useCounterStore()) for state and getters, direct destructuring only for actions. In the Vue code review you look for every destructured access to store state and check whether storeToRefs() is used.
The second common reactivity problem is missing cleanup for watch and watchEffect. A watchEffect can return a cleanup function that runs before the next run or on component unmount. Without cleanup, memory leaks build up, especially with event listener registrations or timer setups inside watchers. A Vue code review check: every watchEffect and watch with side effects (addEventListener, setInterval, WebSocket connections) must have a cleanup function.
4. Composables: quality criteria for reusable logic
Composables are the most powerful feature of the Composition API, but also the biggest source of quality problems in the Vue code review. The first criterion: a composable's name must clearly communicate what it does. useProductData() is unclear, does it load data? Does it hold state? Does it validate? useFetchProduct(id) or useProductStore() are self explanatory. Naming is not a style issue, it is a documentation issue: other developers need to understand what the composable does from its name alone.
The second criterion: composables that register side effects must clean them up on unmount. The pattern is onUnmounted or returning a cleanup function. A composable that registers an addEventListener on the body and never cleans up creates a memory leak that accumulates across page navigations. In the Vue code review, every composable with side effects gets checked for cleanup. The third criterion is testability: can the composable be called in isolation in a unit test? Composables with direct DOM access, hardcoded URL strings or global singleton dependencies are hard to test and need to be refactored.
5. Pinia stores: what has to be checked in review
Pinia stores have their own quality criteria in the Vue code review. The first: async actions must maintain loading and error state. A store that makes API calls without setting isLoading and error in its state forces components to maintain their own loading states, which leads to inconsistencies. The second criterion: state changes may only happen inside actions, never directly from components. A component that calls store.items.push() directly bypasses the store contract and makes the data flow invisible to DevTools and tests.
The third Pinia review criterion is store composition. When a component imports two stores and combines their data to compute a derived value, that computation belongs in a getter or an action that merges both stores. Calculation logic in component templates that depends on multiple store values is a review signal. Fourth criterion: store IDs must be unique and descriptive, not useStore or useMainStore, but useCartStore, useAuthStore, useCheckoutStore.
// Vue Code Review: Pinia anti-pattern vs. correct pattern
// WRONG: component directly mutates store state
// In ProductList.vue:
const store = useProductStore()
store.products.push(newProduct) // direct mutation, bypasses DevTools tracking
// WRONG: loading state managed in component
const isLoading = ref(false)
const loadProducts = async () => {
isLoading.value = true
store.products = await api.getProducts()
isLoading.value = false
}
// RIGHT: store manages its own loading and error state
// In stores/product.ts:
const fetchProducts = async () => {
isLoading.value = true
lastError.value = null
try {
products.value = await api.getProducts()
} catch (err) {
lastError.value = err instanceof Error ? err.message : 'Fetch failed'
} finally {
isLoading.value = false
}
}
// In component: just call the action, read reactive state
const store = useProductStore()
onMounted(() => store.fetchProducts())
// template: v-if="store.isLoading", v-if="store.lastError"
6. TypeScript integration: types as a review quality marker
TypeScript quality is a standalone review criterion in the Vue code review. The most obvious problem: any types. An any in a function, a prop or an API response definition is a hole in the type system, at that point TypeScript no longer offers any guarantees. In the Vue code review, any types are review blockers or at minimum review comments requiring justification. The alternative to any is almost always unknown with explicit type narrowing, or a correct interface for the structure in question.
The second TypeScript criterion: return types for public functions in composables and stores must be explicitly declared. TypeScript can infer return types, but explicit declarations document the composable's API and prevent refactoring from accidentally changing the return type without a compile error. The third criterion: template ref types must be typed correctly. const inputRef = ref<HTMLInputElement | null>(null), not ref(null). Wrong or missing template ref types produce runtime errors that TypeScript could have prevented.
7. Template quality: v-if, v-for and key handling
Template code is often reviewed less strictly in the Vue code review than script code, even though it has just as many potential failure points. The first criterion: never v-if and v-for on the same element. The evaluation order (in Vue 2: v-for first, in Vue 3: v-if first) leads to unexpected behavior and is hard to debug. The correct pattern: v-for on the outer element, v-if on an inner element, or prefilter the list in a getter.
The second template criterion is the :key attribute on v-for. A :key using the array index (:key="index") is almost always wrong, Vue cannot properly distinguish the elements this way and re-renders all elements whenever the list changes. The correct key is a stable, unique ID from the data: :key="product.id". In the Vue code review, every :key="index" deserves a comment, because it leads to visible performance problems in animated lists. Third criterion: complex expressions in the template belong in computed properties, not as inline JavaScript in the template.
8. Performance checks in the Vue code review
Performance problems are harder to spot in the Vue code review than functional bugs, because they often only become visible with larger datasets or more frequent updates. The first performance review criterion: unnecessary reactivity. A large object that is only rendered once for initial display and never changes should not be wrapped in reactive() or ref(). Object.freeze() or using it directly as a props default prevents Vue from creating a reactive proxy for that data.
The second performance criterion: getter misuse. A getter that runs a complex sort or filter operation is recomputed on every change of its dependent state. If that state changes very frequently (for example during live search), this can cause noticeable render delays. The review pattern: getters should be pure transformations without side effects, working on a stable state base. For expensive computations over large datasets, computed() with explicit caching or precomputing values in actions is preferable. Third criterion: v-show vs. v-if, for frequently toggled elements v-show is more efficient because no DOM node gets recreated.
9. Review criteria at a glance
Not every review issue carries the same weight. This overview shows which findings should be treated as review blockers and which are acceptable as informal comments.
| Area | Review Blocker | Review Comment | Reason |
|---|---|---|---|
| Type system | any without justification | Missing return types | any breaks TypeScript guarantees |
| Reactivity | Destructuring without storeToRefs | Unnecessary reactive() wraps | Reactivity loss is a runtime bug |
| Pinia | Direct state mutation from components | Missing isLoading flags | Bypasses DevTools and tests |
| Template | v-if + v-for on the same element | :key="index" on static lists | Undefined behavior in Vue 3 |
| Composables | Side effects without onUnmounted cleanup | Unclear composable names | Memory leak on page navigation |
The review checklist can be complemented in the CI/CD pipeline with static analysis. eslint-plugin-vue with the vue3-recommended ruleset checks many of the criteria above automatically: vue/no-use-v-if-with-v-for, vue/require-v-for-key, vue/no-unused-vars. What the linters cannot check, store architecture, composable cleanup and whether TypeScript typing actually makes sense, remains reserved for human reviewers. A good Vue code review is not a replacement for linters, it is their meaningful complement.
Mironsoft
Vue 3 code quality and team processes
Want to systematize Vue code reviews in your team?
We build review checklists, ESLint configurations and team standards for your Vue 3 project, tailored to your codebase and team experience.
Review checklists
Project specific checklists for components, stores and composables
ESLint setup
eslint-plugin-vue and TypeScript ESLint for automated code quality in CI/CD
Team workshop
Joint review of real PRs with a focus on Vue 3 specific quality patterns
10. Summary
An effective Vue code review systematically checks the areas where Vue 3 projects typically develop quality problems. Reactivity loss through destructuring, missing cleanup in composables and direct state mutation in components are the most common blocker findings. TypeScript any types and misused v-if plus v-for on the same element follow close behind. A review checklist makes these checkpoints consistently applicable regardless of the individual reviewer.
The practical approach: static analysis with eslint-plugin-vue and TypeScript ESLint in the CI pipeline automates the checkable criteria. What linters cannot detect, architecture decisions, store composition, composable design, stays reserved for human review. This keeps review time focused on the findings that genuinely require human judgment, instead of being wasted on mechanically checkable rules.
Vue Code Reviews: the essentials at a glance
Reactivity
Destructuring from stores only with storeToRefs(). Watch/watchEffect with side effects need cleanup functions.
TypeScript
any without justification is a review blocker. Props, emits and return types of composables must be explicitly declared.
Pinia stores
State changes only through actions. Async actions with isLoading and error in store state. Store IDs unique and descriptive.
Template
Never v-if + v-for on the same element. :key always a stable ID, never an array index. Complex expressions in computed properties.