Kurs Vue.js · Moduł 7: Props i zdarzenia
Provide / Inject
W tej lekcji5
Gdy NOVA LAB nadaje komunikat dla całej kolonii, nie przesyła go przez każdy moduł z osobna - używa systemu broadcast. W Vue ten mechanizm to provide/inject - przekazywanie danych przez wiele poziomów bez prop drilling.
Weźmy motyw kolorystyczny paneli. Ustawia go centrala w komponencie App, a potrzebuje go przycisk głęboko w pasku bocznym. Z samymi props motyw musiałby przejść przez Layout, Page i Sidebar, choć żaden z nich go nie używa - każdy tylko podaje go dalej. To właśnie prop drilling: kod pośredników puchnie, a zmiana nazwy propsa oznacza poprawki na każdym piętrze stacji.
Problem Prop Drilling
Bez provide/inject musisz przekazywać props przez każdy poziom, nawet jeśli pośrednicy wcale ich nie potrzebują:
1App → Layout → Page → Sidebar → DeepChild
2(theme) (theme) (theme) (theme) (theme)Z provide/inject przodek nadaje wartość raz, a odbiera ją tylko ten potomek, który naprawdę jej potrzebuje:
1App (provide theme) → ... → DeepChild (inject theme)Pośrednicy zostają nietknięci: nie deklarują propsa theme i nawet nie wiedzą, że sygnał przez nich przechodzi.
Podstawy Provide/Inject
Funkcja provide(klucz, wartość) wywołana w przodku udostępnia wartość całemu jego poddrzewu, a funkcja inject(klucz) w dowolnym potomku ją odbiera. Kluczem jest napis albo Symbol, a drugi argument inject to wartość domyślna na wypadek, gdyby żaden przodek niczego nie nadał. Obie funkcje importujesz z vue i wywołujesz synchronicznie w <script setup>:
1<!-- Provider (przodek) -->
2<script setup>
3import { provide, ref } from 'vue'
4
5const theme = ref('dark')
6provide('theme', theme)
7provide('labName', 'NOVA LAB Alpha')
8</script>
9
10<!-- Consumer (dowolny potomek) -->
11<script setup>
12import { inject } from 'vue'
13
14const theme = inject('theme')
15const labName = inject('labName')
16
17// Z domyślną wartością
18const mode = inject('mode', 'standard')
19</script>Jeśli potomek poprosi o klucz, którego nikt nie udostępnił, i nie poda wartości domyślnej, dostanie undefined, a Vue wypisze w konsoli ostrzeżenie. Dlatego mode ma tu wartość domyślną 'standard', a pozostałe dwa klucze pochodzą od przodka.
Reaktywne Provide
provide przekazuje wartość dosłownie. Zwykły napis dotrze do potomka jako napis i nie pokaże późniejszych zmian. Przekazuj ref() lub reactive(), aby dane były reaktywne - potomek dostanie wtedy ten sam obiekt i zobaczy każdą zmianę. Funkcja readonly opakowuje go w wersję tylko do odczytu:
1<!-- Provider -->
2<script setup>
3import { provide, ref, readonly } from 'vue'
4
5const count = ref(0)
6
7// Readonly - potomek nie może modyfikować
8provide('count', readonly(count))
9
10// Akcje do modyfikacji
11provide('increment', () => count.value++)
12</script>
13
14<!-- Consumer -->
15<script setup>
16import { inject } from 'vue'
17
18const count = inject('count') // readonly ref
19const increment = inject('increment') // funkcja
20</script>Potomek czyta count i odświeża się razem z nim, ale próba przypisania skończy się ostrzeżeniem w konsoli, a wartość się nie zmieni. Zmiany idą przez udostępnioną funkcję increment, więc mutacja stanu zostaje u dostawcy. W szablonie ref rozpakuje się sam, a w skrypcie odczytasz go przez count.value.
App-level Provide
Globalne provide w main.ts udostępnia wartość wszystkim komponentom aplikacji. Metodę provide ma sam obiekt aplikacji zwrócony przez createApp:
1const app = createApp(App)
2app.provide('apiUrl', 'https://mars-api.novalab.space')
3app.mount('#app')Wywołanie stoi przed app.mount, tak jak rejestracja wtyczek. Z tego mechanizmu korzystają zresztą same wtyczki, bo nie mają własnego komponentu, w którym mogłyby wywołać provide.
Typowane Klucze (InjectionKey)
W dużej aplikacji dwa zespoły mogą nieświadomie użyć tego samego klucza tekstowego. Dokumentacja poleca wtedy klucze typu Symbol, eksportowane z jednego pliku. W TypeScript typ InjectionKey dopina do symbolu typ przesyłanej wartości. Fragmenty Provider i Consumer poniżej pochodzą z dwóch różnych komponentów:
1import type { InjectionKey, Ref } from 'vue'
2
3export const themeKey: InjectionKey<Ref<string>> = Symbol('theme')
4
5// Provider
6provide(themeKey, ref('dark'))
7
8// Consumer - TypeScript wie, że to Ref<string> | undefined
9const theme = inject(themeKey)Wynik inject ma w typie dopisek undefined, bo kompilator nie ma pewności, że któryś przodek wywołał provide. Wystarczy podać wartość domyślną, na przykład inject(themeKey, ref('light')), a typ zawęzi się do Ref<string>. Z kolei provide(themeKey, 'dark') z samym napisem zostanie odrzucone już przy kompilacji.
Moja rada: provide i inject rezerwuj dla danych potrzebnych całemu poddrzewu - motywu, zalogowanego członka załogi, konfiguracji. Dla relacji rodzic-dziecko props pozostają czytelniejsze. Mutacje zawsze trzymaj u dostawcy: stan udostępniaj przez readonly, a zmiany przez funkcje. W edytorze poniżej MissionControl stoi pośrodku i nie przekazuje żadnych props, a DeepChild i tak odbiera motyw. Ten wzorzec rozwiniesz w lekcji o zaawansowanych wzorcach komunikacji.
Zapamiętaj: provide to nadajnik rozgłoszeniowy stacji, a inject to odbiornik, który zamontujesz na dowolnym piętrze poniżej.
Kod do tej lekcji: App.vue
1<script setup>
2import { provide, ref, readonly } from 'vue'
3import MissionControl from './MissionControl.vue'
4
5const theme = ref('dark')
6const missionMode = ref('eksploracja')
7
8// Provide reaktywnych danych (readonly)
9provide('theme', readonly(theme))
10provide('missionMode', readonly(missionMode))
11
12// Provide akcji do modyfikacji
13provide('toggleTheme', () => {
14 theme.value = theme.value === 'dark' ? 'light' : 'dark'
15})
16provide('setMission', (mode) => {
17 missionMode.value = mode
18})
19</script>
20
21<template>
22 <div class="nova-lab" :class="theme">
23 <h1>Demo: provide / inject</h1>
24 <p>Motyw: {{ theme }} | Misja: {{ missionMode }}</p>
25
26 <button @click="theme = theme === 'dark' ? 'light' : 'dark'">
27 Przełącz motyw
28 </button>
29
30 <MissionControl />
31 </div>
32</template>
33
34<style scoped>
35.nova-lab {
36 min-height: 100vh;
37 padding: 20px;
38 font-family: monospace;
39}
40.nova-lab.dark {
41 background: #0a0e27;
42 color: #00ff88;
43}
44.nova-lab.light {
45 background: #f0f4f8;
46 color: #1a1a2e;
47}
48h1 { margin-bottom: 15px; }
49button {
50 background: #00b4d8;
51 color: #0a0e27;
52 border: none;
53 padding: 8px 16px;
54 cursor: pointer;
55 margin: 10px 5px;
56 font-family: monospace;
57}
58</style>Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Do czego służy provide/inject w Vue?
2. Jak zapewnić reaktywność w provide/inject?
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj kroki użycia provide/inject:
- Edytor kodu
Użyj provide/inject do przekazania theme (dark/light) w całej aplikacji.
- Klikanie w kolejności
Ułóż kod reaktywnego provide:
- Edytor kodu
Stwórz provider z reaktywnym stanem marsWeather (ref) i metodą updateWeather.