Kurs Vue.js · Moduł 7: Props i zdarzenia

Provide / Inject

4 min czytania
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. 1. Do czego służy provide/inject w Vue?

  2. 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.

Przydatne artykuły