Building Reusable and Maintainable Components in Vue with the Data Provider Pattern

Fotis Adamakis
4 min readApr 13, 2023

One of the most powerful techniques we can use to build reusable and maintainable components is the Provider Pattern. This approach, widely used not only in the Vue ecosystem, allows us to separate the presentation layer from the data fetching, helping us avoid common pitfalls like spaghetti code and prop drilling. By leveraging the Provider Pattern, we can create components that are easier to develop, reuse, and maintain over time.

Let's explore this pattern together.

The Problem

To ensure that we are addressing real-life problems, we will build a basic storefront and showcase a single component with varying data, as outlined below:

The issue at hand is where to fetch the data from, and we have three potential options to consider:

Option 1: Fetching the data at a Page Level

The Product Page could retrieve the data and pass it down as a prop or by using provide/inject to every component. While this approach may work initially, it has the potential to result in a bloated page component as more features are added.

Option 2: Fetching the data inside the child component based on a prop

This approach avoids cluttering the parent component by moving the data fetching into the child components. However, this may lead to tangled and difficult-to-maintain code. Additionally, this may not be scalable, as the intended reusable component will be responsible for fetching the data.

Option 3: Implementing the Data Provider Pattern ⭐️

By moving the data fetching logic to a dedicated provider component, we can keep both parent and child components clean and maintainable. Additionally, this approach ensures that the child components remain reusable while keeping the data-fetching logic centralized.

The Reusable Component

For a reusable component, we will use the following BoringTable.vue

<script setup>
defineProps({
items: {
type: Array,
required: true,
},
title: {
type: String,
},
});
const headers = ["Title", "Description", "Price", "Rating", "Brand"];
</script>

<template>
<h2 class="table-title">{{ title }}</h2>
<table class="boring-table">
<thead>
<tr>
<th v-for="header in headers" :key="header">
{{ header }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td>{{ item.title }}</td>
<td>{{ item.description }}</td>
<td>{{ item.price }}€</td>
<td>{{ item.rating }}✩</td>
<td>{{ item.brand }}</td>
</tr>
</tbody>
</table>
</template>

<style scoped>
// Styles are omitted...
</style>

Our component is boring by design by implementing the Container/Presentational component pattern and with the only responsibility of displaying the data.

The Consumer

The goal is to keep the fetching logic away from the page component and create the following structure.

<script setup>
import BoringTable from "@/components/BoringTable.vue";
import DataProvider from "@/components/DataProvider.ts";
</script>

<template>
<h1>Products Page</h1>

<DataProvider category="smartphones" v-slot="{ data, loading }">
<BoringTable :items="data" title="Smartphones" v-if="loading" />
</DataProvider>

<DataProvider category="laptops" v-slot="{ data, loading }">
<BoringTable :items="data" title="Laptops" v-if="loading" />
</DataProvider>
</template>

Please notice:

  • The category prop passed to each Data Provider and will determine which API will be called.
  • The data and loading prop slots expected from the Provider using a scoped slot.

The Data Provider

The data provider is a straightforward component that retrieves data from an API based on the provided category prop and returns it along with a loading indicator as the default slot.

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

const props = defineProps({
category: {
type: String,
required: true,
},
});

const data = ref(null);
const loaded = ref(false);

async function fetchProducts() {
const response = await fetch(
`https://dummyjson.com/products/category/${props.category}`
).then((res) => res.json());
data.value = response.products;
loaded.value = true;
}
fetchProducts();
</script>

<template>
<slot :data="data" :loading="!loaded" />
</template>

Alternatively, a renderless component can be used.

The Final Solution

Putting everything together, we have a clean and scalable architecture by deferring the data fetching from the Page Level to a dedicated Data Provider component while also keeping the child component presentational and lean.

--

--

« Senior Software Engineer · Author · International Speaker · Vue.js Athens Meetup Organizer »