31 August 2024

Renderless Components

Renderless components, also known as headless components, are a powerful pattern in Vue.js that allow you to abstract logic and state management away from the rendering details. This separation of concerns can lead to more reusable and maintainable code. Before composables came along in Vue.js v3, we used Renderless Components to reuse logic. Here are the key situations where renderless components are particularly useful:

Reusability of Logic: Renderless components are perfect for scenarios where you need to reuse the same logic across multiple components with different render outputs. Examples include form validation logic that can be applied to various forms with different structures and data fetching logic that can be reused across different components, each displaying the data in unique ways. This approach allows for greater flexibility and reusability, ensuring consistent logic while accommodating diverse presentation needs.

Separation of Concerns: When you want to separate business logic from the presentation layer, renderless components offer a clean solution. This approach results in cleaner, more maintainable code and creates a clear distinction between what your component does and how it is presented. By isolating the logic, you enhance code readability and flexibility, making it easier to manage and update your application.

High Customizability: Renderless components enable high customizability of UI components without the need to duplicate logic. This is particularly useful for creating custom dropdowns or autocomplete components, where the logic for filtering and selecting items remains consistent while the UI can vary significantly. Additionally, they are ideal for table or list components that handle sorting and pagination but allow for different row or item layouts. This approach ensures efficient code reuse while providing the flexibility to adapt the UI to different requirements.

Complex Interactions: For components that require complex interactions and state management yet can have various visual representations, renderless components simplify the development process. Examples include drag-and-drop interfaces, where the dragging logic remains consistent while the draggable elements differ, and modal management logic that can be reused for different types of modals. By separating the logic from the presentation, renderless components streamline development and enhance code reusability.

Renderless components in Vue.js are useful for creating reusable, customisable, and maintainable logic without being tied to specific rendering details. They should be used when you want to separate business logic from presentation, achieve high customizability, and handle complex interactions consistently across different UI components. By leveraging renderless components, you can build more modular and flexible Vue applications.

Renderless Component Example

Here's an example of the Numeric Input component implemented as a renderless component. By definition, a renderless component separates the logic from the presentation, passing the necessary data and methods to the parent component, which handles the UI rendering.

Composables also help us separate logic from the presentation. However, Renderless Components, as the name suggests, are a type of Vue components, and they must provide methods and properties via slots to the parent component as we see in the example below.

Renderless Component as a Render Fn (parent)
quantity: ref(0)
Functional Input as a Render Fn Component (child)

In our Numeric Input component example, we pass the increment, decrement, inputValue, onInput, validation, and quantity to the parent component via slots. This allows the parent component to manage the presentation while reusing the core logic.

Renderless components can be implemented as either Single File Components or Render Function components. Both approaches do not render any HTML or UI themselves. Instead, they provide the logic to the parent component through scoped slots.

// components/v4/RenderlessCompAsSFC.vue
<template>
    <div>
        <slot :increment="increment"
              :decrement="decrement"
              :onInput="onInput"
              :inputValue="inputValue"
              :validation="validation"
              :quantity="quantity">
        </slot>
    </div>
</template>

You can view the full example of the Numeric Input component as a Renderless Component (written as an SFC) here.

Below is how the Render Function version of the same component. You can see the full example of the Numeric Input component as a Renderless Component (written as a Render Function) where we use h() in the script section to render out the HTML markup using methods and properties provided via the props.

// components/v5/RenderlessCompAsRenderFn.js
return () => [
  h('div',
    {
      class: "py-4 text-center flex items-center justify-center flex-col text-sm"
    },
    slots.default({
      decrement,
      increment,
      validation: validation.value,
      inputValue: inputValue.value,
      quantity: quantity.value,
      input: (e) => {
        quantity.value = parseInt(e.target.value)
      },
    })
  )
]

In case you have missed earlier articles, you can learn about Render Function components in Vue.js here.

Headless UI component library has a collection of Vue components that uses this renderless, aka headless pattern as well.