Chapter 01 - Mastering Vue.js: Advanced Component Patterns for Powerful Web Apps

Vue.js advanced component patterns enhance reusability and flexibility. Renderless components, higher-order components, scoped slots, compound components, and provide/inject patterns offer powerful solutions for complex application development.

Chapter 01 - Mastering Vue.js: Advanced Component Patterns for Powerful Web Apps

Vue.js has come a long way since its inception, and with its growing popularity, developers are constantly pushing the boundaries of what’s possible with component design. Let’s dive into some advanced component patterns that can take your Vue.js applications to the next level.

Renderless components are a powerful pattern that separates logic from presentation. They don’t render any markup of their own but instead provide functionality to other components through scoped slots. This approach promotes reusability and flexibility in your codebase.

Here’s an example of a renderless component that manages a countdown timer:

<template>
  <slot :timeLeft="timeLeft" :start="start" :pause="pause" :reset="reset" />
</template>

<script>
export default {
  data() {
    return {
      timeLeft: 0,
      timer: null
    }
  },
  methods: {
    start(duration) {
      this.timeLeft = duration
      this.timer = setInterval(() => {
        if (this.timeLeft > 0) {
          this.timeLeft--
        } else {
          clearInterval(this.timer)
        }
      }, 1000)
    },
    pause() {
      clearInterval(this.timer)
    },
    reset() {
      clearInterval(this.timer)
      this.timeLeft = 0
    }
  }
}
</script>

This component doesn’t render anything on its own, but it provides the countdown logic that can be used by other components. You can use it like this:

<template>
  <CountdownTimer v-slot="{ timeLeft, start, pause, reset }">
    <div>
      <p>Time left: {{ timeLeft }} seconds</p>
      <button @click="start(60)">Start 1-minute countdown</button>
      <button @click="pause">Pause</button>
      <button @click="reset">Reset</button>
    </div>
  </CountdownTimer>
</template>

<script>
import CountdownTimer from './CountdownTimer.vue'

export default {
  components: { CountdownTimer }
}
</script>

This pattern allows you to reuse the countdown logic in different contexts, with different UIs, without duplicating code.

Higher-order components (HOCs) are another advanced pattern borrowed from the React world. In Vue, they’re usually implemented as functions that take a component as an argument and return a new component with enhanced functionality.

Here’s an example of a HOC that adds loading state management to a component:

function withLoading(WrappedComponent) {
  return {
    data() {
      return {
        isLoading: false
      }
    },
    methods: {
      startLoading() {
        this.isLoading = true
      },
      endLoading() {
        this.isLoading = false
      }
    },
    render(h) {
      return h(WrappedComponent, {
        props: this.$props,
        on: this.$listeners,
        attrs: this.$attrs,
        scopedSlots: this.$scopedSlots,
        directives: this.$vnode.data.directives,
      })
    }
  }
}

You can use this HOC like this:

<template>
  <div>
    <button @click="fetchData">Fetch Data</button>
    <p v-if="isLoading">Loading...</p>
    <ul v-else>
      <li v-for="item in data" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import { withLoading } from './withLoading'

export default withLoading({
  data() {
    return {
      data: []
    }
  },
  methods: {
    async fetchData() {
      this.startLoading()
      try {
        const response = await fetch('https://api.example.com/data')
        this.data = await response.json()
      } finally {
        this.endLoading()
      }
    }
  }
})
</script>

This HOC adds isLoading, startLoading, and endLoading to the wrapped component, making it easy to manage loading states across your application.

Scoped slots are a powerful feature in Vue that allow child components to pass data back up to their parents. They’re particularly useful for creating flexible, reusable components.

Here’s an example of a component that uses scoped slots to create a flexible list component:

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  props: ['items']
}
</script>

You can use this component in various ways:

<template>
  <div>
    <h2>Simple List</h2>
    <FlexibleList :items="items" />

    <h2>Custom List</h2>
    <FlexibleList :items="items">
      <template v-slot:default="{ item }">
        <strong>{{ item.name }}</strong> ({{ item.id }})
      </template>
    </FlexibleList>
  </div>
</template>

<script>
import FlexibleList from './FlexibleList.vue'

export default {
  components: { FlexibleList },
  data() {
    return {
      items: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' }
      ]
    }
  }
}
</script>

This pattern allows you to create highly reusable components that can adapt to different use cases.

Another powerful pattern is the Compound Component pattern. This pattern involves creating a set of components that work together to form a more complex UI component. Each sub-component is responsible for rendering a specific part of the UI and managing its own state, while the parent component coordinates the overall behavior.

Here’s an example of a compound component for a custom select dropdown:

<!-- Select.vue -->
<template>
  <div class="select">
    <slot></slot>
  </div>
</template>

<script>
export default {
  provide() {
    return {
      select: this
    }
  },
  data() {
    return {
      selectedOption: null
    }
  },
  methods: {
    select(option) {
      this.selectedOption = option
    }
  }
}
</script>

<!-- SelectTrigger.vue -->
<template>
  <button @click="toggleDropdown">
    {{ select.selectedOption ? select.selectedOption.label : 'Select an option' }}
  </button>
</template>

<script>
export default {
  inject: ['select'],
  methods: {
    toggleDropdown() {
      // Logic to toggle dropdown
    }
  }
}
</script>

<!-- SelectOption.vue -->
<template>
  <div @click="selectOption">
    <slot></slot>
  </div>
</template>

<script>
export default {
  inject: ['select'],
  props: ['value', 'label'],
  methods: {
    selectOption() {
      this.select.select({ value: this.value, label: this.label })
    }
  }
}
</script>

You can use these compound components like this:

<template>
  <Select>
    <SelectTrigger />
    <SelectOption value="1" label="Option 1">Option 1</SelectOption>
    <SelectOption value="2" label="Option 2">Option 2</SelectOption>
    <SelectOption value="3" label="Option 3">Option 3</SelectOption>
  </Select>
</template>

<script>
import { Select, SelectTrigger, SelectOption } from './Select'

export default {
  components: { Select, SelectTrigger, SelectOption }
}
</script>

This pattern allows for great flexibility and customization while maintaining a clear structure and separation of concerns.

The Provide/Inject pattern is another powerful tool in Vue’s component communication arsenal. It allows you to pass data deeply through the component tree without having to explicitly pass props at every level. This is particularly useful for theme data, translations, or any other data that needs to be accessible by many components.

Here’s an example of using Provide/Inject for a theme system:

<!-- ThemeProvider.vue -->
<template>
  <div :class="theme">
    <slot></slot>
  </div>
</template>

<script>
export default {
  provide() {
    return {
      theme: this.theme
    }
  },
  props: ['theme']
}
</script>

<!-- ThemedButton.vue -->
<template>
  <button :class="[theme + '-button']">
    <slot></slot>
  </button>
</template>

<script>
export default {
  inject: ['theme']
}
</script>

You can use these components like this:

<template>
  <ThemeProvider theme="dark">
    <h1>Dark Theme</h1>
    <ThemedButton>Click me</ThemedButton>
  </ThemeProvider>
</template>

<script>
import ThemeProvider from './ThemeProvider.vue'
import ThemedButton from './ThemedButton.vue'

export default {
  components: { ThemeProvider, ThemedButton }
}
</script>

This pattern is great for creating themeable components without having to pass theme data through every level of your component tree.

The Async Component pattern is useful for large applications where you want to split your code into smaller chunks and load components only when they’re needed. Vue makes this easy with its defineAsyncComponent function.

Here’s an example:

import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() =>
  import('./components/HeavyComponent.vue')
)

export default {
  components: {
    AsyncComponent
  }
}

This pattern can significantly improve the initial load time of your application by deferring the loading of components until they’re actually needed.

The Controlled Component pattern is borrowed from React and can be useful in Vue as well. In this pattern, the parent component controls the state of a child component through props and events.

Here’s an example of a controlled input component:

<template>
  <input
    :value="value"
    @input="$emit('input', $event.target.value)"
  />
</template>

<script>
export default {
  props: ['value']
}
</script>

You would use this component like this:

<template>
  <div>
    <ControlledInput :value="inputValue" @input="inputValue = $event" />
    <p>Input value: {{ inputValue }}</p>
  </div>
</template>

<script>
import ControlledInput from './ControlledInput.vue'

export default {
  components: { ControlledInput },
  data() {
    return {
      inputValue: ''
    }
  }
}
</script>

This pattern gives the parent component full control over the input’s value, which can be useful in complex forms or when you need to perform validation or transformation on the input value.

The Renderless Data Fetching pattern combines the concepts of renderless components and the provide/inject API to create a reusable data fetching solution. This pattern is particularly useful when you have multiple components that need to fetch and display the same data.

Here’s an example:

<!-- DataFetcher.vue -->
<template>
  <slot v-if="data" :data="data" :refetch="fetchData" />
  <slot v-else-if="error" name="error" :error="error" :refetch="fetchData" />
  <slot v-else name="loading" />
</template>

<script>
export default {
  props: ['url'],
  data() {
    return {
      data: null,
      error: null
    }
  },
  provide() {
    return {