Hone logo
Hone
Problems

Vue 3 App-Level Provide and Inject with TypeScript

This challenge focuses on mastering Vue 3's provide and inject API at the application level. You will implement a system where a global configuration or service can be provided to any component within your Vue application, promoting cleaner code organization and reducing prop drilling.

Problem Description

Your task is to create a Vue 3 application where a specific piece of data (e.g., an API service, a theme configuration, or a user authentication status) is made available globally to all components. This data should be provided at the application root and then injected into specific child components that need to access it. You will implement this using Vue 3's Composition API's provide and inject functions, ensuring type safety with TypeScript.

Key Requirements:

  • App-Level Provide: The data must be provided at the application level using app.provide().
  • Injection into Components: Demonstrate how to inject this provided data into descendant components.
  • TypeScript for Type Safety: Use TypeScript to define the type of the provided data and ensure correct typing during injection.
  • Clear Separation of Concerns: The providing component (likely your main.ts or a root component) should not directly manage the data's consumption by other components.

Expected Behavior:

  • Components that call inject with the correct symbol will receive the provided data.
  • Components that do not inject the data should not encounter errors, but they will not have access to it.
  • Type errors should be caught by the TypeScript compiler if the injected type doesn't match the provided type.

Examples

Example 1: Providing a Simple Configuration Object

Input:

A Vue application setup in main.ts like this:

// main.ts
import { createApp, inject } from 'vue';
import App from './App.vue';

interface AppConfig {
  appName: string;
  version: string;
}

const appConfig: AppConfig = {
  appName: 'My Awesome App',
  version: '1.0.0',
};

const app = createApp(App);

// Provide the configuration at the app level
app.provide('appConfig', appConfig); // Using a string key for now, we'll improve this

app.mount('#app');

A child component ConfigDisplay.vue:

<template>
  <div>
    <h1>App Configuration</h1>
    <p>App Name: {{ config?.appName }}</p>
    <p>Version: {{ config?.version }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from 'vue';

interface AppConfig {
  appName: string;
  version: string;
}

export default defineComponent({
  setup() {
    // Inject the configuration
    const config = inject<AppConfig>('appConfig'); // String key injection

    return {
      config,
    };
  },
});
</script>

Output:

The ConfigDisplay component renders:

<div>
  <h1>App Configuration</h1>
  <p>App Name: My Awesome App</p>
  <p>Version: 1.0.0</p>
</div>

Explanation:

The appConfig object is provided to the entire application using app.provide. The ConfigDisplay component then uses inject with the string key 'appConfig' to retrieve this data. The inject function is typed with AppConfig to ensure type safety.

Example 2: Using Symbol for Provide Key and Default Value

Input:

Improved main.ts using a Symbol for the provide key:

// main.ts
import { createApp, inject, type InjectionKey } from 'vue';
import App from './App.vue';

interface AppConfig {
  appName: string;
  version: string;
}

const APP_CONFIG_KEY: InjectionKey<AppConfig> = Symbol('appConfig');

const appConfig: AppConfig = {
  appName: 'My Awesome App',
  version: '1.0.0',
};

const app = createApp(App);

// Provide using the Symbol key
app.provide(APP_CONFIG_KEY, appConfig);

app.mount('#app');

Improved ConfigDisplay.vue using the Symbol key and a default value:

<template>
  <div>
    <h1>App Configuration</h1>
    <p>App Name: {{ config?.appName ?? 'N/A' }}</p>
    <p>Version: {{ config?.version ?? 'N/A' }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject, type InjectionKey } from 'vue';

interface AppConfig {
  appName: string;
  version: string;
}

const APP_CONFIG_KEY: InjectionKey<AppConfig> = Symbol('appConfig');

export default defineComponent({
  setup() {
    // Inject using the Symbol key and provide a default
    const config = inject(APP_CONFIG_KEY, { appName: 'Default App', version: '0.0.0' });

    return {
      config,
    };
  },
});
</script>

Output:

The ConfigDisplay component renders:

<div>
  <h1>App Configuration</h1>
  <p>App Name: My Awesome App</p>
  <p>Version: 1.0.0</p>
</div>

Explanation:

Using a Symbol for the injection key prevents potential naming collisions with other string-based injections. The inject function is now strongly typed by passing the APP_CONFIG_KEY directly, and a default value is provided, which will be used if the key is not found.

Example 3: Handling Missing Injection

Input:

A component NoConfigDisplay.vue that tries to inject but the APP_CONFIG_KEY was not provided at the app level:

<template>
  <div>
    <h1>App Configuration (Missing)</h1>
    <p>App Name: {{ config?.appName ?? 'N/A' }}</p>
    <p>Version: {{ config?.version ?? 'N/A' }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject, type InjectionKey } from 'vue';

interface AppConfig {
  appName: string;
  version: string;
}

const APP_CONFIG_KEY: InjectionKey<AppConfig> = Symbol('appConfig');

export default defineComponent({
  setup() {
    // Inject with a default value
    const config = inject(APP_CONFIG_KEY, { appName: 'Default App', version: '0.0.0' });

    return {
      config,
    };
  },
});
</script>

Output:

The NoConfigDisplay component renders:

<div>
  <h1>App Configuration (Missing)</h1>
  <p>App Name: Default App</p>
  <p>Version: 0.0.0</p>
</div>

Explanation:

Since APP_CONFIG_KEY was not provided at the application level, the inject function returns its default value { appName: 'Default App', version: '0.0.0' }. This demonstrates the utility of providing default values for robustness.

Constraints

  • The solution must be implemented using Vue 3 and TypeScript.
  • The provide call must be made at the application level (app.provide).
  • The injected data must be accessible by components that are not direct children of the providing component.
  • Use Symbol as the key for provide and inject for better type safety and to avoid key collisions.
  • Define explicit TypeScript interfaces or types for the data being provided.

Notes

  • Consider using readonly for provided data to ensure it's not accidentally mutated in consuming components.
  • Think about how to handle multiple pieces of data being provided.
  • The inject function can optionally take a second argument: a default value to return if the key is not found. This is crucial for robustness.
  • Using app.config.globalProperties is another way to make things globally available, but provide/inject is generally preferred for explicit dependency injection and better type inference.
Loading editor...
typescript