Vue
A Complete 12-Chapter Course
Table of Contents
- What Vue Is, the SFC, Vite Setup
- Template, Interpolation, and ref Reactivity
- Directives โ v-bind, v-on, v-model
- Conditional and List Rendering โ v-if, v-for
- Computed Properties and Watchers
- Components and Props
- Emitting Events and v-model on Components
- Slots
- Lifecycle Hooks
- Composables
- Vue Router
- State Management with Pinia, and Data Fetching
What Vue Is, the SFC, Vite Setup
With both React and Angular already under your belt, Vue is best understood by where it lands between them. Like React, it's component-based, flexible, and unopinionated about much of your app's structure. Like Angular, it uses an HTML template with directives (v-if, v-for) rather than JSX, and ships official solutions for routing and state. Vue's own pitch is being approachable and incrementally adoptable โ and in practice it feels like Angular's template ergonomics with React's lightness.
The Single-File Component (SFC)
A Vue component lives in a .vue file โ a Single-File Component โ with three blocks: <script> for logic, <template> for markup, and an optional <style> for CSS, all in one file. This is a genuine middle ground: React puts markup and logic together in JSX but CSS elsewhere; Angular splits all three into separate files; Vue keeps all three together but cleanly separated by block. {{ name }} is text interpolation โ identical syntax to Angular, and the same role as React's {name}.
<script setup> and the Composition API
The setup attribute on <script> enables the Composition API โ the modern way to write Vue, where everything declared in the script block (variables, functions, imports) is automatically available to the template. There's no return statement, no boilerplate wiring: declare const name = 'Philip' and the template can use {{ name }} directly. This course uses <script setup> throughout.
data(), methods, computed, etc. as separate named sections. It's still fully supported in Vue 3, but <script setup> with the Composition API is the recommended modern default โ the same kind of "modern vs legacy" choice as standalone components over NgModules in Angular, or hooks over class components in React. Recognize the Options API when you meet it; this course teaches only the Composition API.
Style Scoping โ Built In
The scoped attribute on <style> confines those styles to this component only โ the same automatic style encapsulation Angular gives every component, and what React needs CSS Modules or a library to achieve. Two components can both style h1 differently with zero collision, just by adding scoped.
Creating a Project with Vite
npm create vue@latest runs Vue's official scaffolding tool (built on Vite โ the same build tool used throughout the React course), asking a few setup questions (TypeScript, router, Pinia โ all optional and addable later). npm run dev starts the dev server with live reload, exactly as it did for React. The key generated files:
src/main.jsโ the entry point; mounts the rootAppcomponent onto the page.src/App.vueโ the root SFC, the first component you'll edit.index.htmlโ the single real HTML page everything mounts into, just like React's.
Mounting the Root Component
createApp(App).mount('#app') is Vue's bootstrap โ it takes the root component and attaches it to the <div id="app"> in index.html, the direct equivalent of React's createRoot(...).render(<App />) or Angular's bootstrapApplication(AppComponent). Everything else renders inside that root.
| Concept | React | Angular | Vue |
|---|---|---|---|
| Markup style | JSX | HTML template + directives | HTML template + directives |
| Component file | .jsx | .ts+.html+.css | .vue (all three blocks) |
| Interpolation | {value} | {{ value }} | {{ value }} |
| Scoped styles | CSS Modules / lib | Automatic | <style scoped> |
Coding Challenges
Create a new Vue project, and edit App.vue so its template shows "Welcome, [your name]!" by interpolating a constant declared in <script setup>.
๐ View solutionBuild a Greeting.vue SFC with all three blocks โ a name constant in script, an interpolated heading in the template, and a scoped style coloring that heading.
๐ View solutionBuild a second component (e.g. Footer.vue) and use it inside App.vue by importing it in <script setup> and placing its tag in the template โ confirming a child component renders inside a parent.
๐ View solutionChapter 1 Quick Reference
- Vue โ component-based and flexible like React, template+directive-driven like Angular
- SFC (.vue file) โ
<script>,<template>,<style>blocks together in one file - <script setup> โ the Composition API; everything declared is auto-available to the template
- {{ value }} โ text interpolation, identical to Angular
- <style scoped> โ automatic per-component style encapsulation
- npm create vue@latest / npm run dev โ scaffold and run (Vite-based)
- createApp(App).mount('#app') โ bootstraps the root component
- The older Options API appears in legacy code; this course uses
<script setup>throughout - Next chapter: the template, interpolation, and
refreactivity basics
Template, Interpolation, and ref Reactivity
Chapter 1's interpolated values were constants that never changed. The moment a value needs to change and have the UI update, it must be reactive โ Vue's tracking system that re-renders the template when a value changes. In the Composition API, the core tool for that is ref.
ref โ A Reactive Value
ref(0) creates a reactive container holding 0. The catch that trips up everyone at first: in the <script>, you read and write the value through .value (count.value++) โ but in the <template>, Vue unwraps it automatically, so it's just {{ count }}, no .value. This is Vue's equivalent of React's useState, but with two key differences: there's no separate setter function (you assign to .value directly), and the same ref is used both to read and to update.
<script setup>, always use count.value. Inside <template>, always use plain count โ Vue unwraps it for you there. Forgetting .value in the script (writing count++) silently fails to update anything; adding .value in the template ({{ count.value }}) shows undefined.
Why Not a Plain Variable?
A plain let count = 0 increments fine in memory, but Vue isn't tracking it โ nothing tells the framework the UI needs re-rendering, so the displayed number never changes. Exactly the same trap as a plain variable in React (Fundamentals Chapter 3). ref is what makes the value reactive; the change to .value is what Vue detects.
reactive โ For Objects
reactive is an alternative for objects, making the whole object reactive with no .value โ properties are accessed directly (state.age++). In practice, many Vue developers use ref for everything (it works for objects too, via ref({...}).value) to avoid juggling two mental models, but reactive is common and worth recognizing. The trade-off: reactive only works on objects/arrays, and you lose reactivity if you destructure it.
Expressions in the Template
Interpolation isn't limited to a bare value โ any single JavaScript expression works inside {{ }}: arithmetic, method calls, ternaries. The same as React's curly braces, and like Angular's interpolation, it's expressions only (no statements like if or for โ those have their own directives, next chapter).
| React useState | Vue ref | |
|---|---|---|
| Create | const [c, setC] = useState(0) | const c = ref(0) |
| Read (script) | c | c.value |
| Read (markup) | {c} | {{ c }} |
| Update | setC(c + 1) | c.value++ |
Coding Challenges
Build a counter with a ref starting at 0 and a +1 button that increments it (remembering .value in the function), displaying the count via interpolation.
๐ View solutionBuild a component with a reactive object (via reactive) holding firstName and lastName, plus a button that changes one of them, displaying "Full name: [first] [last]" with both updating live.
๐ View solutionBuild a component with a price ref and a quantity ref, displaying their product directly in the template as a {{ }} expression (price * quantity), with buttons to change each โ confirming the total recomputes automatically.
๐ View solutionChapter 2 Quick Reference
- ref(value) โ a reactive value; Vue's equivalent of
useState - Read/write with .value in
<script>; use the plain name (auto-unwrapped) in<template> - No separate setter โ assign to
.valuedirectly (count.value++) - A plain variable won't trigger UI updates โ only a
ref(orreactive) is tracked - reactive({...}) โ makes an object reactive with no
.value; objects/arrays only - {{ }} accepts any single JS expression (math, method calls, ternaries)
- Next chapter: directives โ
v-bind,v-on,v-model
Directives โ v-bind, v-on, v-model
Interpolation (Chapter 2) only handles text content between tags. For everything else โ setting an attribute, responding to an event, syncing a form input โ Vue uses directives: special v- prefixed attributes, the same conceptual mechanism as Angular's bindings, just with different syntax. These three are the workhorses.
v-bind โ Binding an Attribute
v-bind binds an attribute to a reactive expression rather than a static string โ :disabled="isSaving" sets the real boolean disabled property from the isSaving ref. The : shorthand is near-universal in real code. This is the exact equivalent of Angular's [disabled]="isSaving" (square brackets) and serves the same purpose as React's disabled={isSaving} โ passing a real value, not a string.
v-on โ Listening for Events
v-on attaches an event listener โ already used informally in earlier chapters via its @ shorthand. @click="increment" is Vue's version of Angular's (click)="increment()" and React's onClick={increment}. Two conveniences: you can write a method name (@click="increment") or an inline expression (@click="count++"), and the native event object is available as $event when you need it (@input="onInput($event)").
@submit.prevent="onSubmit" calls event.preventDefault() for you (no manual call like React's Fundamentals Chapter 4 needed), @click.stop stops propagation, @keyup.enter fires only on the Enter key. These small declarative shortcuts replace boilerplate you'd write by hand in React.
v-model โ Two-Way Binding
v-model keeps a form input and a ref synchronized in both directions automatically โ typing updates name, and changing name in code updates the input. This is the same end result as Angular's [(ngModel)], and it collapses React's manual controlled-input pattern (value + onChange, Fundamentals Chapter 7) into a single directive. Under the hood it's just :value + @input combined โ a shorthand Vue provides, and one you'll build yourself on a custom component in Chapter 7.
v-model on Other Input Types
v-model automatically adapts to the input type โ a checkbox binds to a boolean, a <select> binds to the chosen option's value, radio buttons to the selected value, all with the same v-model attribute. This is noticeably less fiddly than React, where a checkbox needed checked/e.target.checked while a text input needed value/e.target.value (Fundamentals Chapter 7) โ Vue figures out the right property and event for you.
[(ngModel)] required importing FormsModule into the component, Vue's v-model is built into the template compiler and works out of the box โ no import, no setup. One fewer thing to forget.
| Vue | Angular | React |
|---|---|---|
| :attr="x" | [attr]="x" | attr={x} |
| @event="handler" | (event)="handler()" | onEvent={handler} |
| v-model="x" | [(ngModel)]="x" | value={x} onChange={...} |
| @submit.prevent | manual preventDefault() | manual preventDefault() |
Coding Challenges
Build a component with an isLocked ref, a Save button whose disabled attribute is v-bind-bound to it, and a second button (@click) that toggles isLocked. Use the shorthand syntaxes (: and @).
๐ View solutionBuild a single-field form using @submit.prevent on the form and v-model on a text input, logging the value on submit and clearing the input afterward โ with no manual preventDefault call.
๐ View solutionBuild a component with a text input, a checkbox, and a select dropdown, each v-model-bound to its own ref (string, boolean, string), displaying all three current values live below the form.
๐ View solutionChapter 3 Quick Reference
- v-bind:attr (shorthand :attr) โ bind an attribute to a reactive expression (= Angular
[attr]) - v-on:event (shorthand @event) โ listen for an event (= Angular
(event)/ ReactonEvent) - $event โ the native event object, passed explicitly when needed
- Modifiers:
@submit.prevent,@click.stop,@keyup.enterโ declarative shortcuts - v-model โ two-way binding on a form input (= Angular
[(ngModel)]), no import needed v-modelauto-adapts to input type (text/checkbox/select/radio) โ simpler than React's per-type handling- Next chapter: conditional and list rendering โ
v-ifandv-for
Conditional and List Rendering โ v-if, v-for
React handles conditionals and lists with plain JavaScript in JSX (ternaries, &&, .map()); Angular uses @if/@for blocks. Vue uses directives on the element itself โ v-if and v-for โ closer to Angular's older *ngIf/*ngFor style, but with their own conveniences.
v-if / v-else-if / v-else
v-if conditionally renders an element based on a truthy expression, with optional v-else-if and v-else chained on adjacent elements. Like Angular's @if (and unlike a CSS display: none), a false v-if genuinely removes the element from the DOM entirely. The full v-if/v-else-if/v-else chain neatly handles the loading/error/success pattern that ran through the React projects, without a lookup object or chained ternaries.
v-show โ Toggle Visibility, Keep It in the DOM
v-show looks similar but works differently: the element stays in the DOM and is toggled with CSS display. Use v-if when the element is rarely shown or expensive to create (it's truly added/removed); use v-show when toggling frequently (e.g. a dropdown), since flipping a CSS property is cheaper than repeatedly creating and destroying the element. This explicit choice is something React and Angular handle more implicitly โ Vue names it directly.
v-for โ Rendering a List
v-for="item in items" repeats the element once per array entry โ Vue's .map() equivalent. The :key binding is required (using v-bind from Chapter 3) and serves the identical purpose as React's key and Angular's track: a stable, unique identifier so Vue can update the DOM efficiently when the list changes. A real object array keys on its id:
v-for with an Index
A second variable in parentheses gives the current index โ (todo, index) in todos โ useful for numbering. Note the index is the position, not a good :key on its own (same caution as React's array index): prefer a stable id for the key whenever one exists.
v-for in a <template v-if>. A common clean pattern is a computed visibleTodos that returns only the items to show, then v-for over that โ exactly the "filter before mapping" approach from React's Fundamentals Chapter 6.
The <template> Wrapper
To repeat or conditionally render multiple elements without adding a wrapper element to the DOM, put the directive on a <template> tag โ an invisible grouping element that renders only its contents. This is Vue's equivalent of React's fragment (<>...</>), used here specifically to attach a directive to a group.
| Vue | Angular | React |
|---|---|---|
| v-if / v-else-if / v-else | @if / @else if / @else | Ternary / && |
| v-show | [hidden] / [style.display] | Conditional class/style |
| v-for + :key | @for + track | .map() + key |
| <template> wrapper | <ng-container> | Fragment <>...</> |
Coding Challenges
Build a component with a status ref ("loading"/"error"/"success") cycled by a button, using v-if/v-else-if/v-else to show different content for each status.
๐ View solutionGiven an array of objects ({ id, name }) in a ref, render them as a list with v-for and :key, plus a numbered version using the (item, index) form showing "1. Name", "2. Name", etc.
๐ View solutionBuild a component with a list of items and a "Show details" toggle button. Use v-show to reveal a details panel (kept in the DOM), and separately use v-if elsewhere to show a "No items" message when the array is empty.
๐ View solutionChapter 4 Quick Reference
- v-if / v-else-if / v-else โ conditional rendering; truly adds/removes the element from the DOM
- v-show โ toggles CSS
display, keeping the element in the DOM (cheaper for frequent toggles) - v-for="item in items" โ list rendering; a required :key (= React's
key/ Angular'strack) - (item, index) in items โ also exposes the index; don't use the index as the key if a stable id exists
- Don't combine
v-ifandv-foron one element โ filter via a computed property instead - <template> with a directive โ group multiple elements without an extra DOM node (= React fragment)
- Next chapter: computed properties and watchers
Computed Properties and Watchers
Chapter 2 put a {{ price * quantity }} expression directly in the template. That works, but repeating it or running heavier logic inline gets messy fast. Computed properties are Vue's clean way to derive a reactive value from other reactive values; watchers are for running a side effect when something changes. Both will feel familiar from React's useMemo and useEffect โ but with Vue's automatic dependency tracking.
computed โ A Derived Reactive Value
computed(() => ...) returns a ref-like value that automatically recalculates whenever any reactive value it reads changes โ here, whenever price or quantity changes. Two important properties: it's cached (only recomputes when a dependency actually changes, not on every render), and its dependencies are auto-tracked โ there's no dependency array to maintain, unlike React's useMemo. Vue knows total depends on price and quantity simply because the function read them. In the template it's used as {{ total }} (auto-unwrapped, no .value); in script you'd read total.value.
computed() signal felt clean, this is the same idea โ a derived value that tracks its own dependencies and caches. Vue's computed predates Angular's by years; the convergence is no accident, as both frameworks moved toward fine-grained reactivity. Coming from React, think "useMemo with no dependency array, and you read it like a value."
Computed vs a Method
You could call a regular method in the template instead โ but a method re-runs every time the component re-renders for any reason, even if its inputs didn't change. A computed caches its result and only recomputes when a dependency genuinely changes. For anything beyond a trivial expression, prefer computed โ the caching is free performance.
Filtering a List with computed
This is the clean fix for Chapter 4's "don't combine v-if and v-for" rule: a computed property returns exactly the items that should be shown, and the template does v-for="todo in visibleTodos". The filter recomputes automatically when either todos or showCompleted changes โ the same "filter before mapping" pattern React used in Fundamentals Chapter 6, here expressed reactively.
watch โ Running a Side Effect on Change
watch runs a callback whenever a specific reactive source changes, receiving both the new and old value. It's for genuine side effects in response to a change โ fetching data, saving to localStorage, logging โ the role React's useEffect with a dependency plays. The key distinction from computed: computed derives and returns a value; watch does something and returns nothing. If you find yourself assigning to another ref inside a watch, a computed is usually the better tool.
watchEffect โ Auto-Tracked Watching
watchEffect is a variant that runs immediately and re-runs whenever any reactive value it reads changes โ automatically tracked, no explicit source to name. It's closer in feel to React's useEffect with auto-detected dependencies. Use watch when you need the old value or want to watch one specific source; use watchEffect when you just want "re-run this whenever anything it uses changes."
computed in disguise, and the computed version is simpler, cached, and less bug-prone. Reserve watch/watchEffect for true side effects (I/O, logging, imperative DOM work). The mental rule: "deriving a value โ computed; doing something โ watch."
| Vue | React | Angular |
|---|---|---|
| computed(() => ...) | useMemo (no dep array) | computed() signal |
| watch(src, cb) | useEffect with a dep | effect() / RxJS watch |
| watchEffect(cb) | useEffect (auto deps) | effect() signal |
Coding Challenges
Build a component with firstName and lastName refs and a fullName computed property combining them, with inputs to edit each โ confirming fullName updates automatically.
๐ View solutionBuild a todo list with a "show completed" checkbox, using a computed visibleTodos that filters based on the checkbox, then v-for over visibleTodos โ the clean fix for not combining v-if and v-for.
๐ View solutionBuild a component with a count ref and a watch that logs the new and old value whenever count changes, plus a watchEffect that logs a message referencing count โ observing how the two differ (watchEffect runs immediately, watch does not).
๐ View solutionChapter 5 Quick Reference
- computed(() => ...) โ a cached, auto-tracked derived value (=
useMemowith no dep array) - Prefer
computedover a method for derived values โ it caches; a method re-runs every render - A
computedfilter is the clean fix for "don't combine v-if and v-for" (Chapter 4) - watch(source, (new, old) => ...) โ runs a side effect on change, with old/new values
- watchEffect(() => ...) โ runs immediately and re-runs on any read dependency's change
- Rule of thumb: deriving a value โ computed; doing something โ watch
- Next chapter: components and props
Components and Props
Chapter 1's child components were static. Props make a component reusable by letting a parent pass data into it โ the same concept as React props and Angular @Input. In <script setup>, props are declared with the compiler macro defineProps.
Declaring Props with defineProps
defineProps(['name', 'age']) declares the props this component accepts. It's a compiler macro โ no import needed; Vue's compiler recognizes it inside <script setup>. In the template, props are used directly by name ({{ name }}), just like a local ref. In the script, they're accessed via the returned object (props.name).
Passing Props from a Parent
Import the child, then place its tag with the props as attributes. The crucial detail, carried over from v-bind in Chapter 3: name="Philip" (no colon) passes the literal string "Philip", but :age="35" (with the colon) evaluates as an expression and passes the real number 35. Forgetting the colon on a number/boolean/array prop passes a string instead โ the same string-vs-expression distinction as React's quotes-vs-curly-braces and Angular's plain-vs-bracket attributes.
Typed Props with Defaults and Validation
The object form of defineProps adds type checking, a required flag, and default values โ Vue warns in the console if a required prop is missing or the wrong type is passed. This is richer than React's plain props (which need PropTypes or TypeScript for the same), and comparable to Angular's typed @Input. Defaults work like Angular's, supplying a fallback when the parent omits the prop entirely.
Passing Different Data Types
Anything that isn't a plain string needs the : binding. Note also the naming convention: a prop declared as inStock (camelCase in JS) is passed as :in-stock (kebab-case in the template) โ Vue maps between them automatically, the standard HTML-attribute convention. You can use camelCase in the template too, but kebab-case is the common style.
computed, or a ref initialized from the prop); to actually change the parent's data, emit an event โ covered next chapter. Mutating an object/array prop's contents in place also works technically but is discouraged for the same reason: the parent loses control of its own data.
| Vue | React | Angular |
|---|---|---|
| defineProps(['name']) | Function params { name } | @Input() name |
| name="x" (string) | name="x" | name="x" |
| :age="35" (real value) | age={35} | [age]="35" |
| { type, required, default } | PropTypes / TS + defaults | Typed input + default |
Coding Challenges
Build a MovieCard component with title and year props (declared via defineProps), rendering "Title (Year)". Use it three times in a parent with three different movies, passing year as a real number.
๐ View solutionBuild a Badge component using the object form of defineProps with a text prop (String, required) and a color prop (String, default "gray"). Render it once with a custom color and once without, confirming the default applies.
๐ View solutionBuild a ProductCard component receiving title (String), price (Number), and tags (Array) props, rendering the title, price, and the tags as a list. Pass tags as a real array using the : binding.
๐ View solutionChapter 6 Quick Reference
- defineProps([...]) โ declares a component's props; a compiler macro, no import needed
- Props used directly by name in the template; via
props.namein the script - name="x" passes a string; :prop="expr" passes a real value (number/boolean/array/object)
- Object form adds type, required, and default โ richer than plain React props
- camelCase prop โ kebab-case attribute (
inStockโ:in-stock) mapped automatically - Props are read-only โ flow one way; emit an event to change parent data (next chapter)
- Next chapter: emitting events with defineEmits, and v-model on components
Emitting Events and v-model on Components
Props (Chapter 6) flow data down into a child. To send something back up โ a click, a deletion, a new value โ a child emits an event that the parent listens for. This is Vue's counterpart to React's "pass a callback down" and Angular's @Output/EventEmitter. Declared with the defineEmits macro.
Emitting an Event with defineEmits
defineEmits(['delete']) declares which events this component can emit and returns an emit function. Calling emit('delete', id) fires the delete event with id as its payload. The parent listens with the @ event syntax from Chapter 3 โ @delete="removeTodo" โ and removeTodo receives the emitted id as its argument. Structurally identical to Angular's @Output, and the same role as a React callback prop, but modeled as a custom event the child raises (like Angular, unlike React).
@delete โ the same syntax as @click โ custom events are conventionally named as plain events (delete, save, change), not onDelete. From the parent's perspective <TodoItem @delete="..." /> reads just like listening to a built-in element's event. (This mirrors Angular's @Output naming convention exactly.)
Multiple Events and Validated Emits
A component can declare several events in the array. The object form additionally lets you validate each event's payload with a function (Vue warns if it returns false) โ useful documentation of what each event carries, comparable to typing an Angular EventEmitter<T>.
v-model on a Component
Chapter 3 used v-model on a native <input>. You can also put v-model on a custom component, to build something like a reusable input control with two-way binding. Under the hood, v-model on a component is shorthand for a modelValue prop plus an update:modelValue event:
The child takes the current value via the modelValue prop, binds it to its real input with :value, and emits update:modelValue whenever the input changes. The parent then uses plain v-model="name" โ Vue wires the prop and event together automatically. This is the exact convention Angular uses for custom two-way binding (value + valueChange), and it's why v-model on native inputs "just works": those are built on the same mechanism.
defineModel(), which collapses the whole modelValue-prop-plus-event pattern above into a single line: const model = defineModel(), then bind v-model="model" on the inner input. It's the recommended modern approach for new code โ worth knowing the longer form above too, since it's what defineModel expands to and what you'll see in slightly older code.
v-model component, the child must not reassign modelValue directly โ it emits update:modelValue and lets the parent update the source of truth, which flows back down as the new prop. The one-way-data-flow discipline from Chapter 6 holds: data down via props, changes up via events.
| Vue | Angular | React |
|---|---|---|
| defineEmits(['delete']) | @Output() delete | A callback prop (onDelete) |
| emit('delete', id) | this.delete.emit(id) | onDelete(id) |
| @delete="handler" | (delete)="handler($event)" | onDelete={handler} |
| v-model on component | [(value)] (value + valueChange) | value prop + onChange prop |
Coding Challenges
Build a TodoItem component with id/text props and a delete emit. The parent holds an array of todos, renders one TodoItem each, and removes the matching todo when a delete event fires.
๐ View solutionBuild a CustomInput component supporting v-model via a modelValue prop and an update:modelValue emit, then use v-model on it in a parent and display the bound value live.
๐ View solutionRewrite Challenge 2's CustomInput using the newer defineModel() shorthand, confirming the parent's v-model usage is unchanged.
๐ View solutionChapter 7 Quick Reference
- defineEmits([...]) โ declares emittable events; returns an
emitfunction - emit('event', payload) โ fires an event upward; parent listens with
@event="handler" - Name events plainly (
delete,save), notonXโ they read like native events - v-model on a component = a
modelValueprop + anupdate:modelValueevent - defineModel() โ the modern one-line shorthand for that whole pattern (Vue 3.4+)
- The child never mutates the prop โ it emits, and the parent updates the source of truth
- Next chapter: slots โ Vue's content-projection / composition mechanism
Slots
Props pass data into a child. Slots pass markup โ letting a parent drop arbitrary template content inside a child component. This is Vue's direct equivalent of React's children (Fundamentals Chapter 9) and Angular's <ng-content>, and it's how you build flexible wrappers like cards, modals, and layouts that don't need to know what's placed inside them.
The Default Slot
The <slot></slot> element is a placeholder โ whatever the parent writes between the component's opening and closing tags gets rendered there. Card doesn't know or care it's rendering an <h2> and a <p>; it just wraps whatever it's given. This is exactly React's {children}, just declared with a <slot> tag in the template rather than read as a prop.
Fallback (Default) Content
Content placed inside the <slot> tag is fallback content โ shown only when the parent passes nothing. A small convenience React lacks built-in (you'd write {children ?? 'fallback'} by hand); in Vue it's just whatever you put between the slot tags.
Named Slots โ Multiple Insertion Points
When a component needs several insertion points, give each <slot> a name. The parent targets a named slot with <template #header> (the # is shorthand for v-slot:header); anything not wrapped in a named template goes into the default slot. This is cleaner than React's approach for multi-region layouts, where you'd pass JSX through several separate named props (the SplitLayout pattern from React Fundamentals Chapter 9) โ Vue keeps it all between one set of component tags.
Scoped Slots โ Passing Data Back to the Slot Content
A scoped slot lets the child pass data up to the markup the parent provides โ the child binds values onto its <slot> (:todo="todo"), and the parent receives them by destructuring in v-slot (#default="{ todo }"). This is the powerful case: TodoList owns the looping and data, but the parent decides exactly how each item looks. It's the direct equivalent of React's render-props pattern (Advanced Chapter 5) โ "the component owns behavior, the caller controls rendering" โ but expressed declaratively in the template.
children for simple content, named props for multi-region layouts, and the render-props pattern for "expose data to the caller's markup." Vue's slot system handles all three with one consistent mechanism: default slot, named slots, and scoped slots. If render props felt clunky in React, scoped slots are the cleaner, template-native version of the same idea.
| Vue | React | Angular |
|---|---|---|
| <slot> (default) | {children} | <ng-content> |
| Named slots (#header) | Named JSX props | <ng-content select="..."> |
| Scoped slots | Render props | Structural directive / template context |
| Fallback in <slot> | Manual children ?? fallback | Default ng-content content |
Coding Challenges
Build a Card component with a default slot wrapped in a styled div, then use it twice in a parent with completely different inner markup (a heading vs an image + caption). Add fallback slot text shown when no content is passed.
๐ View solutionBuild a Panel component with named header and footer slots plus a default slot for the body, and fill all three from a parent using <template #header> / default content / <template #footer>.
๐ View solutionBuild a List component that takes an items array prop, loops over it, and exposes each item to a scoped slot (:item="item"). In the parent, use v-slot to render each item your own way (e.g. bold name + muted id).
๐ View solutionChapter 8 Quick Reference
- <slot></slot> โ placeholder for parent-provided markup (= React's
children) - Content inside the
<slot>tag is fallback, shown when the parent passes nothing - Named slots (
<slot name="header">) โ multiple insertion points; parent targets with#header - Scoped slots โ child binds data onto
<slot :x="x">; parent receives via#default="{ x }"(= render props) - Rule of thumb: props for values, slots for markup
- One slot mechanism covers what React split across children, named props, and render props
- Next chapter: lifecycle hooks โ onMounted, onUnmounted, and friends
Lifecycle Hooks
A component is created, mounted to the DOM, updates as its data changes, and is eventually unmounted. Lifecycle hooks let you run code at these moments โ most often to do setup work after mounting (like fetching data) and cleanup before unmounting (like clearing a timer). In the Composition API, these are functions you call inside <script setup>.
onMounted โ After the Component Is in the DOM
onMounted registers a callback that runs once, right after the component is inserted into the DOM โ the standard place for an initial data fetch, the equivalent of React's useEffect(() => {...}, []) and Angular's ngOnInit. By this point the real DOM exists, so it's also safe to access template elements (via template refs) here. Note you can pass an async function directly โ Vue doesn't have React's restriction about effect callbacks not being async.
ngOnInit are distinct), much of Vue's setup just lives at the top level of <script setup> โ refs, computeds, and functions declared there run once when the component is created, before it mounts. You only reach for onMounted specifically when you need the DOM to exist first (or want to defer work until after the first render).
onUnmounted โ Cleanup Before Removal
onUnmounted runs just before the component is destroyed โ the place for cleanup, mirroring React's useEffect cleanup function and Angular's ngOnDestroy. The same universal rule applies: anything that "starts" something ongoing (a timer, an event listener, a subscription) needs a matching "stop" here, or it leaks. A setInterval not cleared in onUnmounted keeps firing after the component is gone.
onUpdated โ After a Re-render
onUpdated runs after the component re-renders due to a reactive change and the DOM has been patched. It's used less often than onMounted/onUnmounted โ for most "react to a value changing" needs, a watch (Chapter 5) targeting the specific value is cleaner and more precise than this catch-all "something updated" hook.
The Common Pattern โ Mount + Cleanup Together
Setup in onMounted paired with matching teardown in onUnmounted is the everyday pattern โ adding then removing an event listener, starting then stopping a subscription. In Chapter 10 you'll see how a composable bundles this paired logic into one reusable function, so the mount/unmount pairing lives in one place rather than scattered across every component that needs it โ Vue's equivalent of a React custom hook with a cleanup return.
onMounted must be registered synchronously at the top level of <script setup> โ not inside an if, a callback, or after an await. This is the same rule-of-hooks constraint as React: Vue needs to associate the hook with the current component instance at setup time, which only works if it's called during the synchronous setup run. Calling onMounted inside an async function after an await, for instance, silently fails to register.
| Vue | React | Angular |
|---|---|---|
| top of <script setup> | component function body | constructor |
| onMounted | useEffect(..., []) | ngOnInit |
| onUpdated | useEffect (no dep array) | ngOnChanges / ngDoCheck |
| onUnmounted | useEffect cleanup | ngOnDestroy |
Coding Challenges
Build a component that fetches a list from any free public API in onMounted and stores it in a ref for display, with a loading state shown until the data arrives.
๐ View solutionBuild a Clock component that starts a setInterval in onMounted (updating a time ref every second) and clears it in onUnmounted. Toggle the component with v-if in a parent to confirm the ticking stops when removed.
๐ View solutionBuild a component that tracks the window's width: add a resize event listener in onMounted (updating a width ref), and remove it in onUnmounted. Display the live width.
๐ View solutionChapter 9 Quick Reference
- Refs/computeds/functions at the top of
<script setup>run at creation, before mount - onMounted โ runs once after the component is in the DOM (=
useEffect(.., [])/ngOnInit) - onUnmounted โ runs before removal; the place for cleanup (= cleanup fn /
ngOnDestroy) - onUpdated โ after a re-render; usually a precise
watchis better - Pair setup in
onMountedwith teardown inonUnmounted(listeners, timers, subscriptions) - Register hooks synchronously in setup โ not after an
awaitor inside a conditional - Next chapter: composables โ bundling reusable stateful logic (Vue's custom hooks)
Composables
A composable is a function that uses Vue's reactivity (ref, computed, lifecycle hooks) to package up reusable, stateful logic โ exactly what React custom hooks do, and what Vue calls its own version of the pattern. There's no special syntax: it's just a plain function, by convention named useSomething, that calls reactivity APIs inside and returns reactive values.
A First Composable โ useCounter
useCounter bundles a ref and its behaviour into one reusable function, returning the reactive count and the functions to change it. Any component imports it and destructures what it needs โ and crucially, each call creates its own independent state, just like calling a React hook. This maps directly onto React's useToggle from Intermediate Chapter 4; the only real syntax difference is returning an object (so you destructure by name) rather than a tuple.
A Composable with Lifecycle โ useMousePosition
This is the real payoff teased in Chapter 9: the onMounted/onUnmounted listener-pairing โ the kind of thing you'd otherwise repeat in every component needing mouse position โ lives in one composable. A composable can call lifecycle hooks itself, and Vue ties them to whichever component is using it. Any component just does const { x, y } = useMousePosition() and gets a self-cleaning, reactive position โ the setup and teardown handled automatically.
useLocalStorage โ A Persisted ref
The same useLocalStorage that was a React custom hook (Intermediate Chapter 4) and an Angular signal+effect (Angular Chapter 13) โ here, a composable combining a ref initialized from storage with a watch that re-saves on every change. const notes = useLocalStorage('notes', []) returns a normal-looking ref that transparently persists. The { deep: true } option makes the watch fire on nested changes inside objects/arrays, not just reassignment.
mixins: option in old code, a composable is the modern replacement.
refs themselves (or a reactive object), not ref.value. Returning count.value hands back a plain number โ a one-time snapshot that won't stay reactive in the component. Return count (the ref) so the component keeps the live, reactive connection. This is the composable equivalent of the same care React takes to return state and its setter, not a stale value.
| Vue composable | React custom hook | Angular equivalent |
|---|---|---|
| useCounter() | useToggle() / custom hook | Injectable service |
| Returns an object (destructure) | Returns a tuple/object | Class instance |
| Can call onMounted/onUnmounted | Can call useEffect | Lifecycle in the service host |
| Each call = isolated state | Each call = isolated state | One shared instance (singleton) |
Coding Challenges
Write a useCounter composable returning { count, increment, decrement, reset }, and use it in two separate components โ confirming each gets its own independent count.
๐ View solutionWrite a useWindowWidth composable that tracks window.innerWidth via a resize listener added in onMounted and removed in onUnmounted, returning the reactive width โ then use it in a component, confirming the listener cleans up when the component is removed.
๐ View solutionWrite a useLocalStorage composable that returns a ref initialized from localStorage and persists it on change via a watch, then use it to persist a counter's value so it survives a page refresh.
๐ View solutionChapter 10 Quick Reference
- A composable is a function (conventionally
useX) packaging reusable, stateful reactive logic โ Vue's custom hook - No special syntax โ call
ref/computed/lifecycle hooks inside, return reactive values - Returns an object (destructure by name); each call gets isolated state
- A composable can call
onMounted/onUnmountedโ bundling setup+teardown in one place - Return the refs, not
ref.value, to keep the reactive connection live - Composables replace Vue 2 mixins, for the same reasons hooks replaced HOCs/mixins in React
- Next chapter: Vue Router โ multiple pages in a single-page app
Vue Router
Routing in Vue is its own official package (vue-router), like React Router was a separate install โ but the concepts are now thoroughly familiar from both the React and Angular courses. Define routes mapping paths to components, render the matched one in an outlet, link without reloading, read params, navigate from code, and guard routes. Same ideas, Vue's API.
Defining Routes
Routes are an array of { path, component } objects, the same shape as Angular's route config and React Router's <Route> list. :id is a dynamic segment; /:pathMatch(.*)* is Vue Router's (slightly cryptic) catch-all for unmatched URLs. createWebHistory() enables clean URLs (no #). The router is then registered in main.js with app.use(router) โ the same plugin pattern Pinia will use next chapter.
RouterView and RouterLink
<RouterView /> is the outlet where the matched route's component renders โ the equivalent of React Router's <Outlet /> and Angular's <router-outlet>. <RouterLink to="/about"> navigates without a full page reload, like React's <Link> and Angular's routerLink. A RouterLink automatically gets an active CSS class when its route is current, so highlighting the active nav item needs no manual URL comparison.
Reading Route Params with useRoute
useRoute() returns the current route object; route.params.id reads the :id segment โ the equivalent of React Router's useParams() and Angular's ActivatedRoute.paramMap. The route object is reactive, so if the same component stays mounted while only the param changes (navigating /product/1 โ /product/2), a watch (Chapter 5) on route.params.id reacts to that change.
Programmatic Navigation with useRouter
useRouter() returns the router instance; router.push(...) navigates from code โ after a form submits, a login succeeds, an action completes โ the equivalent of React Router's useNavigate() and Angular's Router.navigate(). Note the two distinct hooks: useRoute (singular, the current route's data) vs useRouter (the router, for navigating) โ an easy pair to mix up by name.
Navigation Guards
A navigation guard runs before each route change โ beforeEach receives the target (to) and current (from) routes, and either allows navigation (return true or nothing) or redirects (return a path). This is Vue's equivalent of Angular's canActivate guard, and the standard way to keep unauthenticated users out of protected pages. Guards can also be defined per-route (beforeEnter) or inside a component.
loadComponent, Vue Router supports lazy-loading a route's component so its code only downloads when the route is first visited: { path: '/admin', component: () => import('../views/Admin.vue') }. A dynamic import() as the component is all it takes โ no separate Suspense boundary needed (the React equivalent from Advanced Chapter 3). This is the standard way to keep the initial bundle small.
<a href="/about"> instead of <RouterLink to="/about"> triggers a real full-page reload, throwing away all in-memory state โ the same warning as React Router and Angular. RouterLink intercepts the click and updates the URL via the History API without a reload. Always use it for in-app navigation.
| Vue Router | React Router | Angular |
|---|---|---|
| routes array + createRouter | <Routes>/<Route> | routes + provideRouter |
| <RouterView /> | <Outlet /> | <router-outlet> |
| <RouterLink to> | <Link to> | routerLink |
| useRoute().params | useParams() | ActivatedRoute.paramMap |
| useRouter().push() | useNavigate() | Router.navigate() |
| beforeEach guard | Hand-rolled wrapper | canActivate |
Coding Challenges
Set up a 3-page app (Home, About, Contact) with a routes array, a nav using RouterLink, a RouterView, and a catch-all route showing a NotFound component for unmatched URLs.
๐ View solutionAdd a /product/:id route. From a product list, use RouterLink to navigate to a detail page that reads the id via useRoute and displays the matching product.
๐ View solutionAdd a beforeEach navigation guard protecting a /dashboard route, redirecting to /login when a simple isLoggedIn flag is false, with a button toggling the flag to test both outcomes.
๐ View solutionChapter 11 Quick Reference
- createRouter + a routes array (path โ component); register with
app.use(router) - <RouterView /> renders the matched route; <RouterLink to> navigates without reload
- :id dynamic segments; /:pathMatch(.*)* is the catch-all route
- useRoute() โ current route data (
route.params.id); useRouter() โ navigate (router.push) - beforeEach((to, from) => ...) โ a navigation guard; return a path to redirect, true/nothing to allow
- Lazy-load a route with
component: () => import('...')โ built-in code splitting - Always use
RouterLink, never a plain<a href>, for in-app navigation - Next chapter: state management with Pinia, plus a data-fetching pattern (final chapter)
State Management with Pinia, and Data Fetching
For state shared across many distant components, Vue's official answer is Pinia. Like Zustand and Redux for React, or a service for Angular, it holds state outside the component tree so any component can reach it โ but a Pinia store is built from the very same reactivity primitives you already know (ref, computed), making it feel like a composable that happens to be global.
Defining a Store
defineStore('counter', () => {...}) โ the "setup store" form โ is essentially a composable: declare refs (the state), computeds (derived values, called "getters"), and functions (the "actions") inside, and return them. The first argument is a unique store id. If this looks almost identical to a composable from Chapter 10, that's the point โ the difference is that a Pinia store is a single shared instance, not fresh state per call.
Setup, and Using the Store
Register Pinia once with app.use(createPinia()) (the same plugin pattern as the router). Then any component calls useCounterStore() and accesses store.count, store.double, store.increment โ and two completely unrelated components calling it share the exact same state, no props or events between them. This is the same problem Context/Zustand solved in React and a service solved in Angular, here with Vue's own reactivity underneath.
const { count } = useCounterStore() breaks reactivity โ count becomes a disconnected snapshot (the same reactive-destructuring trap from Chapter 2). To pull out reactive state, use storeToRefs: const { count, double } = storeToRefs(store). Actions (functions) can be destructured normally โ only state and getters need storeToRefs.
Data Fetching Inside a Store
An action can be async โ so a store is a natural home for API calls, exactly as a service was in Angular (Chapter 11). The status ref drives loading/error/success rendering, the same single-status pattern used across the React projects. A component calls store.fetchUsers() in onMounted and reads store.users/store.status in the template โ and because the result lives in the store, several components share the fetched data without re-requesting it, the same benefit React Query gave (React Advanced Chapter 2).
refs in module scope (outside the function) โ and for small cases that works. Pinia adds what a real app wants on top: a single well-defined store id, devtools integration (time-travel, inspecting state), server-side-rendering support, and plugins. Reach for a store when state is genuinely global and app-significant; a plain composable stays fine for localized reusable logic.
| Vue (Pinia) | React | Angular |
|---|---|---|
| defineStore + useXStore() | Zustand create() / Context | Injectable service |
| state = ref(...) | store state | service properties |
| getters = computed(...) | derived/selectors | getters / computed |
| actions = functions | store actions | service methods |
| async action for fetching | React Query / thunk | service + HttpClient |
Coding Challenges
Set up Pinia and define a counter store (state: count; getter: double; actions: increment, reset). Use it from two unrelated components, confirming both share the same count with nothing passed between them.
๐ View solutionBuild a cart store (state: items; action addToCart with the duplicate-quantity check; getter totalItems) and use it from a product list and a separate cart-display component. Use storeToRefs to read items reactively.
๐ View solutionBuild a users store with an async fetchUsers action and a status ref (idle/loading/error/success). A component calls fetchUsers in onMounted and renders loading/error/success states from the store.
๐ View solutionChapter 12 Quick Reference
- Pinia โ Vue's official global state; a store is essentially a shared, named composable
- defineStore('id', () => {...}) โ state (
ref), getters (computed), actions (functions), then return them - Register once with
app.use(createPinia()); use viauseXStore()in any component - Two components using the same store share one instance โ no props, no events
- storeToRefs(store) โ destructure state/getters reactively (actions destructure normally)
- An async action makes a store a natural home for API calls + loading/error/success state
- Use a store for genuinely global state; a plain composable for localized reusable logic