By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Vue.js: Propagating Props Like a Pro | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Vue.js: Propagating Props Like a Pro | HackerNoon
Computing

Vue.js: Propagating Props Like a Pro | HackerNoon

News Room
Last updated: 2025/05/03 at 8:02 PM
News Room Published 3 May 2025
Share
SHARE

When you create components for your project, it all starts fun and easy. Create MyButton.vue, add some styling, and voilà.

<template>
  <button class="my-fancy-style">
    <slot></slot>
  </button>
</template>

Then you immediately realize that you need a dozen props, because your design team wants it to be of different colors and sizes, with icons on the left and right, with counters…

const props = withDefaults(defineProps<{
  theme?: ComponentTheme;
  small?: boolean;
  icon?: IconSvg; // I’ve described how I cook icons in my previous article
  rightIcon?: IconSvg;
  counter?: number;
}>(), {
  theme: ComponentTheme.BLUE,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined
});

It’s still ok; it makes sense. After all, you can’t have the “Cancel” and “Ok” buttons of the same color, and you need them to react to user interactions. Oh, right, interactions.

const props = withDefaults(defineProps<{
  theme?: ComponentTheme;
  small?: boolean;
  icon?: IconSvg;
  rightIcon?: IconSvg;
  counter?: number;
  disabled?: boolean;
  loading?: boolean;
}>(), {
  theme: ComponentTheme.BLUE,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined
});

Well, you get the idea: there will be something wild, like passing width: 100% or adding autofocus—we all know how it looks simple in Figma until real life strikes hard.

Now imagine a link button: it looks the same, but when you press it, you should go to the external or internal link. You can wrap them into <RouterLink> or <a> tags every time, but please don’t. You can also add to and href props to your initial component, but you’ll feel suffocated pretty soon:

<component
  :is="to ? RouterLink : href ? 'a' : 'button'"
  <!-- ugh! -->

Of course, you’ll need a “second-level” component that wraps your button (it will also handle the default hyperlink outlines and some other interesting things, but I’ll omit them for the sake of simplicity):

<template>
  <component
    :is="props.to ? RouterLink : 'a'"
    :to="props.to"
    :href="props.href"
    class="my-link-button"
  >
    <MyButton v-bind="$attrs">
      <slot></slot>
    </MyButton>
  </component>
</template>

<script lang="ts" setup>
import MyButton from './MyButton.vue';
import { RouteLocationRaw, RouterLink } from 'vue-router';

const props = defineProps<{
  to?: RouteLocationRaw;
  href?: string;
}>();
</script>

And this is where our story begins.

Square One

Well, basically it’s going to work, I won’t lie. You can still type <MyLinkButton :counter=“2"> , and it’ll be fine. But there will be no autocomplete for the derived props, which is not cool:

Only "href" and "to"Only "href" and "to"

We can propagate props silently, but the IDE doesn’t know a thing about them, and that’s a shame.

The straightforward and obvious solution is to propagate them explicitly:

<template>
  <component
    :is="props.to ? RouterLink : 'a'"
    :to="props.to"
    :href="props.href"
    class="my-link-button"
  >
    <MyButton
      :theme="props.theme"
      :small="props.small"
      :icon="props.icon"
      :right-icon="props.rightIcon"
      :counter="props.counter"
      :disabled="props.disabled"
      :loading="props.loading"
    >
      <slot></slot>
    </MyButton>
  </component>
</template>

<script lang="ts" setup>
// imports...

const props = withDefaults(
  defineProps<{
    theme?: ComponentTheme;
    small?: boolean;
    icon?: IconSvg;
    rightIcon?: IconSvg;
    counter?: number;
    disabled?: boolean;
    loading?: boolean;

    to?: RouteLocationRaw;
    href?: string;
  }>(),
  {
    theme: ComponentTheme.BLUE,
    icon: undefined,
    rightIcon: undefined,
    counter: undefined,
  }
);
</script>

It will work. The IDE will have proper autocomplete. We will have a lot of pain and regret supporting it.

Obviously, the “Don’t Repeat Yourself” principle was not applied here, which means that we will need to synchronize every update. One day, you’ll need to add another prop, and you’ll have to find every component that wraps the basic one. Yes, Button and LinkButton are probably enough, but imagine TextInput and a dozen of components that depend on it: PasswordInput, EmailInput, NumberInput, DateInput, HellKnowsWhatElseInput. Adding a prop shouldn’t cause suffering.

After all, it’s ugly. And the more props we have, the uglier it becomes.

Clean It Up

It’s quite hard to reuse an anonymous type, so let’s give it a name.

// MyButton.props.ts

export interface MyButtonProps {
  theme?: ComponentTheme;
  small?: boolean;
  icon?: IconSvg;
  rightIcon?: IconSvg;
  counter?: number;
  disabled?: boolean;
  loading?: boolean;
}

We cannot export an interface from the .vue file due to some internal script setup magic, so we need to create a separate .ts file. On the bright side, look what we’ve got here:

const props = withDefaults(defineProps<MyButtonProps>(), {
  theme: ComponentTheme.BLUE,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined,
});

Much cleaner, isn’t it? And here’s the inherited one:

interface MyLinkButtonProps {
  to?: RouteLocationRaw;
  href?: string;
}

const props = defineProps<MyButtonProps & MyLinkButtonProps>();

However, here’s a problem: now, when basic props are treated as MyLinkButton’s props, they aren’t propagated with v-bind=”$attrs” anymore, so we need to do it ourselves.

<!-- MyLinkButton.vue -->

<component
  :is="props.to ? RouterLink : 'a'"
  :to="props.to"
  :href="props.href"
  class="my-link-button"
>
  <MyButton v-bind="props"> <!-- there we go -->
    <slot></slot>
  </MyButton>
</component>

It’s all good, but we pass a little bit more than we want to:

W3C disapprovesW3C disapproves

As you can see, now, our underlying button also has a href attribute. It’s not a tragedy, just a bit of clutter and extra bytes, although not cool. Let’s clean it up, too.

<template>
  <component
    :is="props.to ? RouterLink : 'a'"
    :to="props.to"
    :href="props.href"
    class="my-link-button"
  >
    <MyButton v-bind="propsToPass">
      <slot></slot>
    </MyButton>
  </component>
</template>

<script lang="ts" setup>
// imports and definitions…

const props = defineProps<MyButtonProps & MyLinkButtonProps>();

const propsToPass = computed(() =>
  Object.fromEntries(
    Object.entries(props).filter(([key, _]) => !["to", "href"].includes(key))
  )
);
</script>

Now, we only pass what has to be passed, but all those string literals don’t look awesome, do they? And that’s the saddest TypeScript story, guys. Let me tell you about it.

Interfaces vs Abstract Interfaces

If you have ever worked with proper object-oriented languages, you probably know about such things as reflection, which allows us to get the metadata about our structures. Unfortunately, in TypeScript, interfaces are ephemeral; they don’t exist at runtime, and we cannot easily find out which fields belong to the MyButtonProps.

It means that we have two options. First, we can keep things as is: whenever we add a prop to MyLinkButton, we also need to exclude it from propsToPass (and even if we forget about it, it’s not a big deal).

The second way is to use objects instead of interfaces. It might sound senseless, but let me code something: it won’t be horrible; I promise. Well, at least not that horrible.

// MyButton.props.ts

export const defaultMyButtonProps: MyButtonProps = {
  theme: ComponentTheme.BLUE,
  small: false,
  icon: undefined,
  rightIcon: undefined,
  counter: undefined,
  disabled: false,
  loading: false,
};

It doesn’t make sense to create an object just for creating an object, but we can use it for the default props. The declaration in MyButton.vue becomes even cleaner:

const props = withDefaults(defineProps<MyButtonProps>(), defaultMyButtonProps);

Now, we only need to update propsToPass in MyLinkButton.vue:

const propsToPass = computed(() =>
  Object.fromEntries(
    Object.entries(props).filter(([key, _]) =>
      Object.hasOwn(defaultMyButtonProps, key)
    )
  )
);

To make this work, we need to explicitly define all undefined and null fields in defaultMyButtonProps; otherwise, the object doesn’t “haveOwn”.

This way, whenever you add a prop to the basic component, you’ll also need to add it to the object with default values. So, yes, it’s two places again, and maybe it’s not better than the solution from the previous chapter. It’s up to you which one you’ll find cleaner.

I’m Done

It’s not a masterpiece, but it’s probably the best we can do within TypeScript’s limitations.

It also seems that having prop types inside the SFC file is better, but I can’t say that moving them to a separate file made it much worse. But it definitely made prop reusing better, so I’ll consider it to be a small victory in an endless battle we call work.

You can find the code from this article on GitHub.

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article 'Thunderbolts*': Is There a Post-Credits Scene?
Next Article Kentucky Derby 2025 LIVE RESULTS: Sovereignty STORMS to victory, finishing order
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Tea can absorb lead, other harmful metals, Northwestern University scientists find
News
After Ukraine’s innovative airbase attacks, nowhere in Russia is safe
News
👨🏿‍🚀 Daily – PalmPay in talks to raise up to $100M |
Computing
Archaeologists find 6,000-year-old skeletons from Colombia with ancient DNA
News

You Might also Like

Computing

👨🏿‍🚀 Daily – PalmPay in talks to raise up to $100M |

3 Min Read
Computing

Gen-Z’s Top 25 Beauty Influencers You’ll Be Seeing A Lot of in 2025

4 Min Read
Computing

6 AI in Social Media Examples to Inspire Your Strategy

17 Min Read
Computing

Free Integrated Marketing Plan Templates |

22 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?