Skip to the solution
MediumFrontend10 min read

useAttrs() vs $attrs — how do you handle fallthrough attributes in composition API?

295 views

The problem

Question presented to candidate: "We are moving our BaseInput from the Options API to <script setup>. In the template it used $attrs, and it had inheritAttrs: false. How do you do the same things in the Composition API, and what is the difference between useAttrs() and $attrs?"

What a strong answer should cover:

  • $attrs is the template handle on fallthrough attributes; useAttrs() is the script handle in <script setup> (and setup(props, { attrs }) in a plain setup() function). They expose the same data.
  • inheritAttrs: false moves into defineOptions({ inheritAttrs: false }) (Vue 3.3+), which the compiler hoists into the component options.
  • A common pattern: split class and style for the wrapper from the rest for the inner <input>, with computed objects bound by v-bind.
  • The attrs object is read-only and not a reactive() object: do not destructure it at the top of setup, and the docs advise using a prop when you need to watch a value (or reading it in onUpdated).
  • In TypeScript useAttrs() returns Record<string, unknown>, so declare as props anything you need typed.
  • When you need neither (single root, default behaviour), you need neither API: inheritance is automatic.

Clarifying questions expected:

  • "Should class style the wrapper or the input?"
  • "Does any logic in the script depend on the attribute values, or is it only forwarding them?"

Code / implementation expected: Yes — a <script setup> wrapper using defineOptions({ inheritAttrs: false }), useAttrs() and v-bind on the inner element.

  • useAttrs
  • useSlots
  • fallthrough

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read · 9 sections

Read the code

VueA BaseInput that sends class and style to the wrapper and every other attribute to the input
<script setup>
import { ref, computed, useAttrs, defineComponent, h } from "vue";

// In a .vue file this child would be <script setup> with
//   defineOptions({ inheritAttrs: false }) and const attrs = useAttrs()
const BaseInput = defineComponent({
  inheritAttrs: false,
  props: { label: String, modelValue: String },
  emits: ["update:modelValue"],
  setup(props, { emit }) {
    const attrs = useAttrs();   // the same attributes the template sees as $attrs

    // class and style decorate the wrapper; everything else belongs on the <input>.
    const rootAttrs = computed(() => ({ class: ["field", attrs.class], style: attrs.style }));
    const inputAttrs = computed(() => {
      const { class: _c, style: _s, ...rest } = attrs;
      return rest;
    });

    return () =>
      h("label", rootAttrs.value, [
        h("span", props.label + " "),
        h("input", {
          ...inputAttrs.value,
          value: props.modelValue,
          onInput: (e) => emit("update:modelValue", e.target.value),
        }),
      ]);
  },
});

const email = ref("");
const focusCount = ref(0);
const wide = ref(false);
</script>

<template>
  <BaseInput
    v-model="email"
    label="Email"
    :class="{ wide }"
    style="margin: 4px"
    type="email"
    placeholder="you@example.com"
    autocomplete="email"
    @focus="focusCount++"
  />
  <button class="toggle" @click="wide = !wide">toggle wide</button>
  <p>email: "{{ email }}" | focus events: {{ focusCount }}</p>
</template>
VueBaseInput.vue: the same component as <script setup> with defineOptions and useAttrs
<script setup>
import { computed, useAttrs } from "vue";

defineOptions({ inheritAttrs: false });
const props = defineProps({ label: String });
const model = defineModel();
const attrs = useAttrs();

// class and style style the wrapper; everything else belongs on the <input>.
const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }));
const inputAttrs = computed(() => {
  const { class: _c, style: _s, ...rest } = attrs;
  return rest;
});
</script>

<template>
  <label class="field" v-bind="rootAttrs">
    <span>{{ props.label }}</span>
    <input v-model="model" v-bind="inputAttrs" />
  </label>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 65 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

What is the Composition API and how does it differ from the Options API?

Continue