Code review · Node.js
Review a Vue Comment List
Medium Code review
This Vue 3 single-file component shows the comments on a blog post and lets a moderator hide a comment. Comments are written by site visitors.
Review it: list the problems, why they matter, and how to fix each one.
The code to review
There are 5 issues to find.
<template>
<ul>
<li v-for="comment in comments" v-if="!comment.hidden">
<strong>{{ comment.author }}</strong>
<div v-html="comment.body"></div>
<button @click="hide(comment)">Hide</button>
</li>
</ul>
<p>{{ comments.filter(c => !c.hidden).length }} visible comments</p>
</template>
<script setup>
const props = defineProps({ comments: Array });
function hide(comment) {
comment.hidden = true;
fetch(`/api/comments/${comment.id}/hide`, { method: "POST" });
}
</script>
Write your review
Loading the editor… (JavaScript is required to run code in your browser.)
The issues and how to fix them
Show the issues
1. Stored XSS through v-html
Critical, line 5
v-html inserts the comment as raw HTML. A visitor can post <img src=x onerror=...> and run script in every reader's browser, including moderators, which can steal sessions or act as them.
Fix: Render text with {{ comment.body }} (Vue escapes it). If formatting is needed, convert Markdown on the server and sanitise the HTML with an allow-list (DOMPurify) before using v-html.
2. Mutating a prop
Major, line 16
comment.hidden = true changes an object owned by the parent. It happens to work because objects are passed by reference, but it bypasses one-way data flow, so the parent's state can change without the parent knowing.
Fix: Emit an event (emit('hide', comment.id)) and let the owner update its data, or keep local state.
3. Optimistic update without error handling
Major, line 16-17
The comment disappears even if the request fails (or the user is not authorised), so the moderator thinks it is hidden when it is not.
Fix: Await the request, check response.ok, and roll back or show an error on failure. Also send the CSRF token the backend expects.
4. v-if and v-for on the same element
Major, line 3
In Vue 3, v-if is evaluated before v-for, so comment is undefined in the condition (an error), and the pattern is discouraged in both Vue 2 and 3.
Fix: Filter in a computed property (visibleComments) and loop over that, or move v-if to an inner <template>.
5. Missing :key in v-for
Minor, line 3
Without a key, Vue patches list items in place by position, which can keep the wrong DOM state when comments are hidden or reordered.
Fix: Add :key="comment.id".
Any time user content meets v-html (or React's dangerouslySetInnerHTML, or jQuery's .html()), XSS is the first finding. After that, interviewers look for Vue-specific knowledge: one-way data flow, v-if/v-for precedence, and keys.
Fixed version
<template>
<ul>
<li v-for="comment in visibleComments" :key="comment.id">
<strong>{{ comment.author }}</strong>
<p>{{ comment.body }}</p>
<button :disabled="pending === comment.id" @click="hide(comment.id)">Hide</button>
</li>
</ul>
<p>{{ visibleComments.length }} visible comments</p>
</template>
<script setup>
import { computed, ref } from "vue";
const props = defineProps({ comments: { type: Array, required: true } });
const emit = defineEmits(["hidden"]);
const pending = ref(null);
const visibleComments = computed(() => props.comments.filter((c) => !c.hidden));
async function hide(id) {
pending.value = id;
try {
const r = await fetch(`/api/comments/${id}/hide`, { method: "POST", headers: { "X-CSRF-Token": csrfToken() } });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
emit("hidden", id); // the parent owns the data
} catch (e) {
alert("Could not hide the comment. Please try again.");
} finally {
pending.value = null;
}
}
</script>
Follow-up questions
- What would a Content Security Policy add here?
- How does
computeddiffer from a method call in the template?
More Code Clinic: Code Review problems
- Review a Laravel Orders Controller · Medium
- Review an Express User Search Endpoint · Easy
- Review a Payment Webhook Handler · Medium
- Review a Sign-Up Function · Medium
- Review a CSV Export Endpoint · Medium
- Review a React Search Component · Medium