Slots and Composition
Props carry data into a component. But some components need to receive markup - a Card that frames whatever you put in it, a Modal that wraps any content. And some logic - "track the mouse," "fetch with loading state" - wants to be reused across components that share no markup at all. Vue has one tool for each: slots for markup, composables for logic. Together they're how Vue codebases stay DRY without inheritance trees.
Slots: the component owns the frame, you own the contents
<!-- Card.vue -->
{{ title }}
Nothing here yet. <!-- caller's content lands here; text = fallback -->
<!-- caller -->
Deleting your account is permanent.
Delete
What just happened: everything between <Card> and </Card> replaced the <slot> outlet. The
Card controls structure and styling; the caller controls contents - including live, reactive
contents with their own handlers. Text inside <slot> is the fallback when a caller passes
nothing.
Named slots give a component several outlets:
<!-- PageLayout.vue -->
<!-- the unnamed one is "default" -->
Orders
Order list goes here.
Updated hourly.
The #header syntax (short for v-slot:header) targets a <template> block at a named outlet.
One layout component, every page slotting its parts in - this is the pattern Vue Router's layouts
and every component library's dialogs are built from.
⚠️ Gotcha: slot content is compiled in the parent's scope. Inside
<template #header> you can read the parent's state, but not the child's - a slot is a window
into the child's markup, not its data. When the child must share data back into the slot (a list
component handing each item to caller-provided markup), that's scoped slots:
<slot :item="item" /> in the child, <template #default="{ item }"> in the parent. Worth
recognizing on sight; a follow-up guide gives it a full treatment.
Composables: logic as a function
The Composition API's payoff beyond organization: reactive logic can live in a plain function.
Convention: name starts with use, lives in src/composables/.
// composables/useFetch.js
import from 'vue';
export
<!-- any component -->
Loading…
Couldn't load orders.
{{ o.ref }}
What just happened: the fetch machinery - three refs and their choreography - moved into a function any component can call. Each call creates fresh refs (no shared state between callers, unless you deliberately hoist refs to module scope). The component keeps only what's unique to it: which URL, and what the states look like.
💡 Key point: a composable is not a framework feature - it's a plain function that happens to
create refs and computeds. That's the Composition API's whole design: because reactivity lives in
importable functions (ref, computed, watch) rather than in component options, your logic
can be an importable function too. Notice the destructuring is safe here: data and loading are
refs - containers, per phase 3 - so handing them out preserves reactivity.
provide / inject: skipping the middle layers
Props work level to level. For values needed across many levels - theme, current user, locale - threading a prop through five components that don't use it gets old. A parent can provide; any descendant can inject:
// App.vue (or any ancestor)
import from 'vue';
const theme = ;
; // provide the ref itself, not theme.value
// any component, any depth below
import from 'vue';
const theme = ; // second arg: default if nobody provided
What just happened: the descendant received the same ref the ancestor provided - reactive, so
theme changes propagate to every injector. Provide the ref, not its unwrapped value: provide('theme', theme.value) hands out a frozen string, and phase 3 told you why that's dead on arrival.
The judgment call is the same as any broadcast mechanism: provide/inject shines for
widely-read, rarely-changed app context, and turns into hide-and-seek if you route everyday
parent-child data through it. Props are greppable; injections need the reader to know the key
exists. Default to props; inject for genuine cross-cutting context.
Recap
- Slots receive markup: default slot for one outlet, named slots (
#header) for several, fallback content inside<slot>. - Slot content sees the parent's scope; scoped slots are the child-hands-data-back variant.
- Composables = reactive logic in plain
use*functions; fresh refs per call, importable anywhere, safely destructurable because refs are containers. provide/injectcarries reactive context past intermediate layers - provide the ref itself.- Props for everyday flow; injection for app-wide context; slots whenever a component should frame content it doesn't own.
[
{
"q": "Inside <template #header> passed to a layout component, you reference one of the layout's internal refs and get undefined. Why?",
"choices": [
"Named slot templates can only contain static HTML",
"Slot content compiles in the parent's scope - the child's data isn't visible unless the child exposes it via a scoped slot",
"The ref needed .value in the template",
"provide/inject is required to use data inside slots"
],
"answer": 1,
"why": [
"Slot templates are fully dynamic - handlers, bindings, all of it; the constraint is whose data they see.",
null,
"Templates auto-unwrap refs - and no unwrapping trick grants access to another component's scope.",
"Injection shares app context; the standard channel for child-to-slot data is the scoped slot."
],
"explain": "A slot is the caller's markup shown inside the child's frame - it evaluates against the caller's scope. Children share data into slots explicitly: <slot :item=\"item\">."
},
{
"q": "Two components both call useFetch(). Do they share the data ref?",
"choices": [
"Yes - composables create module-level state shared by all callers",
"No - each call runs the function fresh and creates its own refs",
"Only if both components are children of the same parent",
"Yes, unless the composable is marked as scoped"
],
"answer": 1,
"why": [
"Refs created inside the function body are per-call; sharing happens only when refs are deliberately hoisted to module scope outside the function.",
null,
"The component tree has no bearing on it - function-call semantics do.",
"There's no scoped marker - the function's own structure decides what's shared."
],
"explain": "A composable is a plain function. Each call executes its body and makes fresh refs - isolation by default, shared state only if you hoist refs outside the function on purpose."
}
]
← Phase 4: Components: Props, Events, and v-model · Guide overview · Phase 6: Watchers, Lifecycle, and Fetching →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. Inside <template #header> passed to a layout component, you reference one of the layout's internal refs and get undefined. Why?
2. Two components both call useFetch(). Do they share the data ref?