Home
Blogs
Series
Components
Advanced Patterns in Vue 3: The Power of Renderless Components

Advanced Patterns in Vue 3: The Power of Renderless Components

Discover how Renderless Components in Vue 3 can improve code reuse and the separation of logic and presentation in your applications.

The Renderless Pattern in Vue 3: Architecture and Separation of Concerns

In the Vue 3 ecosystem, code reuse has evolved significantly thanks to the Composition API. However, a recurring challenge still remains: How do you share complex UI logic without enforcing a specific design or HTML structure? That’s where the Renderless Components pattern becomes an essential tool for software architecture.

What is a Renderless Component?

A renderless component (one that doesn’t render) is a component that doesn’t generate any markup or CSS of its own. Its only responsibility is to encapsulate state and behavioral logic and expose that data to the parent component. The parent, in turn, decides exactly which DOM elements to use and how to style them.

This pattern is the backbone of “Headless UI” libraries, allowing a component’s logic—such as managing a modal, a dropdown, or a form—to be universal, while the design stays 100% customizable.

The Engine: Scoped Slots

This pattern is implemented using Scoped Slots. Unlike a regular slot, a scoped slot allows the child component to send data “up” to the parent’s template at runtime.

Practical Example: A Visibility Controller

Imagine a component that manages an “open/closed” state—something essential in menus and modals.

<script setup>
  import { ref } from 'vue';

  const isOpen = ref(false);
  const toggle = () => {
    isOpen.value = !isOpen.value;
  };

  // Expose the state and method to the slot
</script>
 
<template>
  <slot :isOpen="isOpen" :toggle="toggle"></slot>
</template>
<script>
  export default {
    data() {
      return {
        isOpen: false
      };
    },
    methods: {
      toggle() {
        this.isOpen = !this.isOpen;
      }
    }
  };
  // Expose the state and method to the slot
</script>
 
<template>
  <slot :isOpen="isOpen" :toggle="toggle"></slot>
</template>

Implementation in the Parent When consuming this component, you get complete creative freedom:

ParentComponent.vue
<template>
  <LogicToggle v-slot="{ isOpen, toggle }">
    <button @click="toggle">
      {{ isOpen ? 'Close Menu' : 'Open Menu' }}
    </button>
    <div v-if="isOpen" class="custom-dropdown">
      Dynamic content goes here
    </div>
  </LogicToggle>
</template>

Senior Optimization: Render Functions (h)

To reach a production-grade level—especially in libraries distributed via NPM—it’s recommended to avoid the <template> block. By using a render function and the h (hyperscript) method, we eliminate template compilation overhead and avoid creating unnecessary extra DOM nodes.

LogicToggle.js
import { ref, h } from 'vue';
 
export default {
  setup(props, { slots }) {
    const isOpen = ref(false);
    const toggle = () => (isOpen.value = !isOpen.value);
 
    return () => {
      // Return the default slot while passing the state
      return slots.default ? slots.default({
        isOpen: isOpen.value,
        toggle
      }) : null;
    };
  }
};

This approach lets the component behave as a pure data “relay”, keeping the Virtual DOM clean and efficient.

Renderless Components vs. Composables

A common question is why not simply use a Composable (useToggle). The right choice depends on the context:

FeatureComposablesRenderless Components
EncapsulationPure JavaScript logic.Logic tied to the component lifecycle.
TemplateImported in the script.Declared declaratively in the template.
ScopeIdeal for global/business logic.Ideal for UI patterns (accessibility, events).
SlotsNo access to slots.Can orchestrate multiple subcomponents.
Learning curveRequires understanding pure reactivity.More intuitive for template-oriented developers.

Renderless Components shine when the logic needs to interact with the lifecycle (like onMounted) or when you want to create a component hierarchy that shares implicit state (Compound Components).

Competitive Advantages of the Pattern

  1. Maintainability: If the validation logic changes, you only modify the renderless component. The UI remains intact.
  2. Testability: Makes it easier to unit test state logic without dealing with CSS selectors or style collisions.
  3. Extensibility: Enables multiple visual versions of the same functionality without duplicating core logic.

Final Thoughts

While this pattern offers unmatched flexibility, it should be used thoughtfully. In very large templates, excessive use of v-slot can make code harder to read. However, for building design systems and component libraries, Renderless Components are the gold standard in Vue 3 architecture.

Edit this page on GitHub

Found an issue or want to improve this post? You can propose changes directly.