The Art of Props Management in Vue 3: Lessons from E-commerce Architecture
Marco Quintella

Marco Quintella @marco_quintella

About: Senior Fullstack Developer, currently delivering social and AI experiences for the e-commerce industry.

Location:
Brasília, Brazil
Joined:
Jun 4, 2024

The Art of Props Management in Vue 3: Lessons from E-commerce Architecture

Publish Date: May 20
0 0

In the sprawling world of multi-brand e-commerce platforms, where a single codebase powers numerous storefronts, component communication becomes an intricate dance. Imagine a digital mall where every shop has its own facade, but behind the scenes, they share the same foundational infrastructure. This is the reality for many large e-commerce operations today.

As a developer navigating this complex landscape, mastering Vue's props system isn't just helpful—it's essential. Let's explore how to elegantly handle props in Vue 3 using TypeScript and the Composition API, drawing from real-world e-commerce scenarios.

The Props Journey: From Parent to Child

In a multi-brand e-commerce platform, props travel like products through a supply chain. They originate from a source and make their way through various components until they reach their final destination. Understanding this journey is the first step in becoming a props master.

Basic Props in Vue 3 with TypeScript

Let's start with a product card component, a fundamental element in any e-commerce site:

<script setup lang="ts">
// ProductCard.vue
defineProps<{
  product: {
    id: string;
    name: string;
    price: number;
    imageUrl: string;
    brand: string;
    isOnSale?: boolean;
  };
  showRating?: boolean;
  theme?: 'light' | 'dark' | 'branded';
}>();
</script>

<template>
  <div class="product-card" :class="theme">
    <img :src="product.imageUrl" :alt="product.name" />
    <h3>{{ product.name }}</h3>
    <p class="price" :class="{ 'on-sale': product.isOnSale }">
      ${{ product.price.toFixed(2) }}
    </p>
    <p class="brand">{{ product.brand }}</p>
    <star-rating v-if="showRating" :product-id="product.id" />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

In this example, we have a product card that can be customized based on the brand's requirements. The theme prop allows for different visual styles, while showRating toggles the visibility of the rating component.

The Props Cascade: When Components Go Deep

Now, imagine we're building a product listing page. We have multiple levels of components:

  1. ProductListingPage (contains multiple product categories)
  2. ProductCategory (contains multiple product rows)
  3. ProductRow (contains multiple product cards)
  4. ProductCard (displays individual product)

Each level needs to pass certain props down to its children. This is where props management becomes crucial.

The Prop-Drilling Problem

Consider this scenario: we need to pass a currency setting from the top-level component down to the product cards. The traditional approach would be:

<script setup lang="ts">
// ProductListingPage.vue
const currency = ref('USD');
</script>

<template>
  <product-category 
    v-for="category in categories" 
    :key="category.id" 
    :category="category" 
    :currency="currency"
  />
</template>
Enter fullscreen mode Exit fullscreen mode
<script setup lang="ts">
// ProductCategory.vue
defineProps<{
  category: { id: string; name: string; products: Product[] };
  currency: string;
}>();
</script>

<template>
  <product-row 
    v-for="(row, index) in productRows" 
    :key="index" 
    :products="row" 
    :currency="currency"
  />
</template>
Enter fullscreen mode Exit fullscreen mode
<script setup lang="ts">
// ProductRow.vue
defineProps<{
  products: Product[];
  currency: string;
}>();
</script>

<template>
  <product-card 
    v-for="product in products" 
    :key="product.id" 
    :product="product" 
    :currency="currency"
  />
</template>
Enter fullscreen mode Exit fullscreen mode
<script setup lang="ts">
// ProductCard.vue (updated)
defineProps<{
  product: Product;
  currency: string;
  showRating?: boolean;
  theme?: 'light' | 'dark' | 'branded';
}>();
</script>

<template>
  <div class="product-card" :class="theme">
    <!-- ... -->
    <p class="price" :class="{ 'on-sale': product.isOnSale }">
      {{ formatPrice(product.price, currency) }}
    </p>
    <!-- ... -->
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

This approach, known as "prop drilling," quickly becomes unwieldy. The currency prop must pass through ProductCategory and ProductRow even though these components don't use it themselves.

Elegant Solutions for Props Management

1. Using Provide/Inject for Deep Props

One of the most elegant solutions to avoid prop drilling is using Vue's provide and inject features:

<script setup lang="ts">
// ProductListingPage.vue
import { provide, ref } from 'vue';

const currency = ref('USD');
// The provide key should be a symbol in real applications for type safety
provide('currency', currency);
</script>

<template>
  <product-category 
    v-for="category in categories" 
    :key="category.id" 
    :category="category"
  />
</template>
Enter fullscreen mode Exit fullscreen mode

Then, in the ProductCard.vue component:

<script setup lang="ts">
import { inject } from 'vue';

defineProps<{
  product: Product;
  showRating?: boolean;
  theme?: 'light' | 'dark' | 'branded';
}>();

// TypeScript might need a default value or type assertion
const currency = inject('currency', ref('USD'));
</script>
Enter fullscreen mode Exit fullscreen mode

With this approach, intermediate components like ProductCategory and ProductRow don't need to be aware of the currency prop at all.

2. TypeScript Enhancements for Provide/Inject

To make this approach more robust with TypeScript, we can create a type-safe system:

// types.ts
import { InjectionKey, Ref } from 'vue';

export interface EcommerceConfig {
  currency: Ref<string>;
  priceDisplay: Ref<'withTax' | 'withoutTax'>;
  theme: Ref<'light' | 'dark' | 'branded'>;
}

export const EcommerceConfigKey: InjectionKey<EcommerceConfig> = Symbol('EcommerceConfig');
Enter fullscreen mode Exit fullscreen mode

Then in our root component:

<script setup lang="ts">
import { provide, ref } from 'vue';
import { EcommerceConfigKey } from './types';

const config = {
  currency: ref('USD'),
  priceDisplay: ref('withTax' as const),
  theme: ref('light' as const)
};

provide(EcommerceConfigKey, config);
</script>
Enter fullscreen mode Exit fullscreen mode

And in child components:

<script setup lang="ts">
import { inject } from 'vue';
import { EcommerceConfigKey } from './types';

const { currency, theme } = inject(EcommerceConfigKey)!;
</script>
Enter fullscreen mode Exit fullscreen mode

This pattern is especially valuable in multi-brand e-commerce platforms, where brand-specific settings need to be accessible throughout the component tree.

3. Props Destructuring with Defaults

For direct props usage, Vue 3's Composition API allows for elegant destructuring with defaults:

<script setup lang="ts">
// BrandProductCard.vue
import { computed } from 'vue';

interface Props {
  product: Product;
  showRating?: boolean;
  theme?: 'light' | 'dark' | 'branded';
  brandAccentColor?: string;
}

const props = withDefaults(defineProps<Props>(), {
  showRating: false,
  theme: 'light',
  brandAccentColor: '#e91e63'
});

const cardStyle = computed(() => ({
  '--accent-color': props.brandAccentColor
}));
</script>

<template>
  <div class="product-card" :class="theme" :style="cardStyle">
    <!-- Component content -->
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

4. Creating Intermediate Components for Props Grouping

When dealing with complex UI elements that require many props, it's often beneficial to create intermediate components that group related props together:

<script setup lang="ts">
// ProductPricing.vue
defineProps<{
  regularPrice: number;
  salePrice?: number;
  currency: string;
  showTax: boolean;
  taxRate?: number;
}>();
</script>

<template>
  <div class="product-pricing">
    <span v-if="salePrice" class="original-price">{{ formatPrice(regularPrice, currency) }}</span>
    <span class="current-price">
      {{ formatPrice(salePrice || regularPrice, currency) }}
    </span>
    <span v-if="showTax" class="tax-info">
      {{ taxRate ? `Includes ${taxRate}% tax` : 'Tax included' }}
    </span>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

This specialized component can now be used within the ProductCard component:

<script setup lang="ts">
// ProductCard.vue (simplified)
defineProps<{
  product: Product;
  showTax: boolean;
  currency: string;
}>();
</script>

<template>
  <div class="product-card">
    <!-- Other product info -->
    <product-pricing
      :regular-price="product.price"
      :sale-price="product.salePrice"
      :currency="currency"
      :show-tax="showTax"
      :tax-rate="product.taxRate"
    />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

This approach creates logical groupings of props, making the component structure clearer and more maintainable.

Advanced Props Patterns for E-commerce Scenarios

1. Dynamic Props for Multi-brand Customization

In a multi-brand environment, different brands might require different variations of the same component. We can use computed props to handle this:

<script setup lang="ts">
// DynamicProductCard.vue
import { computed } from 'vue';
import { useStoreConfig } from '@/composables/useStoreConfig';

const props = defineProps<{
  product: Product;
  brandId: string;
}>();

// Get store-specific configuration
const storeConfig = useStoreConfig(props.brandId);

// Compute the correct theme based on brand settings
const cardTheme = computed(() => {
  if (storeConfig.value.useDarkMode) return 'dark';
  if (storeConfig.value.useCustomBranding) return 'branded';
  return 'light';
});

// Determine whether to show the product's rating based on brand preference
const shouldShowRating = computed(() => {
  return storeConfig.value.showProductRatings && product.hasRatings;
});
</script>

<template>
  <product-card
    :product="product"
    :theme="cardTheme"
    :show-rating="shouldShowRating"
  />
</template>
Enter fullscreen mode Exit fullscreen mode

This pattern allows for brand-specific customization without cluttering the component's public API.

2. Props Validation and Transformation

For complex e-commerce applications, validating and transforming props becomes increasingly important:

<script setup lang="ts">
// PriceDisplay.vue
import { computed } from 'vue';

interface Props {
  amount: number;
  currency: string;
  format?: 'standard' | 'compact';
}

const props = defineProps<Props>();

// Validate the currency format
const validCurrency = computed(() => {
  const supportedCurrencies = ['USD', 'EUR', 'GBP', 'CAD', 'AUD'];
  return supportedCurrencies.includes(props.currency) ? props.currency : 'USD';
});

// Transform and format the price
const formattedPrice = computed(() => {
  const options = {
    style: 'currency',
    currency: validCurrency.value,
    notation: props.format === 'compact' ? 'compact' as const : 'standard' as const
  };

  return new Intl.NumberFormat(navigator.language, options).format(props.amount);
});
</script>

<template>
  <span class="price">{{ formattedPrice }}</span>
</template>
Enter fullscreen mode Exit fullscreen mode

This component not only displays prices but also validates and normalizes its input props, ensuring consistent behavior across the platform.

3. Using Props for Feature Flagging

In complex e-commerce systems, feature flags are common. Props can be used to implement this pattern:

<script setup lang="ts">
// CheckoutButton.vue
defineProps<{
  productId: string;
  // Feature flags
  enableOneClickCheckout?: boolean;
  enableSaveForLater?: boolean;
  enableWishlist?: boolean;
}>();

// Component logic that adjusts based on enabled features
</script>

<template>
  <div class="checkout-actions">
    <button class="add-to-cart">Add to Cart</button>

    <button 
      v-if="enableOneClickCheckout" 
      class="one-click-checkout"
    >
      Buy Now
    </button>

    <button 
      v-if="enableSaveForLater" 
      class="save-for-later"
    >
      Save for Later
    </button>

    <button 
      v-if="enableWishlist" 
      class="add-to-wishlist"
    >
      Add to Wishlist
    </button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

This approach allows different brands or markets to enable or disable features based on their specific requirements.

Best Practices for Props in Large E-commerce Applications

1. Create Props Interfaces and Types

For complex components, create dedicated interface files:

// types/product-card.types.ts
export interface ProductCardProps {
  product: Product;
  showRating?: boolean;
  theme?: 'light' | 'dark' | 'branded';
  enableQuickView?: boolean;
  enableWishlist?: boolean;
}

// Then in your component
import { ProductCardProps } from '@/types/product-card.types';

defineProps<ProductCardProps>();
Enter fullscreen mode Exit fullscreen mode

This approach centralizes prop definitions and makes them reusable across components.

2. Use Composables for Complex Props Logic

When prop handling logic becomes complex, extract it into composables:

// composables/useProductDisplay.ts
import { computed, ComputedRef } from 'vue';
import type { Product } from '@/types';

interface ProductDisplayOptions {
  product: Product;
  currency: string;
  showTax: boolean;
}

export function useProductDisplay(options: ProductDisplayOptions) {
  const formattedPrice = computed(() => {
    // Price formatting logic
    return formatPrice(options.product.price, options.currency);
  });

  const displayName = computed(() => {
    // Product name formatting logic
    return options.product.brand 
      ? `${options.product.brand} - ${options.product.name}`
      : options.product.name;
  });

  const taxInfo = computed(() => {
    // Tax display logic
    if (!options.showTax) return null;
    // Calculate and return tax information
  });

  return {
    formattedPrice,
    displayName,
    taxInfo
  };
}

// In your component
const { formattedPrice, displayName, taxInfo } = useProductDisplay({
  product: props.product,
  currency: props.currency,
  showTax: props.showTax
});
Enter fullscreen mode Exit fullscreen mode

This pattern keeps components clean and focused on rendering, while complex business logic lives in dedicated composables.

3. Document Props with JSDoc

Documentation is essential in large teams. Use JSDoc to document your props:

<script setup lang="ts">
/**
 * Product Card component for displaying product information
 * @prop {Product} product - The product data object
 * @prop {boolean} [showRating=false] - Whether to display product ratings
 * @prop {'light'|'dark'|'branded'} [theme='light'] - Visual theme for the card
 * @prop {string} [currency='USD'] - Currency code for price display
 */
defineProps<{
  product: Product;
  showRating?: boolean;
  theme?: 'light' | 'dark' | 'branded';
  currency?: string;
}>();
</script>
Enter fullscreen mode Exit fullscreen mode

This documentation helps other developers understand your component's API without digging into the implementation.

Conclusion: The Art of Props Mastery

In large e-commerce platforms that power multiple brands, effective props management is both an art and a science. By leveraging Vue 3's modern features like the Composition API, TypeScript integration, and provide/inject pattern, we can create component architectures that are both powerful and maintainable.

Remember these key takeaways:

  1. Use provide/inject for deeply nested props and application-wide settings
  2. Create specialized intermediate components to group related props
  3. Leverage TypeScript for type safety and better developer experience
  4. Extract complex prop handling logic into composables
  5. Document your component APIs thoroughly

By mastering these patterns, you'll be well-equipped to build scalable, maintainable component systems that can support multiple brands and evolving business requirements in the complex world of e-commerce.

Comments 0 total

    Add comment