The new Provide and Inject in Vue 3

In Vue we use props to pass data between components.

However, in more complex applications, we may need to pass data across multiple layers to a lower level component. In such cases, components pass certain props down without needing them themselves. This pattern is known as prop drilling.

For developers of plugins or component libraries, problems also arise with providing data. These are even more severe because of the lack of access to the consuming application.

These problems can be bypassed by the two methods provide and inject. Through so-called dependency providers, data can be passed to any lower-level component, no matter how deep it is. Completely without prop drilling.

import { provide } from 'vue'

export default {
  setup() {
    provide(/* key */ 'count', /* value */ 0)
  }
}
<Parent>
  <Child>
    <Grandchild><Grandchild>
  </Child>
</Parent>
import { inject } from 'vue'

export default {
  setup() {
    const count = inject(/* key */ 'count')
    console.log(count) // 0
  }
}

However, this becomes particularly useful when working with reactive data and methods. For example, consider a table component whose columns can be defined by components rather than props:

<MyTable>
  <MyColumn key="id" label="Identifier" />
  <MyColumn key="name" label="First name" :sortable="true" />
  <MyColumn key="email" label="Email address" :sortable="true" />
</MyTable>

We could create such functionality by using provideand inject:

import { provide, ref } from 'vue'

export default {
  setup() {
    const columns = ref<Column[]>([])
    
    provide('TableKey', {
      columns,
    })
  }
}
import { inject } from 'vue'

export default {
  props: ['key', 'label', 'sortable']
  setup(props) {
    const table = inject('TableKey')
    table?.columns.push(props)
  }
}

If we take a closer look at this example, we can see several pitfalls that should be avoided right from the start. For this, Vue offers some recommendations and optimization options.

# Symbol and relocation of keys

The key used is specified as an inline string and therefore does not scale well. We cannot ensure that nobody else uses this key or that we do not mistype. Therefore it is a good idea to store and export the keys in a separate file and make them more collision-proof by using a symbol declaration (see MDN).

// keys.ts
export const MY_TABLE_KEY = Symbol('MY_TABLE_KEY')

// in provider component
import { MY_TABLE_KEY } from '@/keys'
provide(MY_TABLE_KEY, {...})

// in injector component
import { MY_TABLE_KEY } from '@/keys'
const injected = inject(MY_TABLE_KEY)

# Data typing

However, the use of constants does not help with proper typing. provide and inject are usually defined in different components, so we can never really be sure in both places that we are not mistyping and using the data correctly. Did we really name the variable in MyTable columns or was it cols after all?

Therefore the type InjectionKey should be used. This will ensure synchronization of the type between the provider and consumer.

Translated with www.DeepL.com/Translator (free version)

// keys.ts
import { InjectionKey } from 'vue';
import { TableConfig } from '@/types';
export const MY_TABLE_KEY: InjectionKey<TableConfig> = Symbol('MY_TABLE_KEY');

// in provider component... Type Error
provide(MY_TABLE_KEY, {});

// in injector component
const injected = inject(MY_TABLE_KEY) // typed as TableConfig | undefined
injected.columns // safe access through autocompletion

# Default values and guaranteed provision of data

Since there is no guarantee that provide was actually called further up the tree, the return value of inject is always nullable. inject allows a second parameter, which makes it possible to specify a default value, but this does not help us to map the desired functionality and is only useful for static data.

Therefore it is recommended to write a helper function for inject which checks if the data was really provided and throws an exception otherwise.

function requireInjection<T>(key: InjectionKey<T>, defaultValue?: T) {
  const resolved = inject(key, defaultValue);
  if (!resolved) {
    throw new Error(`${key} was not provided.`);
  }
  return resolved;
}

If we now use this helper function in MyColumn, we can access the data completely safely.

import { inject } from 'vue'
import { MY_TABLE_KEY } from '@/keys'
import { requireInjection } from '@/utils'

export default {
  props: ['key', 'label', 'sortable']
  setup(props) {
    const table = requireInjection(MY_TABLE_KEY)
    // autocompletion and without optional chaining, since it is safe to use
    table.register(props) 
  }
}

# Immutability

Finally, we turn our attention to the reactive property columns. The provided data should always be protected from direct manipulation to ensure traceability and a consistent data flow. For this we use the readonly function. Functions (see register) should be provided by which a controlled manipulation in the provider can be guaranteed.

import { provide, ref, readonly } from 'vue'
import { MY_TABLE_KEY } from '@/keys'

export default {
  setup() {
    const columns = ref([])

    function register(column: Column) {
      columns.value.push(column)
    }
    
    provide('TableKey', {
      columns: readonly(columns),
      register
    })
  }
}

# Wrapping Up

At this point, you already know how you can make your components more flexible and independent by using provide/inject. Tell me, where do you plan to use them?

In Vue.js Conf we'll cover this topic in depth and many others, from Pinia by its author Eduardo (Posva) to testing and sharing all news on the Vue ecosystem.

Will I see you in Berlin? 😉 As a VueDose reader, grab your 20% discounted ticket if you don't have it yet!

Don't miss out anything about Vue, your favourite framework.

Subscribe to receive all the articles we publish in a concise format, perfect for busy devs.

Related Articles

Data Provider component in Vue.js

Use scoped slots to create a data provider in Vue.js

Alex Jover Morales

Alex Jover Morales

Sep 24, 2019

Using Scoped Slots in Vue.js

Quick example on how to use scoped slots for component reusability in vuejs

Alex Jover Morales

Alex Jover Morales

Sep 15, 2019

Create an ImageSelect component on top of vue-multiselect

Build an ImageSelect Vue.js component using the popular vue-multiselect package following the Adaptive Components pattern.

Alex Jover Morales

Alex Jover Morales

Feb 24, 2019

Sponsors

VueDose is proudly supported by its sponsors. If you enjoy it, consider supporting it to ensure the project maintainability.

Silver
Learning Partner