Reviewing AI-generated code
Assignment
The team at the counter needs an overview of the loans that are overdue.
As the team at the counter, I want to see the overdue loans and remind members of them, so that the things come back on time.
The new volunteer asked an AI assistant for the components. The code looks reasonable, but the lists stay empty, and the console reports errors. The acceptance criteria are:
- The code follows Vue 3 with
<script setup>, without Vue 2 patterns. - No component changes a prop, and none loses the connection to its state.
- A checklist and tests catch mistakes like these before the code reaches
main.
Learning goals
After this chapter you can
- spot typical mistakes in suggested Vue code and find their source in the documentation,
- translate Vue 2 patterns into Vue 3,
- find and fix lost reactivity and changed props,
- write lists with keys and without
v-ifnext tov-for, - use a checklist, tests and a linter as a safety net.
Reviewing code you didn't write
Section titled “Reviewing code you didn't write”Chapter 0.1 noted that assistants often write patterns from older versions of Vue. You're responsible for what you commit, no matter who wrote it. So read suggested code as you would in a review, line by line, and look up every pattern you don't know in the documentation. In the project, the AI declaration records what you checked and changed, and in the screencast you show a place where a suggestion was wrong.
Vue 2 patterns
Section titled “Vue 2 patterns”According to vuejs.org, Vue 2 reached its end of life on December 31, 2023. The migration guide lists what changed in Vue 3. Instead of new Vue, createApp starts an app, and global functions like Vue.component hang off the app. Filters are gone, and a function or a computed value takes over their job. $on, $off and $once were removed, as were the modifier .native and $listeners, and Vue.set is no longer needed.
Some of this doesn't stand out right away. A filter like {{ loan.until | date }} compiles without an error in Vue 3, because Vue reads the expression as JavaScript, where | is a bitwise OR. The page then shows a number instead of a date.
The Options API in script setup
Section titled “The Options API in script setup”Code in <script setup> runs like the body of setup(), and according to the Vue documentation there's no this there. A suggested this.loans in a computed value therefore fails at runtime. Fields like data, methods or computed in a second <script> belong to the Options API and don't fit <script setup>. Nobody needs to import defineProps and defineEmits. Whoever does gets a warning while compiling that defineProps is a compiler macro. Props without types, such as defineProps(['loans']), are only fit for prototypes according to the style guide.
Vuex and Vue CLI
Section titled “Vuex and Vue CLI”For shared state, the Vue documentation recommends Pinia, and Vuex is in maintenance mode. A suggestion with new Vuex.Store, commit and mapState therefore belongs in a store with defineStore, as in chapter 7.2. Vue CLI is in maintenance mode as well, and a new project starts with create-vue and Vite. Instructions with vue create or npm run serve come from that time.
Lost reactivity
Section titled “Lost reactivity”Whoever takes a reactive object or a store apart directly gets copies of the values. The page then shows the state at the moment of taking it apart and follows no change anymore, as chapters 5.1 and 7.2 showed.
<script setup>import { reactive } from 'vue'
const state = reactive({ count: 0 })// Taking the object apart copies the number, the copy no longer follows the stateconst { count } = state
function add() { state.count++}</script>
<template> <button type="button" @click="add">Count up</button> <p>state.count: {{ state.count }}</p> <p>count: {{ count }}</p></template>toRefs keeps the connection for a reactive object, storeToRefs for a store. Since Vue 3.5 you may destructure props, but according to the Vue documentation watch needs a destructured prop as a function, such as watch(() => loans, …).
Changed props
Section titled “Changed props”According to the Vue documentation, data only flow from the parent to the child. When a child changes its prop, Vue warns in the console, and the change has no effect.
App.vue
<script setup>import { ref } from 'vue'
import Counter from './Counter.vue'
const count = ref(0)</script>
<template> <p>Counted: {{ count }}</p> <Counter :count="count" /></template>Counter.vue
<script setup>const props = defineProps({ count: { type: Number, required: true } })
function add() { props.count++}</script>
<template> <button type="button" @click="add">Count up</button></template>Instead, the child reports an event, and the parent changes its state. If the child only needs a starting value, it makes a copy of its own with ref.
Lists with keys
Section titled “Lists with keys”The style guide asks for a key with every v-for. Without a key, or with the index as the key, Vue reuses the elements by their position. When an entry disappears, the state inside the elements, such as typed text, moves on to the next entry.
<script setup>import { ref } from 'vue'
const notes = ref([ { id: 1, title: 'Return the deposit' }, { id: 2, title: 'Dry the tent' }, { id: 3, title: 'Reserve the benches' },])
function remove(id) { notes.value = notes.value.filter((note) => note.id !== id)}</script>
<template> <ul> <li v-for="(note, index) in notes" :key="index"> <label :for="`note-${note.id}`">{{ note.title }}</label> <input :id="`note-${note.id}`" type="text"> <button type="button" @click="remove(note.id)">Remove</button> </li> </ul></template>According to the style guide, v-if and v-for never belong on the same element. In Vue 3, Vue evaluates v-if first, when the loop variable doesn't exist yet. A computed value filters the list beforehand, or a <template v-for> wraps the element with v-if.
A checklist
Section titled “A checklist”These questions help with every suggested piece of Vue code.
- Does the code run with Vue 3 and
<script setup>, withoutthis, filters,$onandnew Vue? - Do props have types, and does no component change its props?
- Do values from stores and reactive objects stay connected?
- Does every
v-forhave a uniquekey, and is there nov-ifon the same element? - Does the code use Pinia, Vue Router and Vite instead of Vuex, Vue CLI and older functions?
- Does the page show loading, errors and empty lists when data come from a server?
- Is there a test for what the code should do, and does it fail when the code is wrong?
Tests and a linter as a safety net
Section titled “Tests and a linter as a safety net”Tests check what the code should do, and chapter 8.4 shows how to check whether they notice a mistake. A linter finds some mistakes while you type. With the option "Linter", create-vue sets up ESLint with eslint-plugin-vue, as chapter 3.2 showed. Its rules for Vue 3 report, among other things, changed props with vue/no-mutating-props, missing keys with vue/require-v-for-key, v-if next to v-for with vue/no-use-v-if-with-v-for and Vue 2 patterns with rules like vue/no-deprecated-filter. It only catches lost reactivity with rules that aren't part of the presets, and only a test knows whether a list sorts correctly.
Giving the assistant context
Section titled “Giving the assistant context”An assistant doesn't know your project. Tell it the versions, such as Vue 3 with <script setup>, Pinia and Vue Router, and how your project is organized, and ask it to explain what its suggestion does. Whatever you can't explain, you don't take over. Passwords, keys and personal data never go into a request to an assistant.
In your project
Section titled “In your project”The AI declaration in your README states for every tool what you used it for, which parts are affected and what you checked or changed in the result. While you work, note the places where a suggestion was wrong, together with the commit that fixed it. The screencast asks about one of them.
In code
Section titled “In code”Work with the examples of this chapter. Read the code and write down what you expect before you change anything.
- In the example with the changed prop, click "Count up". What does the console report, and what does the number show? Rewrite the child so that it reports an event.
- In the example without keys, type some text into the second row and remove the first. Where is your text now? Change the key to
note.idand repeat it. - In the example with the lost reactivity, click several times. Which number follows? Replace the destructuring with
toRefs(state). - In the exercise project of this chapter, run the tests before you change anything. Which messages point to Vue 2 patterns?
Exercises
Section titled “Exercises”The exercises fix the code the assistant suggested for the team. You'll find the project in the folder exercises/08-architecture/ai-review/en of the course repo. Copy the folder, install the packages and start the tests.
npm installnpm testExercise Getting started
OverdueList.vue mixes <script setup> with this and a Vue 2 filter, and its props have no types. Rewrite the component in Vue 3. It shows the loans whose day is before today, the oldest first, with a date as usual in the language of the page.
Solution
<script setup>import { computed } from 'vue'
const props = defineProps({ loans: { type: Array, required: true }, today: { type: String, required: true },})
// The loans whose day has passed, the oldest first. There is no this in <script setup>.const overdue = computed(() => props.loans.filter((loan) => loan.until < props.today).toSorted((a, b) => a.until.localeCompare(b.until)),)
// Vue 3 has no filters, a function does the same jobfunction formatDate(value) { return new Date(value).toLocaleDateString('en-US', { timeZone: 'UTC' })}</script>
<template> <h2>Overdue loans</h2> <ul class="overdue"> <li v-for="loan in overdue" :key="loan.id">{{ loan.name }}, due since {{ formatDate(loan.until) }}</li> </ul></template>Exercise Going further
The team page takes the store apart directly and therefore shows no loans. LoanRow.vue changes its prop reminded instead of reporting the click on "Remind". Fix both, so that the store records who was reminded.
Solution
const loansStore = useLoansStore()// storeToRefs keeps the connection to the store, actions come straight from the storeconst { loans, reminded } = storeToRefs(loansStore)const today = new Date().toISOString().slice(0, 10)
loansStore.load()<script setup>defineProps({ loan: { type: Object, required: true }, reminded: { type: Boolean, default: false },})
// A prop belongs to the parent, the row only reports the clickconst emit = defineEmits(['remind'])</script>
<template> <li> {{ loan.name }} <span v-if="reminded" class="reminded">reminded</span> <button type="button" @click="emit('remind', loan.id)">Remind</button> </li></template>Exercise Challenge
NoteList.vue has v-if and v-for on the same element, no key, and it changes the prop notes when removing a note. Show only the open notes, give every row a unique key and leave the prop unchanged.
Solution
<script setup>import { computed, ref } from 'vue'
const props = defineProps({ notes: { type: Array, required: true } })
// A copy of the first state, the list changes here and leaves the prop aloneconst list = ref(props.notes.map((note) => ({ ...note })))
// v-if and v-for never on the same element, a computed value filters firstconst openNotes = computed(() => list.value.filter((note) => !note.done))
function remove(id) { list.value = list.value.filter((note) => note.id !== id)}</script>
<template> <h2>Notes</h2> <ul class="notes"> <!-- The id as key keeps every field with its note when another one disappears --> <li v-for="note in openNotes" :key="note.id"> <label :for="`note-${note.id}`">{{ note.title }}</label> <input :id="`note-${note.id}`" type="text"> <button type="button" @click="remove(note.id)">Remove</button> </li> </ul></template>Common mistakes
Section titled “Common mistakes”| Mistake | What helps |
|---|---|
A computed value in <script setup> reads this and fails. | Take the props from defineProps and read props.loans. |
An interpolation with | shows a number instead of text. | Vue 3 has no filters anymore. Call a function. |
| A list from a store stays empty although the store has loaded. | Take the store apart with storeToRefs or read through the store. |
| The console reports that a target is readonly. | A component changes its prop. Report an event. |
| Typed text jumps into another row after removing one. | Use a unique key, not the index. |
Instructions start with vue create or install Vuex. | Use create-vue and Pinia. |
Self-check
Section titled “Self-check”Self-check
- How do you recognize code that comes from Vue 2?
- Why does a list from a store that was taken apart directly stay empty?
- What happens when a list uses the index as its key and an entry disappears?
- Which mistakes does a linter find, and which only a test?
Answers
- By
new Vue,thisin<script setup>, filters,$on,.native,Vue.setor by Vuex and Vue CLI. - Taking it apart copies the values. When the store later replaces its list, the page still shows the old one.
- Vue reuses the elements by their position, and their state ends up with another entry.
- The linter finds patterns like changed props, missing keys and Vue 2 functions. Only a test shows whether the app does the right thing.
Assignment
The team sees the overdue loans and reminds the members of them. With that, the app is complete for the course. The competence grid shows you where you stand after part 8, and your own project applies the chapters of this part.
Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.