Vue 3.5's Reactive Props Destructure Finally Feels Right

By amillionmonkeys
#Vue.js#JavaScript#Frontend Development

Vue 3.5 fixed one of my biggest complaints about the Composition API. Here's how reactive props destructuring changes my component patterns.

I've been writing Vue 3 Composition API code for three years now, and there's been one persistent annoyance: you couldn't destructure props without losing reactivity. Every developer I've worked with (including freelancers and collaborators) has hit this same gotcha.

Vue 3.5 finally fixed it. Reactive props destructuring is now stable, and it's quietly changing how I write components. Here's what I learned migrating a dozen production apps.

The Old Problem: props.something Everywhere

If you've written Composition API code, you've felt this pain. You receive props in your setup function, and you want to use them. The natural JavaScript thing to do is destructure:

// This looks right but breaks reactivity
const { title, count } = props;

The moment you destructure, you lose reactivity. Those values are now static primitives. When the parent component updates title, your component doesn't react.

The official solution was to use toRefs():

import { toRefs } from 'vue';
 
const { title, count } = toRefs(props);

But now title and count are refs, so you need .value everywhere in your JavaScript but not in your template. We ended up with code that looked like this:

const { title, count } = toRefs(props);
 
const formattedTitle = computed(() => {
  return title.value.toUpperCase(); // .value here
});
 
const handleClick = () => {
  console.log(title.value); // .value here too
};

It worked, but it felt clunky. Most developers (including myself) settled on just using props.propertyName throughout the component and skipping destructuring entirely.

What Vue 3.5 Actually Changed

Vue 3.5 introduced reactivity transform for props destructuring, but made it stable and built-in. You can now destructure props directly in the setup function signature, and Vue's compiler maintains reactivity automatically.

Here's the new syntax:

<script setup>
const { title, count = 0 } = defineProps({
  title: String,
  count: Number
});
 
// title and count are now reactive values
// No .value needed, no toRefs needed
const formattedTitle = computed(() => title.toUpperCase());
</script>

Under the hood, Vue's compiler transforms this into reactive references. You get the clean syntax of destructuring without losing reactivity.

Default values work exactly as you'd expect:

const { title = 'Untitled', count = 0, enabled = true } = defineProps({
  title: String,
  count: Number,
  enabled: Boolean
});

How I'm Actually Using This

I started migrating my components gradually. Here's a real before/after from a data table component I use across several client projects.

Before Vue 3.5

<script setup>
const props = defineProps({
  data: Array,
  columns: Array,
  sortable: Boolean,
  pageSize: Number
});
 
const currentPage = ref(1);
 
const sortedData = computed(() => {
  if (!props.sortable) return props.data;
  // sorting logic using props.data
});
 
const paginatedData = computed(() => {
  const start = (currentPage.value - 1) * props.pageSize;
  return sortedData.value.slice(start, start + props.pageSize);
});
 
watchEffect(() => {
  console.log(`Displaying ${props.data.length} items`);
});
</script>

Every reference to props required the props. prefix. It's readable, but verbose.

After Vue 3.5

<script setup>
const { data, columns, sortable = false, pageSize = 10 } = defineProps({
  data: Array,
  columns: Array,
  sortable: Boolean,
  pageSize: Number
});
 
const currentPage = ref(1);
 
const sortedData = computed(() => {
  if (!sortable) return data;
  // sorting logic using data directly
});
 
const paginatedData = computed(() => {
  const start = (currentPage.value - 1) * pageSize;
  return sortedData.value.slice(start, start + pageSize);
});
 
watchEffect(() => {
  console.log(`Displaying ${data.length} items`);
});
</script>

The difference is subtle but significant. The code reads more naturally. I also get explicit default values right in the destructure, which documents expected behavior better than scattered fallback checks.

Where It Gets Interesting: Composables

The real win is in composables. I write a lot of reusable logic extracted into composable functions, and props destructuring makes these much cleaner.

Here's a composable I use for form validation:

// useFormValidation.js
export function useFormValidation({ fields, rules, validateOn = 'blur' }) {
  const errors = ref({});
 
  const validate = (fieldName) => {
    const rule = rules[fieldName];
    const value = fields[fieldName];
 
    if (rule && !rule(value)) {
      errors.value[fieldName] = `Invalid ${fieldName}`;
    } else {
      delete errors.value[fieldName];
    }
  };
 
  // Can use validateOn directly without .value
  if (validateOn === 'input') {
    // real-time validation
  }
 
  return { errors, validate };
}

I call it from components like this:

<script setup>
const { initialData, validationRules } = defineProps({
  initialData: Object,
  validationRules: Object
});
 
const formData = reactive({ ...initialData });
 
const { errors, validate } = useFormValidation({
  fields: formData,
  rules: validationRules,
  validateOn: 'blur'
});
</script>

The props flow naturally into the composable. Before 3.5, we'd either pass the entire props object or use toRefs() and deal with .value everywhere.

Gotchas We Hit

Rest Syntax Works Differently

You can use rest syntax to grab remaining props:

const { title, ...rest } = defineProps({
  title: String,
  description: String,
  tags: Array
});

But rest is a plain object, not reactive. If you need reactivity for the rest props, stick with the full props object:

const { title } = defineProps({
  title: String,
  description: String,
  tags: Array
});
 
const props = defineProps(); // Keep this if you need reactive access to other props

This caught me once in a wrapper component that passes arbitrary props to a child.

TypeScript Needs Slight Adjustment

My TypeScript setup required a small change to type definitions:

interface Props {
  title: string;
  count?: number;
  enabled?: boolean;
}
 
const { title, count = 0, enabled = true } = defineProps<Props>();

TypeScript correctly infers the types, but you can't use both the runtime props definition and TypeScript types at the same time. Pick one approach.

Not Available in Options API

This is Composition API only. If you're still using Options API (some of my legacy projects are), you can't use reactive props destructuring. That's fine—Options API components have this.propName which is already concise.

Migration Strategy That Worked

I didn't migrate everything overnight. Here's what I did:

  1. New components first: All new components use reactive props destructuring. This let me get comfortable with the syntax without risk.

  2. Touch points during feature work: When I modified existing components for new features, I'd migrate them if the changes were substantial. Small bug fixes didn't warrant migration.

  3. Standardize on defaults: I standardized on always providing default values in the destructure for optional props. It makes components more self-documenting.

  4. Composables got priority: I migrated my shared composables library early because the benefit was most obvious there.

Five months in, about 60% of my Vue codebase uses the new syntax. The remaining 40% is older components that work fine and don't need changes.

The Bigger Picture

Reactive props destructuring is part of Vue 3.5's broader focus on developer experience improvements. I also adopted:

  • useTemplateRef for cleaner template refs
  • Improved TypeScript support for generic components
  • Better SSR hydration mismatch warnings

Together, these changes make Vue feel more polished. The Composition API finally has feature parity with Options API in terms of ergonomics, which is what Vue 3 always needed.

Should You Migrate?

If you're starting a new Vue 3 project, absolutely use reactive props destructuring. It's the cleaner pattern.

For existing projects, migrate gradually as you touch components. Don't do a big-bang migration—the risk isn't worth the reward. Old props.propertyName syntax still works perfectly fine.

The official Vue 3.5 announcement covers all the technical details. The reactivity transform documentation explains how it works under the hood.

Key Takeaways

  • Vue 3.5's reactive props destructuring eliminates the need for toRefs() or props. prefixes
  • Destructure directly in defineProps() and values remain reactive automatically
  • Default values are cleaner and more explicit
  • Composables benefit the most from this syntax
  • Rest syntax and TypeScript have minor gotchas to watch for
  • Migrate gradually—no rush to update existing working code

I'm building several Vue.js applications right now using Vue 3.5, and the developer experience is noticeably better. If you're planning a Vue migration or starting a new project, get in touch to discuss how I can help.

T: 07512 944360 | E: [email protected]

© 2025 amillionmonkeys ltd. All rights reserved.