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.
Ниже рассмотрим варианты решения данной проблемы.
Первое очевидное решение, которым я всегда пользовался (и продолжаю пользоваться) — это НЕ деструктурировать объект 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>.
Если всё же хочется использовать деструктуризацию, тогда вы можете сохранить реактивность деструктурированных 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 именно так вы получаете доступ к значению ссылки).
Если вы в своем проекте 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.