JAVASCRIPT
Managing Global Theming with Provide/Inject in Deep Component Trees
Utilize Vue 3's `provide` and `inject` to efficiently pass theme-related data down to deeply nested components without prop drilling, maintaining clean and scalable code.
// Composables/useTheme.js
import { inject, provide, ref, readonly } from 'vue'
const ThemeSymbol = Symbol('theme')
export function provideTheme(initialTheme = 'light') {
const theme = ref(initialTheme)
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// Provide a readonly version of theme and the toggle function
provide(ThemeSymbol, { theme: readonly(theme), toggleTheme })
}
export function useTheme() {
const themeContext = inject(ThemeSymbol)
if (!themeContext) {
throw new Error('useTheme must be used within a component that provides a theme.')
}
return themeContext
}
// App.vue (Root component)
<template>
<div :class="['app-container', theme]">
<h1>Vue 3 Theming Example</h1>
<button @click="toggleTheme">Toggle Theme (Current: {{ theme }})</button>
<ParentComponent />
</div>
</template>
<script setup>
import { provideTheme, useTheme } from './composables/useTheme'
import ParentComponent from './components/ParentComponent.vue'
import { onMounted } from 'vue'
// Provide theme context at the root
provideTheme('light')
// Use theme context in the root component itself
const { theme, toggleTheme } = useTheme()
// Example of setting initial theme from localStorage
onMounted(() => {
const savedTheme = localStorage.getItem('app-theme')
if (savedTheme && savedTheme !== theme.value) {
theme.value = savedTheme // Directly update ref, or call a specific action if the composable had one.
}
})
</script>
<style>
.app-container.light { background-color: #f0f2f5; color: #333; }
.app-container.dark { background-color: #333; color: #f0f2f5; }
button { margin: 10px; padding: 8px 15px; }
</style>
// components/ParentComponent.vue
<template>
<div class="parent-card">
<h2>Parent Component</h2>
<ChildComponent />
</div>
</template>
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>
<style scoped>
.parent-card { border: 1px solid #ccc; padding: 15px; margin: 10px; background-color: var(--card-bg-color); }
.app-container.light .parent-card { --card-bg-color: #fff; }
.app-container.dark .parent-card { --card-bg-color: #555; }
</style>
// components/ChildComponent.vue
<template>
<div class="child-card">
<h3>Child Component</h3>
<p>Current Theme: {{ theme }}</p>
<GrandchildComponent />
</div>
</template>
<script setup>
import { useTheme } from '../composables/useTheme'
import GrandchildComponent from './GrandchildComponent.vue'
const { theme } = useTheme()
</script>
<style scoped>
.child-card { border: 1px dashed #aaa; padding: 10px; margin: 10px; background-color: var(--card-bg-color); }
.app-container.light .child-card { --card-bg-color: #f7f7f7; }
.app-container.dark .child-card { --card-bg-color: #666; }
</style>
// components/GrandchildComponent.vue
<template>
<div class="grandchild-box">
<h4>Grandchild Component</h4>
<p>Theme from inject: {{ theme }}</p>
<button @click="toggleTheme">Toggle Theme from Grandchild</button>
</div>
</template>
<script setup>
import { useTheme } from '../composables/useTheme'
const { theme, toggleTheme } = useTheme()
</script>
<style scoped>
.grandchild-box { border: 1px dotted #888; padding: 8px; margin: 8px; background-color: var(--card-bg-color); }
.app-container.light .grandchild-box { --card-bg-color: #eee; }
.app-container.dark .grandchild-box { --card-bg-color: #777; }
</style>
How it works: This snippet demonstrates the `provide`/`inject` mechanism in Vue 3 for managing global state like a theme across deeply nested components without cumbersome prop drilling. A custom composable `useTheme.js` defines a `Symbol` for the injection key, `provideTheme` to establish the theme context with a reactive `ref` and a `toggleTheme` function at a parent level (typically the root `App.vue`), and `useTheme` to inject this context in any descendant component, regardless of its depth. By making the provided `theme` `readonly`, we ensure that only the `toggleTheme` action can modify it, maintaining data flow predictability.