vuejs/eslint-plugin-vue

`no-setup-props-reactivity-loss` triggered when passing destructured props to composable

Closed this issue · 5 comments

Checklist

  • I have tried restarting my IDE and the issue persists.
  • I have read the FAQ and my problem is not listed.

Tell us about your environment

  • ESLint version: 9.29.0
  • eslint-plugin-vue version: 10.2.0
  • Vue version: 3.5.16
  • Node version: 22
  • Operating System: macOS

Please show your full configuration:

import ts from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import globals from 'globals';

export default ts.config(
  {
    name: 'vue',
    extends: [...ts.configs.recommended, ...vue.configs['flat/recommended']],
    languageOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module',
      globals: globals.browser,
      parserOptions: {
        parser: ts.parser,
      },
    },
    files: ['**/*.vue'],
    rules: {
      'vue/block-order': ['error', { order: ['template', 'script', 'style'] }],
      'vue/block-tag-newline': 'error',
      'vue/component-name-in-template-casing': 'error',
      'vue/component-options-name-casing': 'error',
      'vue/custom-event-name-casing': 'error',
      'vue/define-emits-declaration': 'error',
      'vue/define-macros-order': 'error',
      'vue/define-props-declaration': 'error',
      'vue/match-component-file-name': 'error',
      'vue/match-component-import-name': 'error',
      'vue/multi-word-component-names': 'off',
      'vue/next-tick-style': 'error',
      'vue/no-boolean-default': 'error',
      'vue/no-deprecated-model-definition': 'error',
      'vue/no-duplicate-attr-inheritance': 'error',
      'vue/no-empty-component-block': 'error',
      'vue/no-multiple-objects-in-class': 'error',
      'vue/no-potential-component-option-typo': 'error',
      'vue/no-ref-object-reactivity-loss': 'error',
      'vue/no-required-prop-with-default': 'error',
      'vue/no-reserved-component-names': 'off',
      'vue/no-setup-props-reactivity-loss': 'error',
      'vue/no-template-target-blank': 'error',
      'vue/no-this-in-before-route-enter': 'error',
      'vue/no-use-v-else-with-v-for': 'error',
      'vue/no-useless-mustaches': 'error',
      'vue/no-useless-v-bind': 'error',
      'vue/no-v-text': 'error',
      'vue/padding-line-between-blocks': 'error',
      'vue/prefer-define-options': 'error',
      'vue/prefer-prop-type-boolean-first': 'error',
      'vue/prefer-separate-static-class': 'error',
      'vue/prefer-true-attribute-shorthand': 'error',
      'vue/require-direct-export': 'error',
      'vue/require-explicit-slots': 'error',
      'vue/require-macro-variable-name': 'error',
      'vue/no-undef-properties': 'error',
      'vue/no-unused-emit-declarations': 'error',
      'vue/no-unused-properties': 'error',
      'vue/no-unused-refs': 'error',
    },
  },
);

What did you do?

const { count, maxDigits = undefined } = defineProps<{
  count: number;
  maxDigits?: number;
}>();

const { label } = useMaxCount(count, maxDigits);

where useMaxCount is typed as:

export function useMaxCount(
  number: MaybeRefOrGetter<number | undefined>,
  maxDigits: MaybeRefOrGetter<number | undefined>,
) {
  // implementation
}

What did you expect to happen?

I expected to linting issues.

What actually happened?

I got the following eslint error:

  26:19  error  Getting a value from the `props` in root scope of `<script setup>` will cause the value to lose reactivity  vue/no-setup-props-reactivity-loss

Repository to reproduce this issue

Coming...

This is expected behavior, you've lost reactivity when passing count and maxDigits to your composable since they're transformed to __props.count and __props.maxDigits under the hood respectively. So it's pretty much the same as if you did:

const props = defineProps<...>();
const { label } = useMaxCount(props.count, props.maxDigits);

What you actually want here is:

useMaxCount(() => count, () => maxDigits);

And access your values inside the composable with toValue().

This should not trigger any error as of Vue 3.5 as Vue.js now handles this specific case for us. (https://vuejs.org/guide/components/props.html#reactive-props-destructure)

EDIT: Or maybe we should be able to configure the expected behavior depending on the Vue version as some other rules do.

Vue.js now handles this specific case for us.

No, it doesn't. Vue only handles destructuring itself, but it has nothing to do with the way you pass destructured props down to composables. In this specific example reactivity gets lost and this is why the error is triggered.

You can replace useCount(number) with useCount(() => number) to see the difference.

@bgondy No, reactivity is indeed lost, even with reactive props destructure. See https://vuejs.org/guide/components/props.html#passing-destructured-props-into-functions:

When we pass a destructured prop into a function […] this will not work as expected because it is equivalent to watch(props.foo, ...) - we are passing a value instead of a reactive data source to watch. In fact, Vue's compiler will catch such cases and throw a warning.

See @mattersj's answer above. So I'll close this as the rule is working as intended.

You're right, I didn't understand it this way, my bad.