Thuta Learning
ExercisesWeb Developmentbeginner

Practice: Components, Router & State Challenge

Relax. We'll talk through this in plain words — no textbook voice.

What you'll walk away with

  • Practice the Components, Router & State Challenge hands-on
  • Reinforce the skills you've already learned through practice
  • Get comfortable finding bugs, fixing them, and checking your own work

Hold on, let's think about it this way

This lesson is about combining everything you learned in the Intermediate/Advanced chapter — component communication, composables, routing, and state management — all at once. The tasks are designed to mirror scenarios you'd actually run into in a real production app. We're not teaching new concepts here; we're testing whether you can combine what you already know. If you finish Task 1 before moving on to Tasks 2 and 3, you'll see how the logic connects more clearly.

Exercises

Task 1: Pass an 'items' array from the parent component down to a child list component as props, and when the delete button in the child is clicked, emit('delete', id) to send the event back to the parent to delete the item. Task 2: Write a composable called useCounter() that returns count, increment, and decrement, then import it into two components at the same time and prove that each component's state stays independent. Task 3: Build a Pinia store called 'useCartStore' with an addItem action and a total price getter, then use Vue Router to navigate between two routes ('/cart' and '/checkout') and verify that the store data doesn't disappear.

Code Example

javascript
// useCounter.js (Task 2)
import { ref } from 'vue'

export function useCounter(start = 0) {
  const count = ref(start)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

// stores/cart.js (Task 3)
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  getters: {
    total: (state) => state.items.reduce((sum, i) => sum + i.price, 0)
  },
  actions: {
    addItem(item) {
      this.items.push(item)
    }
  }
})

// ListItem.vue child (Task 1)
// props: { item: Object }
// emits: ['delete']
// template button: @click="$emit('delete', item.id)"
You should see
Clicking the delete button in the child removes the item from the parent's items array; the counters in each component using the composable count independently without affecting each other; and even after navigating from the cart page to the checkout page, the cart total price stays intact.

Try It in 5 Minutes

Create two components for Task 2 (CounterA.vue, CounterB.vue), import useCounter() into both, and use console.log to prove within 5 minutes that clicking the button in one doesn't affect the other's state.

A Word of Caution

If you want to destructure state from a Pinia store, you must use storeToRefs() — otherwise the reactivity link breaks and the UI stops updating.

Easy traps

  • Mutating a prop (item) directly from the child component, causing it to fall out of sync with the parent's state
  • Destructuring a Pinia store's reactive object directly in a component (const { items } = store), which breaks reactivity

Now Try It Yourself

Create two components for Task 2 (CounterA.vue, CounterB.vue), import useCounter() into both, and use console.log to prove within 5 minutes that clicking the button in one doesn't affect the other's state.

You'll know it worked when: Clicking the delete button in the child removes the item from the parent's items array; the counters in each component using the composable count independently without affecting each other; and even after navigating from the cart page to the checkout page, the cart total price stays intact.

Practice: Components, Router & State Challenge | Thuta Learning