Vue.js provide/inject

При создании сложных Vue приложений приходится решать много задач по разделению кода на части (компоненты и прочее). Когда мы уже разбили большой кусок кода на некую иерархию компонентов, то обычно передаем вложенным компонентам данные через свойства. Но если глубина, на которую необходимо передать данные вниз по дереву больше 1 уровня, то передавать через свойства не то, что неудобно, но и неправильно.

Неудобно, потому что дублировать свойства приходится на каждом уровне. А неправильно, потому что данные передаются по пути в компоненты, которым эти данные не нужны (они служат лишь посредниками для передачи данных следующим элементам. Чтобы избежать этих проблем можно использовать provide/inject.

Давайте рассмотрим как это сделать на простом примере. Будем использовать Composition API, так старый подход (Options API) стал устаревшим, хоть и поддерживается.

Для демонстрации сделаем 3 компонента:

  • Parent.vue — компонент, который имеет доступ к данным
  • ChildLevel1.vue — вложенный в предыдущий компонент. В примере он будет один, но на практике в место него может быть целая иерархическая цепочка компонентов
  • ChildLevel2.vue — компонент, в который нужно передать данные из Parent.vue

В Parent.vue с помощью provide() мы можем сделать локальные данные компонента доступными в компонентах нижних уровней. В первом параметре provide передается имя, под которым будет доступна величина, а во втором параметре, — переменная или значение, которое будет доступно под этим именем.

<template>
  <div class="parent">
    <h2>Parent</h2>
    <input type="text" v-model="reactiveValue1"/>
    <p>
      <strong>Parent local value #1:</strong> {{ reactiveValue1 }}
    </p>
    <input type="text" v-model="reactiveValue2"/>
    <p>
      <strong>Parent local value #2:</strong> {{ reactiveValue2 }}
    </p>
    <child-level1></child-level1>
  </div>
</template>

import {ref, provide, readonly} from 'vue'
import ChildLevel1 from "./ChildLevel1";

export default {
  components: {ChildLevel1},
  setup() {
    provide("providedStaticValue", 'Hello from parent');

    const reactiveValue1 = ref('Default value for reactive #1')
    provide('providedReactiveValue1', reactiveValue1);

    const reactiveValue2 = ref('Default value for reactive #2')
    provide('providedReactiveValue2', readonly(reactiveValue2));

    const clearReactiveValue2 = () => {
      reactiveValue2.value = ''
    }
    provide('clearReactiveValue', clearReactiveValue2)

    return {
      reactiveValue1,
      reactiveValue2
    }
  }
}

Можно передать как статические данные (см. имя providedStaticValue), так и реактивные — providedReactiveValue1 и providedReactiveValue2.

В ChildLevel1.vue мы не обращаемся к каким-либо данным компонента Parent.vue.

<template>
  <div class="level1">
    <h3>Child Level 1</h3>
    <child-level2></child-level2>
  </div>
</template>
import ChildLevel2 from "./ChildLevel2";

export default {
  components: {ChildLevel2},
}

И, наконец, в компоненте ChildLevel2.vue происходит обращение к данным через inject(), где первым параметром указывается, заданное ранее в Parent.vue, имя, а вторым — значение по-умолчанию.

<template>
  <div class="level2">
    <h4>Child Level 2</h4>
    <p>
        <strong>
            Parent local value injected into level 2:
        </strong> {{ injectedStaticValue }}
    </p>
    <p>
        <strong>
            Parent local reactive value #1 injected into level 2:
        </strong> {{ injectedReactiveValue1 }}
    </p>
    <button @click="clearReactiveValue1">
        Erase parent reactive value #1 direct in level 2
    </button>
    <p>
        <strong>
            Parent local reactive value #2 injected into level 2:
        </strong> {{ injectedReactiveValue2 }}
    </p>
    <button @click="clearReactiveValue2">
        Erase parent reactive value #2 via injected method
    </button>
    <button @click="clearReactiveValue2Broken">
        Erase parent reactive value #2 direct in level 2 (will not work)
    </button>
  </div>
</template>
import {inject} from "vue";

export default {
  setup() {
    const injectedStaticValue = inject("providedStaticValue", 
        'Default value for static if not provided');

    const injectedReactiveValue1 = inject('providedReactiveValue1', 
        'Default for reactive #1');
    const injectedReactiveValue2 = inject('providedReactiveValue2', 
        'Default for reactive #2');

    function clearReactiveValue1 () {
      injectedReactiveValue1.value = ''
    }

    const clearReactiveValue2 = inject("clearReactiveValue")

    function clearReactiveValue2Broken () {
      injectedReactiveValue2.value = ''
    }

    return {
      injectedStaticValue,
      injectedReactiveValue1,
      injectedReactiveValue2,
      clearReactiveValue1,
      clearReactiveValue2,
      clearReactiveValue2Broken
    }
  }
}

И здесь можно увидеть самое главное — как работать с внедренными через inject переменными и значениями.

Во-первых происходит привязка к локальным данным компонента ChildLevel2. Для статических данных — к injectedStaticValue, для реактивных — injectedReactiveValue1 и injectedReactiveValue2.

Статические (не реактивные) данные мы только отображаем в шаблоне (можно использовать в методах как readonly. Реактивные мы можем менять.

Можно прямо изменить локальные данные ChildLevel2 и это изменение отобразится в Parent. Это демонстрируется в методе clearReactiveValue1(). Но в документации рекомендуется все методы, которые меняют свойство компонента, располагать в этом компоненте.

Поэтому правильнее использовать подход, который показан для injectedReactiveValue2. Чтобы изменить эти данные мы внедряем метод из Parent в ChildLevel2 — clearReactiveValue2(). При этом также сделать величину readonly в Parent при вызове provide. Таким образом, метод clearReactiveValue2Broken() в ChildLevel2 уже не сможет поменять реактивную величину. В отладочной консоли браузера будет отображаться предупреждение об этом при попытке вызвать эту функцию.

Подход очень удобный. Но не всегда в таких случаях следует использовать его. Пожалуй, использование оправдано, когда Parent и Child тесно связаны друг с другом, как, например, TabContainer и TabContent или Menu и MenuItem. Когда компоненты планируется распространять в пакете. В ином случае, наверное, правильнее использовать Vuex. Выделив данные в логический модуль, если это возможно.

Полезные ссылки:

Leave a Reply

Ваш адрес email не будет опубликован.