# Render Functions and Component Patterns

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Advanced Components
Source: https://www.devtutorials.ai/learn/vue/advanced-components/render-functions-and-component-patterns

> h() and virtual nodes, props, children, components, slots, lists and conditions in render functions, templates vs render functions and JSX, and component patterns: presentational and container components, controlled components, headless components and generic components with generic="T", and Vue components as web components with defineCustomElement.

## Below the Template

- Vue compiles every template into a **render function**.
- A render function returns **virtual nodes** (VNodes): small JavaScript objects that describe elements.
- You can write render functions yourself with `h()`.
- It is rarely needed. But it explains how Vue works, and helps when a template becomes awkward.

**Simple lesson:** templates turn into render functions that return VNodes. `h()` writes them by hand.

### A Real-World Example

- Meera wants a heading component where the level comes from a prop: `h1`, `h2` or `h3`.
- In a template, that needs three `v-if` branches.
- A render function needs one line: `h("h" + level, text)`.

</script>

<template>
    <AppHeading :level="1">Meera's Kitchen</AppHeading>
    <AppHeading :level="2">Today's menu</AppHeading>
    <AppHeading :level="3">Breakfast</AppHeading>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** one component made `h1`, `h2` and `h3` tags.
- `slots.default?.()` rendered the text between the tags.

### What You Will Learn

1. What `h()` and VNodes are.
2. How to pass props, attributes and children.
3. How to render components and slots.
4. How to render lists and conditions.
5. When to pick a template or a render function, and what JSX is.
6. Presentational and container components.
7. Controlled components.
8. Headless components.
9. Generic components and Vue components as web components.

## h() and VNodes

- `h(type, props, children)` creates a VNode.
- **type** = a tag name like `"button"`, or a component.
- **props** = an object of attributes, props and listeners: `{ class: "big", onClick: fn }`.
- **children** = text, a VNode, or an array of VNodes.
- Listeners are written `onClick`, `onInput`: `on` + the event name with a capital.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="fig-vue-vnode" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-vnode">Animation: a template line becomes an h() call, which becomes a VNode object, which becomes a real button element on the page</title>
  <defs><marker id="vn-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <rect x="10" y="40" width="140" height="70" rx="10" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="80" y="30" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>template</text><text x="80" y="80" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>{"<button>Add</button>"}</text>
    <rect x="170" y="40" width="140" height="70" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="240" y="30" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>render function</text><text x="240" y="80" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>h("button", "Add")</text>
    <rect x="330" y="40" width="150" height="70" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} /><text x="405" y="30" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>VNode</text><text x="405" y="72" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>{"{ type: \"button\","}</text><text x="405" y="90" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>{"children: \"Add\" }"}</text>
    <rect x="500" y="40" width="130" height="70" rx="10" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }} /><text x="565" y="30" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>page</text>
  </g>
  <rect x="535" y="60" width="60" height="30" rx="6" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-green)", strokeWidth: 2 }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.75;0.8;0.95;1" dur="6s" repeatCount="indefinite" /></rect>
  <text x="565" y="80" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Add<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.75;0.8;0.95;1" dur="6s" repeatCount="indefinite" /></text>
  <path d="M150 75 H168 M310 75 H328 M480 75 H498" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <circle r="7" cy="130" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="80;240;405;565;565" keyTimes="0;0.25;0.5;0.75;1" dur="6s" repeatCount="indefinite" /></circle>
  <text x="320" y="170" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>The compiler writes the render function for you. Vue turns VNodes into real elements.</text>
</svg>
<figcaption>Template → render function → VNode → real element.</figcaption>
</figure>

        h("div", { class: "row" }, [
          h("span", { style: { fontWeight: "bold" } }, props.dish),
          " ",
          h(
            "button",
            {
              onClick: () => {
                count.value++;
                emit("ordered", props.dish);
              },
            },
            \`Order (\${count.value})\`,
          ),
        ]);
    },
});
`,
    "App.vue": `<script setup lang="ts">
</script>

<template>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `h(type, props, children)`. Listeners are `onClick`, `onInput` and so on.

`setup()` must return a **function** that returns VNodes: `return () => h(...)`. Returning `h(...)` directly renders once and never updates.

## Components, Slots, Lists and Conditions

- **Component:** pass the imported component as the type: `h(DishCard, { name: "Idli" })`.
- **Slots out:** `slots.default?.()` renders what the parent passed.
- **Slots in:** pass an object of functions: `h(Card, null, { default: () => "Hi", footer: () => "…" })`.
- **Lists:** use `array.map(...)`. Give each VNode a `key`.
- **Conditions:** use `if`, `? :` or `&&` in plain JavaScript.

const vegOnly = ref<boolean>(false);
const dishes = [
    { id: 1, name: "Masala Dosa", veg: true },
    { id: 2, name: "Chicken Biryani", veg: false },
    { id: 3, name: "Idli", veg: true },
];
</script>

<template>
    <label><input type="checkbox" v-model="vegOnly" /> Veg only</label>
      <template #title>Breakfast menu</template>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## Templates or Render Functions?

<div className="tscroll">

| Point | Template | Render function |
|---|---|---|
| Easy to read for HTML people | Yes | Less |
| Compiler speed-ups | Yes: Vue marks what can never change | No |
| Very dynamic output (tag names, many branches) | Clumsy | Easy |
| Used in | Almost all components | Library components, special cases |

</div>

- **JSX** = HTML-like syntax inside JavaScript. With a plugin, Vue can use it instead of `h()`. React uses it by default.
- Choose templates by default. Reach for `h()` only when a template fights you.

Templates are faster than hand-written render functions in most cases. The template compiler adds hints that tell Vue which parts can never change, so updates skip them. You will see this in **How Vue Renders**.

**Simple lesson:** templates by default, render functions for unusual, very dynamic output.

## Component Patterns

### Presentational and Container Components

- **Presentational component** = only shows data from props and emits events. No fetching, no stores.
- **Container component** = loads data or reads stores, then passes it to presentational components.
- Presentational components are easy to reuse and to test.

interface Dish {
    id: number;
    name: string;
    price: number;
}

const dishes = ref<Dish[]>([]);
const loading = ref<boolean>(true);

onMounted(async () => {
    const res = await fetch("https://api.meeraskitchen.dev/dishes");
    dishes.value = (await res.json()) as Dish[];
    loading.value = false;
});
</script>

<template>
    <p v-if="loading">Loading…</p>
</template>
`,
    "DishList.vue": `<script setup lang="ts">

interface Dish {
    id: number;
    name: string;
    price: number;
}

defineProps({ dishes: { type: Array as PropType<Dish[]>, required: true } });
defineEmits(["pick"]);
</script>

<template>
    <ul>
      <li v-for="d in dishes" :key="d.id">
        {{ d.name }} ₹{{ d.price }} <button @click="$emit('pick', d.name)">Pick</button>
      </li>
    </ul>
</template>
`,
    "App.vue": `<script setup lang="ts">
</script>

<template>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Controlled Components

- A **controlled component** does not own its value. The parent does, through `v-model`.
- It shows the value it gets, and asks for changes with an event.
- `defineModel()` from **Props and Events** makes this easy.

const level = defineModel<number>({ required: true });
const labels = ["Mild", "Medium", "Hot"];
</script>

<template>
    <button v-for="(label, i) in labels" :key="label" :class="{ on: level === i }" @click="level = i">{{ label }}</button>
</template>

<style>
.on { background: crimson; color: white; }
</style>
`,
    "App.vue": `<script setup lang="ts">

const spice = ref<number>(1);
</script>

<template>
    <p>The parent owns the value: {{ spice }}</p>
    <button @click="spice = 0">Parent resets to Mild</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Headless Components

- A **headless component** has behaviour but no markup of its own.
- It passes state and functions out through a scoped slot. You saw this as a renderless component in **Slots, Attributes and Provide/Inject**.
- Big UI libraries use it, so you can style everything yourself.
- A composable is often a simpler way to share the same behaviour.

const open = ref<boolean>(false);
const toggle = () => (open.value = !open.value);
const close = () => (open.value = false);
</script>

<template>
    <slot :open="open" :toggle="toggle" :close="close" />
</template>
`,
    "App.vue": `<script setup lang="ts">
</script>

<template>
      <button :aria-expanded="open" @click="toggle">Drinks ▾</button>
      <ul v-if="open" class="drop">
        <li @click="close">Filter coffee</li>
        <li @click="close">Mango lassi</li>
      </ul>
</template>

<style>
.drop { border: 1px solid #ccc; width: 160px; padding: 4px 18px; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## Generic Components

- You built a generic `PickList` in **TypeScript with Vue**. Here it is as a pattern.
- Use it when one component must work with **any** item type: a list, a table, a select box.
- `generic="T"` gives the component a type parameter. Props and events use `T`.
- In the JavaScript flavour, the same component uses plain runtime props.

defineProps<{ items: T[]; label: (item: T) => string }>();
const emit = defineEmits<{ pick: [item: T] }>();
</script>

<template>
    <button v-for="item in items" :key="item.id" @click="emit('pick', item)">{{ label(item) }}</button>
</template>
`,
    "App.vue": `<script setup lang="ts">

interface Table {
    id: number;
    seats: number;
}

const tables: Table[] = [
    { id: 1, seats: 2 },
    { id: 2, seats: 6 },
];
const chosen = ref<Table | null>(null);

function tableLabel(t: Table): string {
    return \`Table \${t.id} (\${t.seats})\`;
}

function choose(t: Table): void {
    chosen.value = t;
}
</script>

<template>
    <p>Chosen: {{ chosen ? chosen.seats + " seats" : "none" }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "GenericPicker.vue": `<script setup>
defineProps({
    items: { type: Array, required: true },
    label: { type: Function, required: true },
});
const emit = defineEmits(["pick"]);
</script>

<template>
    <button v-for="item in items" :key="item.id" @click="emit('pick', item)">{{ label(item) }}</button>
</template>
`,
  }}
/>

Generic components shine in shared libraries: one `DataTable` or `SelectBox` for dishes, orders and staff, all fully typed. In app code, plain props are often enough.

**Simple lesson:** presentational for looks, containers for data, controlled for `v-model`, headless for behaviour, generic for any type.

## Vue Components as Web Components

- A **web component** (custom element) is a tag the browser itself knows, such as `<order-badge>`.
- It works in any page: plain HTML, React, Angular, WordPress. No Vue knowledge needed on that page.
- `defineCustomElement(Component)` turns a Vue component into one. `customElements.define("order-badge", Element)` registers it.
- Props become HTML attributes. Emitted events become DOM `CustomEvent`s.
- The styles go inside the element's **shadow DOM**, so they do not leak out.
- Use it for widgets other teams embed. For a whole Vue app, keep normal components.

const props = defineProps({ dish: { type: String, default: "Dosa" } });
const emit = defineEmits(["ordered"]);
const count = ref<number>(0);

function order(): void {
    count.value++;
    emit("ordered", { dish: props.dish, count: count.value });
}
</script>

<template>
    <button class="badge" @click="order">Order {{ dish }} ({{ count }})</button>
</template>
`,
    "main.ts": `import { defineCustomElement } from "vue";

customElements.define("order-badge", defineCustomElement(OrderBadge));

// From here on, this is plain HTML and DOM code, with no Vue.
const root = document.getElementById("app")!;
root.innerHTML = \`
    <p>A plain HTML page:</p>
    <order-badge dish="Idli"></order-badge>
    <order-badge dish="Vada"></order-badge>
\`;

root.addEventListener("ordered", (e) => {
    const [detail] = (e as CustomEvent).detail;
    console.log(\`ordered event: \${detail.dish} × \${detail.count}\`);
});
`,
  }}
/>

- **What happened:** the page used `<order-badge>` like any HTML tag. Each click sent a DOM event.
- Vue puts the emitted arguments into `event.detail` as an array.

Build a web component library with Vite's library mode. Set `compilerOptions.isCustomElement` when you **use** other custom elements inside Vue templates, so Vue does not try to resolve them as components.

## TL;DR

- **Render function** = a function that returns VNodes. Templates compile into one.
- **VNode** = a plain object that describes an element or component.
- **`h(type, props, children)`** = creates a VNode. Listeners are `onClick`.
- **Slots in `h()`** = out: `slots.default?.()`. In: `{ default: () => ... }`.
- **Lists and conditions** = `map` with `key`, and plain `if` / `? :`.
- **Template vs `h()`** = templates by default. They are also faster.
- **Presentational / container** = shows props / loads data and passes it down.
- **Controlled / headless** = parent owns the value / behaviour without markup.
- **`generic="T"`** = a typed component for any item type (TypeScript).

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `h("button", { click: fn })` | Event names stay as in templates | Write `onClick: fn` | The click does nothing |
| Returning VNodes from `setup` once | `return h(...)` is enough | Return a function: `() => h(...)` | The output never updates |
| No `key` in mapped lists | Lists work without keys | Add `key` to each VNode | Wrong items update and warnings |
| Render functions everywhere | They are always more powerful | Templates by default | Harder code and no compiler speed-ups |
| Presentational component fetching data | Every component can fetch | Fetch in a container, pass props down | Hard to reuse and test |
| `generic` in a JavaScript file | Generics work in JS | Use runtime props in JavaScript | A syntax error |

</div>

## Check Yourself

## Exercises

## What to Remember

- Templates compile into render functions that return VNodes.
- `h(type, props, children)`: listeners are `onX`, lists use `map` with keys.
- Use templates by default. They are clearer and faster.
- Presentational components show. Containers load. Controlled components follow `v-model`.
- Headless components share behaviour. `generic="T"` types reusable components.

Next: **How Vue Renders**
