Как деструктурировать пропсы во Vue3 Composition API

31 декабря 2024·

9 минут для чтения

Деструктуризация объектов является очень распространённой практикой в JavaScript, которая позволяет сделать код более понятным за счёт извлечения определённых свойств. Однако, во VueJS, деструктуризация пропсов, нарушает реактивность.

В этой статье вы узнаете, как правильно деструктурировать свойства компонента Vue, чтобы свойства не теряли реактивность.

На одном из собеседований на позицию разработчика Vue, мне задали вопрос о том, возможно ли применить деструктуризацию к пропсам? Тогда я подумал, что раз пропсы являются объектами, а к любому объекту в javascript мы можем применить деструктуризацию, то почему бы и нет?

Но, другой стороны, не просто так же мне задали этот вопрос: значит, здесь есть какие-то особенности. Поэтому я ответил правду, что обычно, я не использую деструктуризацию для пропсов, а обращаюсь к значениям из пропсов напрямую через props точкасвойство

Уже после собеседования, я решил для себя, разобраться, какие же могут быть проблемы?

В однофайловых компонентах, использующих <script setup>, пропсы могут быть объявлены с помощью макроса defineProps()

    
      
<script setup lang="ts">
const props = defineProps()
// ...
</script>
    
  

Здесь props — это реактивный объект, содержащий некоторые кастомные атрибуты, передаваемые компоненту от родительского компонента. Если эти атрибуты в родительском компоненте изменятся, то и реактивный объект props в дочернем компоненте так же должен измениться.

Если в качестве пропсов передать какой-то большой объект, а в дочернем компоненте нам нужно два-три свойства, то вы, возможно, захотите получить доступ просто к этим отдельным двум-трём свойствам, деструкткрировав props. Однако, вы здесь получите неприятный сюрприз в виде потери реактивности, т. е. деструктурированные пропсы потеряют свою реактивность!

Рассмотрим пример. У нас есть дочерний компонент DoubledCounter.vue, который принимает в качестве пропса значение count как число и отображает удвоенное принимаемое значение.

Обычная деструктуризация пропсов (с потерей реактивности)

Доступ к свойству count осуществляется после деструктуризации объекта props const { count } = defineProps():

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

const props = defineProps<{ count: number }>()
const { count } = props; // Здесь теряется реактивность
const doubledCounter = computed(() => (count * 2));
</script>

<template> Удвоенный счётчик {{ doubledCounter }} </template>
    
  
Открыть демо ≫

Откройте демо и нажмите несколько раз кнопку уменьшения или увеличения счётчика. Вы заметите, что значение "Удвоенный счётчик: " остаётся равным нулю и не меняется, несмотря на то, что сам count, который мы передаём в качестве props меняется.

Таким образом, если мы для props используем обычную деструктуризацию через const { count } = defineProps(), реактивность теряется.

Реактивность теряется, поскольку при обычной деструктуризации, count становится обычной переменной, имеющей примитивное значение (число). Но реактивность Vue не может работать напрямую с примитивными значениями: она работает либо с использованием ref , либо с реактивным объектом reactive.

Ниже рассмотрим варианты решения данной проблемы.

Решение 1. Обращаться к объекту props напрямую

Первое очевидное решение, которым я всегда пользовался (и продолжаю пользоваться) — это НЕ деструктурировать объект props, а получать доступ к свойствам напрямую, используя метод доступа к свойствам объекта: props.count.

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

const props = defineProps<{ count: number }>();
const doubledCounter = computed(() => (props.count * 2));
</script>

<template> Удвоенный счётчик {{ doubledCounter }} </template>
    
  
Открыть демо ≫

В приведенном выше примере доступ props.count внутри computed(), сохраняет реактивность при изменениях props.count. Объект props.count является реактивным, и любые изменения в нем отслеживаются правильно.

Недостатком этого подхода является то, что вам всегда придется использовать метод доступа к свойству (например props.count ), для доступа к свойству внутри <script setup>.

Решение 2. Использовать помощник toRefs()

Если всё же хочется использовать деструктуризацию, тогда вы можете сохранить реактивность деструктурированных props, намеренно преобразуя каждое свойство объекта props в ссылку. Vue предоставляет для этого специальный помощник toRefs(), который делает именно это.

    
      
<script setup lang="ts">
import { toRefs, computed } from 'vue';

const props = defineProps<{ count: number }>();
const { count } = toRefs(props);
const doubledCounter = computed(() => (count.value * 2));
</script>

<template> Удвоенный счётчик {{ doubledCounter }} </template>
    
  
Открыть демо ≫

toRefs(props) возвращает объект, где каждое свойство является ссылкой на соответствующее свойство.

Теперь деструктуризация const { count } = toRefs(props) безопасна, потому что count является ссылкой на props.count. Теперь, каждый раз, когда props.count изменяется, ссылка count реагирует на изменение props.

Имея count в качестве ссылки, внутри computed() вам нужно получить доступ к значению свойства, используя count.value (потому что count.value именно так вы получаете доступ к значению ссылки).

Решение 3. Деструктуризация пропсов с помощью vite

Если вы в своем проекте Vue.js, в качестве сборщика, используете Vite вы можете использовать опцию времени компиляции Vite. Начиная с Vue.js 3.3, вы можете включить конфигурацию destructureProps: true. Эта опция включает деструктуризацию свойств и назначение значений по умолчанию props внутри вашего блока <script setup>.

Вот конфигурация Vite, которую вам нужно применить для деструктуризации пропсов:

    
      
/**
 * @see https://vitejs.dev/config/
 */
export default defineConfig({  
  plugins: [
    vue({
      script: {
        propsDestructure: true,
      }
    })
  ]
})
    
  

Примечание: вам может потребоваться настроить конфигурацию ESLint, чтобы принимать деструктурированные свойства в корне вашего блока <script setup>:

    
      
{
  "rules": {
    "vue/no-setup-props-destructure": "off"
  }
}
    
  

Отсюда вы можете деструктурировать свойства внутри вашего блока <script setup>, а также назначить значения по умолчанию:

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

const { count } = defineProps<{ count: number }>(); // Здесь уже  реактивность НЕ теряется
const doubledCounter = computed(() => (count * 2));
</script>

<template> Удвоенный счётчик {{ doubledCounter }} </template>
    
  
Открыть демо ≫

Заключение

Резюмируя всё, что написано выше, что мы имеем?

Применяя деструктуризацию, const { propA, propB } = defineProps() вы теряете реактивность пропсов.

Я нашёл три основных подхода к решению проблемы потери реактивности.

Первый способ, который я использовал всегда и продолжаю использовать и предлагаю всем — это просто не деструктурировать свойства, а получить к ним прямой доступ с помощью метода доступа к свойствам: props.propA, props.propsB.

Второй подход подразумевает намеренное использование props как объекта refs: { propA, propB } = toRefs(props) . Это сохраняет реактивность после деструктуризации. Затем вы можете получить доступ к свойствам как к автономным refs, например propsA.value, propB.value, и т.д.

И третий способ (только для версий Vue старше 3.3) - это деструктуризация props с помощью vite.