Vue Fast Track for Angular Developers

· 7 min read ·
Vue Fast Track for Angular Developers

I already know components, reactivity, routing, and state, so this maps what I know onto Vue syntax and hopefully gets me building something real.

Mental model — Angular vs Vue

Angular Vue
@Component decorator <script setup> in .vue file
@Input() / @Output() defineProps() / defineEmits()
*ngIf / *ngFor v-if / v-for
[prop] / (event) :prop / @event
ng-content <slot>
Signal / Observable ref() / reactive()
Pure pipe computed()
Service (singleton) Composable (useX function) or Pinia store
RouterModule Vue Router (same API shape)
Angular CLI / Webpack Vite (faster, simpler config)
NgRx Pinia (much lighter)
ngOnInit / ngOnDestroy onMounted / onUnmounted

Week 1 — Orientation

Week 1 Orientation — Angular to Vue mapping

Topics

  • Composition API vs Options API — start with Composition, it maps closer to Angular’s mental model
  • ref() and reactive() — think signals/observables but simpler. Mutate directly, no .next()
  • computed() — same as a pure pipe, derived value with no side effects
  • Template syntax: v-if, v-for, v-bind (:), v-on (@)
  • Single File Components: HTML + script + style in one .vue file, no separate class file
  • Read: vuejs.org/guide/introduction through “Reactivity Fundamentals”

Exercise

Counter + todo list Build two components: a counter and a todo list. No libraries, no routing. Just ref(), computed(), and template directives.

  • Scaffold a new project: npm create vue@latest
  • Counter: increment, decrement, reset buttons. Display count and a computed “positive/negative/zero” label
  • Todo list: add items, mark complete, filter by status using v-if and computed()
  • Style with scoped <style scoped> — note how it’s contained to the component
  • Stretch: add a “clear completed” button and an item count badge
Goal: get one component working end-to-end

The big shift from Angular: reactivity in Vue is implicit. You don’t subscribe or call markForCheck(). Wrap a value in ref(), mutate it, and the template just updates. Trust it.

Counter.vue
<script setup>
import {ref, computed} from 'vue'
const count = ref(0)
const label = computed(() => {
if (count.value > 0) return 'positive'
if (count.value < 0) return 'negative'
return 'zero'
})
</script>
<template>
<div>
<p>{{count}} — {{label}}</p>
<button @click="count++">+</button>
<button @click="count--">-</button>
<button @click="count = 0">reset</button>
</div>
</template>

Week 2 — Components and state

Week 2 Components and state

Topics

  • Props: defineProps() replaces @Input(). Type-safe, same concept.
  • Emits: defineEmits() replaces @Output(). No EventEmitter, just strings.
  • Slots: <slot > replaces ng - content. Named slots replace select.
  • Lifecycle: onMounted, onUnmounted, onUpdated — same ideas, different names
  • Pinia for shared state — much lighter than NgRx. A store is just a function.
  • Read: Pinia quickstart docs (15 min, genuinely short)

Exercise

Parent/child score tracker

Build a score tracker with a parent component that owns state and two child components that read and update it.

  • Parent holds a scores array in a Pinia store
  • Child PlayerCard receives player name + score via props, displays them
  • Child emits an increment event; parent updates the store
  • Add a slot to PlayerCard for optional extra content (badge, note, etc.)
  • Stretch: add a “reset all” button in the parent that clears the Pinia store
Goal: parent/child wiring with shared state

Pinia replaces both Angular services and NgRx in one concept. Define a store with defineStore(), use it anywhere with useMyStore(). No reducers, no actions, no boilerplate. It feels almost too simple at first.

PlayerCard.vue
<script setup >
const props = defineProps < {name: string; score: number} > ();
const emit = defineEmits < {increment: []} > ();
</script>
<template>
<div>
<h3>{{props.name}}</h3>
<p>{{props.score}}</p>
<slot/>
<!-- ng-content equivalent -->
<button @click="emit('increment')">+1 </button>
</div>
</template>

Week 3 — Routing and app structure

Week 3 Routing and real app structure

Topics

  • Vue Router: router - link = routerLink. Route guards work identically to Angular’s CanActivate.
  • Composables — Vue’s answer to Angular services. Extract logic into useX() functions, import anywhere.
  • HTTP: no HttpClient. Use fetch or Axios. Wrap in a composable to keep components clean.
  • Vite — the build tool replacing Angular CLI. Config is a single vite.config.ts.
  • Dynamic route params: useRoute().params.id replaces ActivatedRoute

Exercise

Two-page app with API data Build a minimal two-page app: a list view and a detail view. Fetch data from a public API.

  • Use https://jsonplaceholder.typicode.com/posts as your data source
  • Page 1: list of post titles, each links to the detail page
  • Page 2: detail view at/posts/:id, fetches single post on mount
  • Extract the fetch logic into ausePosts() composable
  • Add a route guard that redirects to home if the post ID is not a number
  • Stretch: add a loading state and a basic error state
Goal: multi-page app with routing + API call

Composables are the thing that clicks everything together in Vue. If you’re pulling logic into Angular services, the instinct translates directly — just write a function that starts with “use” and returns reactive state. No class, no DI token, no providedIn.

usePosts.ts
// composable — think: Angular service
import {ref} from 'vue'
export function usePosts() {
const posts = ref([])
const loading = ref(false)
async function fetchAll() {
loading.value = true
posts.value = await fetch('/api/posts').then(r => r.json())
loading.value = false
}
return {posts, loading, fetchAll}
}
PostList.vue
<script setup>
import {onMounted} from 'vue'
import {usePosts} from './usePosts'
const {posts, loading, fetchAll} = usePosts()
onMounted(fetchAll)
</script>

Week 4 — Build something real

Week 4 Build something real

Topics

  • <script setup> syntax — what you’ll see in every modern Vue codebase. Get comfortable here.
  • v-model on custom components: replaces Angular’s ControlValueAccessor pattern, much less painful
  • Teleport: renders DOM somewhere else in the tree (modals, tooltips). Same idea as Angular CDK Portal.
  • Nuxt — Vue’s Next.js equivalent. Worth a skim if the role involves SSR.
  • Read through a real Vue codebase on GitHub to see patterns in the wild

Exercise

Port a ResuRank view to Vue Take one view from ResuRank or JobDash and rebuild it in Vue. Pick something with a form, a list, and some state logic.

  • Recreate the job input form using v-model for two-way binding
  • Display results in a list with computed filtering (filter by score threshold)
  • Use a Pinia store to hold the results list
  • Add a modal using <Teleport to="body"> for viewing a result detail
  • Stretch: add a simple route for /results and /results/:id
Goal: something you can show in an interview

Porting a feature you already built in Angular is the fastest way to cement the Vue mental model. You already know what the code needs to do — the exercise is just translation. After this week you’ll stop thinking “how do I do X in Vue” and start just writing it.

ScoreInput.vue
<!-- v-model replaces ControlValueAccessor — Vue 3.4+ -->
<script setup>
const model = defineModel
<string>()
</script>
<template>
<input :value="model" @input="model = $event.target.value"/>
</template>
Parent.vue
<!-- same as Angular [(ngModel)] -->
<ScoreInput v-model="query"/>

Resources

Share this post