Updated Jul 18, 2026

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 -->
<script setup>
defineProps({ title: String });
</script>

<template>
  <section class="card">
    <h2>{{ title }}</h2>
    <slot>Nothing here yet.</slot>   <!-- caller's content lands here; text = fallback -->
  </section>
</template>
<!-- caller -->
<Card title="Danger zone">
  <p>Deleting your account is permanent.</p>
  <button @click="confirmDelete">Delete</button>
</Card>

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 -->
<template>
  <header><slot name="header" /></header>
  <main><slot /></main>                    <!-- the unnamed one is "default" -->
  <footer><slot name="footer" /></footer>
</template>
<PageLayout>
  <template #header><h1>Orders</h1></template>
  Order list goes here.
  <template #footer><small>Updated hourly.</small></template>
</PageLayout>

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 { ref } from 'vue';

export function useFetch(getUrl) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(false);

  async function load() {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(getUrl());
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      data.value = await res.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

  return { data, error, loading, load };
}
<!-- any component -->
<script setup>
import { useFetch } from '@/composables/useFetch';
const { data: orders, loading, error, load } = useFetch(() => '/api/orders');
load();
</script>

<template>
  <p v-if="loading">Loading…</p>
  <p v-else-if="error">Couldn't load orders.</p>
  <ul v-else><li v-for="o in orders" :key="o.id">{{ o.ref }}</li></ul>
</template>

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 { provide, ref } from 'vue';
const theme = ref('dark');
provide('theme', theme);       // provide the ref itself, not theme.value
// any component, any depth below
import { inject } from 'vue';
const theme = inject('theme', ref('light'));   // 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

  1. Slots receive markup: default slot for one outlet, named slots (#header) for several, fallback content inside <slot>.
  2. Slot content sees the parent's scope; scoped slots are the child-hands-data-back variant.
  3. Composables = reactive logic in plain use* functions; fresh refs per call, importable anywhere, safely destructurable because refs are containers.
  4. provide/inject carries reactive context past intermediate layers - provide the ref itself.
  5. 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?