Kurs Vue.js · Moduł 3: Reaktywność

Reaktywność w Praktyce - Panel Kontrolny

6 min czytania
W tej lekcji5

Połączmy wszystko w praktycznych przykładach systemów NOVA LAB.

Do tej pory każde narzędzie reaktywności oglądałeś osobno. Prawdziwe panele stacji łączą je naraz: formularz konfiguracji musi walidować dane w locie, monitor modułów filtruje listę według wpisanej frazy, a system zaopatrzenia liczy sumy zamówienia. Wszystkie trzy panele poniżej mają ten sam układ warstw. Najpierw dane w ref() lub reactive(), potem wartości wyliczane przez computed(), dalej akcje, czyli funkcje zmieniające stan, a na końcu szablon z reaktywnymi wiązaniami.

Panel Konfiguracji Reaktora z Walidacją

Szablon formularza używa v-model, czyli wiązania dwukierunkowego pola z danymi, które szczegółowo poznasz w module 5. Modyfikator .number zamienia wpisany tekst na liczbę, a @submit.prevent i :disabled znasz z modułu 2:

1<template>
2  <form @submit.prevent="submitConfig" class="reactor-config" style="background: #0a0e27; color: #00ff88;">
3    <div>
4      <label>Nazwa modułu:</label>
5      <input v-model="config.name" style="background: #1a1e37; color: #00ff88;" />
6      <span v-if="errors.name" style="color: #ff4444;">{{ errors.name }}</span>
7    </div>
8
9    <div>
10      <label>Temperatura maksymalna (K):</label>
11      <input v-model.number="config.maxTemp" type="number" style="background: #1a1e37; color: #00ff88;" />
12      <span v-if="errors.maxTemp" style="color: #ff4444;">{{ errors.maxTemp }}</span>
13    </div>
14
15    <div>
16      <label>Status:</label>
17      <input v-model="config.status" style="background: #1a1e37; color: #00ff88;" />
18    </div>
19
20    <button :disabled="!isValid" style="background: #00b4d8; color: #0a0e27;">Zapisz konfigurację</button>
21  </form>
22</template>

Komunikaty błędów pojawiają się przez v-if tylko wtedy, gdy jest co pokazać, a przycisk zapisu blokuje się, dopóki formularz nie jest poprawny. Dane formularza to spójna grupa pól, więc trzymamy je w reactive():

1<script setup>
2import { reactive, computed } from 'vue'
3
4const config = reactive({
5  name: '',
6  maxTemp: null,
7  status: ''
8})

Walidacja to wartość wyliczana z tych pól. W pierwszej wersji tego panelu computed sam zapisywał komunikaty do osobnego obiektu errors. Dokumentacja Vue zabrania jednak efektów ubocznych w computed: getter ma tylko liczyć i zwracać wynik. Dlatego same błędy są tutaj computed:

1// Walidacja jako wartość wyliczana - bez efektów ubocznych
2const errors = computed(() => ({
3  // Walidacja nazwy
4  name: config.name.length < 3 ? 'Nazwa musi mieć min. 3 znaki' : '',
5  // Walidacja temperatury
6  maxTemp: config.maxTemp && (config.maxTemp < 1000 || config.maxTemp > 10000)
7    ? 'Temperatura musi być między 1000 a 10000 K'
8    : ''
9}))
10
11const isValid = computed(() => !errors.value.name && !errors.value.maxTemp)

Szablon nie wymagał żadnej zmiany: errors to ref na najwyższym poziomie, więc errors.name działa w nim bez .value. Wpisanie trzeciego znaku nazwy przelicza oba computed i odblokowuje przycisk. Akcja wysyłająca konfigurację czyta już tylko gotowy wynik:

1function submitConfig() {
2  if (isValid.value) {
3    console.log('Wysyłanie konfiguracji do NOVA LAB:', config)
4  }
5}
6</script>

System Monitoringu Modułów

Monitor łączy pole wyszukiwania i listę rozwijaną z przefiltrowaną listą modułów. v-model działa także na <select>:

1<template>
2  <div class="module-monitor" style="background: #0a0e27; color: #00ff88;">
3    <input v-model="searchQuery" placeholder="Szukaj modułu..." style="background: #1a1e37; color: #00ff88;" />
4    <select v-model="filterStatus" style="background: #1a1e37; color: #00ff88;">
5      <option value="">Wszystkie statusy</option>
6      <option value="ACTIVE">Aktywne</option>
7      <option value="STANDBY">Gotowość</option>
8    </select>
9
10    <ul>
11      <li v-for="module in filteredModules" :key="module.id" style="border-bottom: 1px solid #00b4d8;">
12        {{ module.name }} - {{ module.status }}
13      </li>
14    </ul>
15
16    <p>Znaleziono: {{ filteredModules.length }} modułów</p>
17  </div>
18</template>

Szablon nie filtruje niczego sam - dostaje gotową listę filteredModules. Stan to dwa proste refy i tablica modułów:

1<script setup>
2import { ref, computed } from 'vue'
3
4const searchQuery = ref('')
5const filterStatus = ref('')
6
7const modules = ref([
8  { id: 1, name: 'Oxygen Generator Alpha', status: 'ACTIVE' },
9  { id: 2, name: 'Plasma Core Beta', status: 'STANDBY' },
10  { id: 3, name: 'Water Recycler Gamma', status: 'ACTIVE' },
11  { id: 4, name: 'Power Distributor Delta', status: 'STANDBY' }
12])

Filtr łączy oba warunki. Zapis !filterStatus.value || znaczy: gdy nie wybrano statusu, przepuść każdy moduł:

1const filteredModules = computed(() => {
2  return modules.value.filter(module => {
3    const matchesSearch = module.name
4      .toLowerCase()
5      .includes(searchQuery.value.toLowerCase())
6
7    const matchesStatus = !filterStatus.value ||
8      module.status === filterStatus.value
9
10    return matchesSearch && matchesStatus
11  })
12})
13</script>

Oryginalna tablica modules pozostaje nietknięta, a lista na ekranie odświeża się przy każdym wpisanym znaku.

System Zaopatrzenia Misji

Zamówienie zaopatrzenia pokazuje liczbę sztuk, pozycje z przyciskiem usuwania i sumę w kredytach:

1<template>
2  <div class="supply-cart" style="background: #0a0e27; color: #00ff88;">
3    <h2>Zamówienie ({{ supplies.itemCount }} pozycji)</h2>
4
5    <div v-for="item in supplies.items" :key="item.id" style="border-bottom: 1px solid #00b4d8;">
6      {{ item.name }} - {{ item.quantity }} x {{ item.price }} CR
7      <button @click="removeItem(item.id)" style="background: #ff4444; color: #fff;">Usuń</button>
8    </div>
9
10    <p><strong>Suma: {{ supplies.total }} CR (Kredyty)</strong></p>
11  </div>
12</template>

Tutaj wartości wyliczane zapisano inaczej niż w poprzednich panelach: jako gettery wewnątrz samego obiektu reaktywnego. Getter to właściwość JavaScriptu, której wartość przy każdym odczycie liczy funkcja oznaczona słowem get:

1<script setup>
2import { reactive } from 'vue'
3
4const supplies = reactive({
5  items: [
6    { id: 1, name: 'Oxygen Canister', quantity: 10, price: 50 },
7    { id: 2, name: 'Plasma Cell', quantity: 5, price: 200 }
8  ],
9
10  // Computed jako właściwość
11  get itemCount() {
12    return this.items.reduce((sum, item) => sum + item.quantity, 0)
13  },
14
15  get total() {
16    return this.items.reduce((sum, item) =>
17      sum + item.quantity * item.price, 0)
18  }
19})

Na starcie itemCount wynosi 15, a total 1500 CR. Getter liczy wynik przy każdym odczycie i w przeciwieństwie do computed() go nie zapamiętuje, co przy krótkiej liście nie ma znaczenia. Akcja usuwania zmienia tablicę w miejscu:

1function removeItem(id) {
2  const index = supplies.items.findIndex(item => item.id === id)
3  if (index > -1) {
4    supplies.items.splice(index, 1)
5  }
6}
7</script>

Po usunięciu ogniw plazmowych suma spada do 500 CR, bo splice to metoda, którą Vue wykrywa.

readonly() - Podgląd bez Prawa Zapisu

Monitor w mesie ma pokazywać stan reaktora, ale nie może go zmieniać. readonly() zwraca głębokie Proxy tylko do odczytu:

1import { reactive, readonly } from 'vue'
2
3const reactorState = reactive({ temperature: 2500 })
4const reactorView = readonly(reactorState)
5
6reactorView.temperature = 9999 // ostrzeżenie w konsoli, zmiana odrzucona
7reactorState.temperature = 2600 // zmiana przez oryginał działa
8console.log(reactorView.temperature) // 2600

Próba zapisu kończy się w trybie deweloperskim ostrzeżeniem Vue, a wartość zostaje bez zmian. Widok tylko do odczytu nie jest kopią: pokazuje każdą zmianę oryginału. Wzorzec jest prosty - modyfikowalny stan trzymasz u siebie, na zewnątrz udostępniasz wersję readonly, a zmiany robisz wyłącznie przez własne funkcje.

Dobór Narzędzi

Dokumentacja Vue zaleca ref() jako podstawowe narzędzie. reactive() sprawdza się przy spójnych grupach pól, shallowRef() z triggerRef() przy ogromnych listach, markRaw() przy instancjach z bibliotek zewnętrznych, a readonly() przy udostępnianiu stanu. Pod spodem Vue 3 przechwytuje dostęp do obiektów natywnym Proxy, a do refów getterami i setterami. Trzymaj się podziału na dane, wartości wyliczane, akcje i szablon a każdy panel będzie czytelny. W projekcie modułu zbudujesz z tych warstw pełny system monitorowania reaktora.

Zapamiętaj: dobry panel stacji ma jedno źródło prawdy, wartości wyliczane zamiast ręcznych kopii i akcje, które jako jedyne dotykają stanu.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref, reactive, markRaw, isReactive, toRaw } from 'vue'
3
4// markRaw() - Kontrola reaktora plazmowego
5// Zaawansowana kontrola reaktywności w NOVA LAB
6
7// Symulator dużego obiektu (np. biblioteka 3D)
8class HeavySimulationEngine {
9  constructor(name) {
10    this.name = name
11    this.iterations = 0
12    this.data = new Array(1000).fill(0).map((_, i) => ({
13      id: i,
14      value: Math.random() * 100
15    }))
16  }
17
18  simulate() {
19    this.iterations++
20    // Ciężkie obliczenia
21    return this.data.reduce((sum, item) => sum + item.value, 0)
22  }
23
24  reset() {
25    this.iterations = 0
26  }
27}
28
29// Reactor z normalnym obiektem (reaktywny)
30const reactiveReactor = reactive({
31  id: 'REACTOR-A',
32  status: 'ONLINE',
33  power: 100,
34  engine: new HeavySimulationEngine('Reactive Engine'),
35  lastUpdate: new Date().toISOString()
36})
37
38// Reactor z markRaw (silnik nie reaktywny)
39const optimizedReactor = reactive({
40  id: 'REACTOR-B',
41  status: 'ONLINE',
42  power: 100,
43  engine: markRaw(new HeavySimulationEngine('Optimized Engine')),
44  lastUpdate: new Date().toISOString()
45})
46
47// Sprawdzenie reaktywności
48const reactiveEngineIsReactive = isReactive(reactiveReactor.engine)
49const optimizedEngineIsReactive = isReactive(optimizedReactor.engine)
50
51// Counters dla demonstracji wydajności
52const reactiveSimulationCount = ref(0)
53const optimizedSimulationCount = ref(0)
54
55function runReactiveSimulation() {
56  const start = performance.now()
57
58  // Modyfikacja reaktywnego silnika
59  reactiveReactor.engine.simulate()
60  reactiveReactor.lastUpdate = new Date().toISOString()
61  reactiveSimulationCount.value++
62
63  const end = performance.now()
64  console.log(`Reactive simulation: ${(end - start).toFixed(2)}ms`)
65}
66
67function runOptimizedSimulation() {
68  const start = performance.now()
69
70  // Modyfikacja markRaw silnika
71  optimizedReactor.engine.simulate()
72  optimizedReactor.lastUpdate = new Date().toISOString()
73  optimizedSimulationCount.value++
74
75  const end = performance.now()
76  console.log(`Optimized simulation: ${(end - start).toFixed(2)}ms`)
77}
78
79// Przykład praktyczny - mapa z wieloma obiektami
80const sensorNetwork = reactive({
81  location: 'Mars Base Alpha',
82  activeSensors: 0,
83  sensors: markRaw(new Map([
84    ['TEMP-001', { type: 'temperature', value: 22 }],
85    ['PRES-001', { type: 'pressure', value: 101 }],
86    ['OXY-001', { type: 'oxygen', value: 21 }]
87  ]))
88})
89
90function addSensor() {
91  const id = `SENS-${Date.now()}`
92  sensorNetwork.sensors.set(id, {
93    type: 'generic',
94    value: Math.random() * 100
95  })
96  // Aktualizujemy reaktywny licznik
97  sensorNetwork.activeSensors = sensorNetwork.sensors.size
98}
99
100function removeSensor() {
101  const keys = Array.from(sensorNetwork.sensors.keys())
102  if (keys.length > 0) {
103    sensorNetwork.sensors.delete(keys[keys.length - 1])
104    sensorNetwork.activeSensors = sensorNetwork.sensors.size
105  }
106}
107
108// toRaw() - dostęp do oryginalnego obiektu
109function getRawReactor() {
110  const raw = toRaw(reactiveReactor)
111  console.log('Raw reactor (non-reactive):', raw)
112  console.log('Is reactive?', isReactive(raw)) // false
113}
114
115// Przykład z integracją zewnętrznej biblioteki
116const chartConfig = reactive({
117  title: 'Reactor Power Output',
118  // Duża biblioteka wykresów - nie potrzebujemy jej reaktywnej
119  chartInstance: markRaw({
120    render() {
121      return 'Chart rendered'
122    },
123    update(data) {
124      console.log('Chart updated with:', data)
125    },
126    destroy() {
127      console.log('Chart destroyed')
128    }
129  })
130})
131
132function updateChart() {
133  chartConfig.chartInstance.update({
134    power: reactiveReactor.power,
135    timestamp: new Date()
136  })
137}
138</script>
139
140<template>
141  <div class="mark-raw-demo">
142    <div class="header">
143      <h1>KONTROLA REAKTORA - NOVA LAB</h1>
144      <p class="subtitle">markRaw() i toRaw() - Zaawansowana kontrola reaktywności</p>
145    </div>
146
147    <!-- Porównanie wydajności -->
148    <div class="comparison-section">
149      <h2>Porównanie wydajności: Reaktywny vs markRaw</h2>
150
151      <div class="reactors-grid">
152        <!-- Reaktor A - Reaktywny -->
153        <div class="reactor-card">
154          <div class="reactor-header">
155            <h3>{{ reactiveReactor.id }}</h3>
156            <span class="badge badge-reactive">REAKTYWNY</span>
157          </div>
158
159          <div class="reactor-stats">
160            <div class="stat">
161              <div class="label">Status</div>
162              <div class="value">{{ reactiveReactor.status }}</div>
163            </div>
164            <div class="stat">
165              <div class="label">Moc</div>
166              <div class="value">{{ reactiveReactor.power }}%</div>
167            </div>
168            <div class="stat">
169              <div class="label">Iteracje silnika</div>
170              <div class="value">{{ reactiveReactor.engine.iterations }}</div>
171            </div>
172            <div class="stat">
173              <div class="label">Silnik reaktywny?</div>
174              <div class="value">{{ reactiveEngineIsReactive ? 'TAK' : 'NIE' }}</div>
175            </div>
176          </div>
177
178          <div class="simulation-info">
179            <p>Symulacje uruchomione: <strong>{{ reactiveSimulationCount }}</strong></p>
180            <p class="warning">Każda zmiana w silniku wywołuje reaktywność!</p>
181          </div>
182
183          <button @click="runReactiveSimulation" class="btn btn-primary">
184            ▶Uruchom symulację
185          </button>
186
187          <div class="code-box">
188            <pre>// Normalny obiekt - reaktywny
189const reactor = reactive({
190  engine: new Engine()
191})
192// engine i wszystkie jego właściwości
193// są reaktywne Wolniejsze!</pre>
194          </div>
195        </div>
196
197        <!-- Reaktor B - Zoptymalizowany -->
198        <div class="reactor-card">
199          <div class="reactor-header">
200            <h3>{{ optimizedReactor.id }}</h3>
201            <span class="badge badge-optimized">ZOPTYMALIZOWANY</span>
202          </div>
203
204          <div class="reactor-stats">
205            <div class="stat">
206              <div class="label">Status</div>
207              <div class="value">{{ optimizedReactor.status }}</div>
208            </div>
209            <div class="stat">
210              <div class="label">Moc</div>
211              <div class="value">{{ optimizedReactor.power }}%</div>
212            </div>
213            <div class="stat">
214              <div class="label">Iteracje silnika</div>
215              <div class="value">{{ optimizedReactor.engine.iterations }}</div>
216            </div>
217            <div class="stat">
218              <div class="label">Silnik reaktywny?</div>
219              <div class="value">{{ optimizedEngineIsReactive ? 'TAK' : 'NIE' }}</div>
220            </div>
221          </div>
222
223          <div class="simulation-info">
224            <p>Symulacje uruchomione: <strong>{{ optimizedSimulationCount }}</strong></p>
225            <p class="success">Silnik nie jest reaktywny - wydajniejsze!</p>
226          </div>
227
228          <button @click="runOptimizedSimulation" class="btn btn-success">
229            Uruchom symulację (szybsza)
230          </button>
231
232          <div class="code-box">
233            <pre>// markRaw - nie reaktywny
234const reactor = reactive({
235  engine: markRaw(new Engine())
236})
237// engine NIE jest reaktywny
238// Szybsze!</pre>
239          </div>
240        </div>
241      </div>
242    </div>
243
244    <!-- Sieć sensorów -->
245    <div class="sensors-section">
246      <h2>Sieć sensorów (praktyczny przykład markRaw)</h2>
247
248      <div class="sensors-card">
249        <div class="sensors-header">
250          <h3>{{ sensorNetwork.location }}</h3>
251          <div class="sensor-count">
252            Aktywne sensory: <strong>{{ sensorNetwork.activeSensors }}</strong>
253          </div>
254        </div>
255
256        <div class="sensors-grid">
257          <div
258            v-for="[id, sensor] in sensorNetwork.sensors"
259            :key="id"
260            class="sensor-item"
261          >
262            <div class="sensor-id">{{ id }}</div>
263            <div class="sensor-type">{{ sensor.type }}</div>
264            <div class="sensor-value">{{ sensor.value.toFixed(2) }}</div>
265          </div>
266        </div>
267
268        <div class="controls">
269          <button @click="addSensor" class="btn btn-success">
270            Dodaj sensor
271          </button>
272          <button @click="removeSensor" class="btn btn-danger">
273            Usuń sensor
274          </button>
275        </div>
276
277        <div class="info">
278          <p>Mapa sensorów używa <code>markRaw()</code></p>
279          <p>Tylko licznik <code>activeSensors</code> jest reaktywny</p>
280        </div>
281
282        <div class="code-box">
283          <pre>const network = reactive({
284  activeSensors: 0,
285  // Map nie musi być reaktywny
286  sensors: markRaw(new Map())
287})
288
289// Aktualizuj tylko reaktywny licznik
290network.activeSensors = network.sensors.size</pre>
291        </div>
292      </div>
293    </div>
294
295    <!-- Integracja bibliotek -->
296    <div class="library-section">
297      <h2>Integracja z zewnętrznymi bibliotekami</h2>
298
299      <div class="library-card">
300        <h3>{{ chartConfig.title }}</h3>
301
302        <div class="chart-placeholder">
303          <div class="chart-visual">
304            Wykres mocy reaktora
305          </div>
306          <p>Instance wykresu: {{ chartConfig.chartInstance.render() }}</p>
307        </div>
308
309        <button @click="updateChart" class="btn btn-info">
310          Aktualizuj wykres
311        </button>
312
313        <div class="code-box">
314          <pre>import Chart from 'chart-library'
315
316const config = reactive({
317  title: 'My Chart',
318  // Duża biblioteka - nie potrzebujemy reaktywności
319  chartInstance: markRaw(new Chart())
320})
321
322// Ręcznie aktualizuj gdy potrzeba
323config.chartInstance.update(newData)</pre>
324        </div>
325      </div>
326    </div>
327
328    <!-- toRaw() demo -->
329    <div class="toraw-section">
330      <h2>toRaw() - Dostęp do oryginalnego obiektu</h2>
331
332      <div class="toraw-card">
333        <p>Czasami potrzebujesz dostępu do oryginalnego, nie-reaktywnego obiektu</p>
334
335        <button @click="getRawReactor" class="btn btn-warning">
336          Pobierz RAW reactor (sprawdź konsolę)
337        </button>
338
339        <div class="code-box">
340          <pre>const reactive = reactive({ value: 1 })
341
342// Pobierz oryginalny obiekt
343const raw = toRaw(reactive)
344
345// Zmiany w raw NIE wywołają reaktywności
346raw.value = 2
347
348console.log(isReactive(raw)) // false</pre>
349        </div>
350
351        <div class="use-cases">
352          <h4>Kiedy używać toRaw()?</h4>
353          <ul>
354            <li>Przekazywanie do API które nie potrzebuje reaktywności</li>
355            <li>Operacje na surowych danych bez wywołania reaktywności</li>
356            <li>Debugowanie - sprawdzanie oryginalnych wartości</li>
357          </ul>
358        </div>
359      </div>
360    </div>
361
362    <!-- Info box -->
363    <div class="info-box">
364      <h3>Kiedy używać markRaw()?</h3>
365      <div class="use-cases-grid">
366        <div class="use-case">
367          <h4>Duże obiekty danych</h4>
368          <p>Gigantyczne struktury danych, które nie muszą być reaktywne</p>
369        </div>
370        <div class="use-case">
371          <h4>Zewnętrzne biblioteki</h4>
372          <p>Instancje Chart.js, Three.js, D3.js - nie potrzebują reaktywności</p>
373        </div>
374        <div class="use-case">
375          <h4>Map, Set, WeakMap</h4>
376          <p>Kolekcje które zmieniasz często, ale nie potrzebujesz śledzić każdej zmiany</p>
377        </div>
378        <div class="use-case">
379          <h4>Optymalizacja wydajności</h4>
380          <p>Gdy reaktywność spowalnia aplikację niepotrzebnie</p>
381        </div>
382      </div>
383
384      <div class="warning-box">
385        <h4>Uwaga!</h4>
386        <p><code>markRaw()</code> jest nieodwracalny - obiekt nigdy nie stanie się reaktywny!</p>
387        <p>Używaj tylko gdy jesteś pewny, że reaktywność nie jest potrzebna.</p>
388      </div>
389    </div>
390  </div>
391</template>
392
393<style scoped>
394.mark-raw-demo {
395  min-height: 100vh;
396  background: #0a0e27;
397  color: #e0e0e0;
398  padding: 2rem;
399  font-family: 'Courier New', monospace;
400}
401
402.header {
403  text-align: center;
404  margin-bottom: 2rem;
405  border-bottom: 2px solid #00ff88;
406  padding-bottom: 1rem;
407}
408
409.header h1 {
410  color: #00ff88;
411  font-size: 2rem;
412  margin: 0;
413  text-shadow: 0 0 10px #00ff88;
414}
415
416.subtitle {
417  color: #00b4d8;
418  margin: 0.5rem 0 0 0;
419}
420
421.comparison-section,
422.sensors-section,
423.library-section,
424.toraw-section {
425  background: #1a1f3a;
426  padding: 1.5rem;
427  border-radius: 8px;
428  border: 1px solid #00b4d8;
429  margin-bottom: 2rem;
430}
431
432h2 {
433  color: #00ff88;
434  margin-top: 0;
435}
436
437.reactors-grid {
438  display: grid;
439  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
440  gap: 1.5rem;
441}
442
443.reactor-card,
444.sensors-card,
445.library-card,
446.toraw-card {
447  background: #0a0e27;
448  padding: 1.5rem;
449  border-radius: 6px;
450}
451
452.reactor-header,
453.sensors-header {
454  display: flex;
455  justify-content: space-between;
456  align-items: center;
457  margin-bottom: 1.5rem;
458  padding-bottom: 1rem;
459  border-bottom: 1px solid #333;
460}
461
462.reactor-header h3,
463.sensors-header h3 {
464  color: #00b4d8;
465  margin: 0;
466}
467
468.badge {
469  padding: 0.3rem 0.8rem;
470  border-radius: 20px;
471  font-size: 0.8rem;
472  font-weight: bold;
473}
474
475.badge-reactive {
476  background: #ff8800;
477  color: #0a0e27;
478}
479
480.badge-optimized {
481  background: #00ff88;
482  color: #0a0e27;
483}
484
485.reactor-stats {
486  display: grid;
487  grid-template-columns: repeat(2, 1fr);
488  gap: 1rem;
489  margin-bottom: 1.5rem;
490}
491
492.stat {
493  background: #1a1f3a;
494  padding: 1rem;
495  border-radius: 4px;
496}
497
498.stat .label {
499  font-size: 0.85rem;
500  color: #888;
501  margin-bottom: 0.3rem;
502}
503
504.stat .value {
505  font-size: 1.2rem;
506  color: #00ff88;
507  font-weight: bold;
508}
509
510.simulation-info {
511  background: #1a1f3a;
512  padding: 1rem;
513  border-radius: 4px;
514  margin-bottom: 1rem;
515}
516
517.simulation-info p {
518  margin: 0.5rem 0;
519}
520
521.simulation-info strong {
522  color: #00ff88;
523}
524
525.warning {
526  color: #ff8800;
527}
528
529.success {
530  color: #00ff88;
531}
532
533.btn {
534  width: 100%;
535  padding: 1rem;
536  font-size: 1rem;
537  border: none;
538  border-radius: 6px;
539  cursor: pointer;
540  font-weight: bold;
541  transition: all 0.3s ease;
542  font-family: 'Courier New', monospace;
543  margin-bottom: 1rem;
544}
545
546.btn-primary {
547  background: linear-gradient(135deg, #00b4d8, #0088cc);
548  color: white;
549}
550
551.btn-success {
552  background: linear-gradient(135deg, #00ff88, #00dd77);
553  color: #0a0e27;
554}
555
556.btn-danger {
557  background: linear-gradient(135deg, #ff4444, #cc0000);
558  color: white;
559}
560
561.btn-info {
562  background: linear-gradient(135deg, #00b4d8, #00ff88);
563  color: #0a0e27;
564}
565
566.btn-warning {
567  background: linear-gradient(135deg, #ffaa00, #ff8800);
568  color: #0a0e27;
569}
570
571.btn:hover {
572  transform: scale(1.02);
573  box-shadow: 0 0 20px currentColor;
574}
575
576.code-box {
577  background: #1a1f3a;
578  padding: 1rem;
579  border-radius: 4px;
580  border-left: 3px solid #00ff88;
581}
582
583.code-box pre {
584  margin: 0;
585  color: #00ff88;
586  font-size: 0.85rem;
587  overflow-x: auto;
588}
589
590.sensor-count {
591  color: #00ff88;
592  font-weight: bold;
593}
594
595.sensors-grid {
596  display: grid;
597  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
598  gap: 1rem;
599  margin: 1.5rem 0;
600}
601
602.sensor-item {
603  background: #1a1f3a;
604  padding: 1rem;
605  border-radius: 4px;
606  border-left: 3px solid #00b4d8;
607}
608
609.sensor-id {
610  color: #00b4d8;
611  font-weight: bold;
612  font-size: 0.9rem;
613}
614
615.sensor-type {
616  color: #888;
617  font-size: 0.85rem;
618  margin: 0.3rem 0;
619  text-transform: uppercase;
620}
621
622.sensor-value {
623  color: #00ff88;
624  font-size: 1.2rem;
625  font-weight: bold;
626}
627
628.controls {
629  display: flex;
630  gap: 1rem;
631}
632
633.controls .btn {
634  flex: 1;
635}
636
637.info {
638  background: #1a1f3a;
639  padding: 1rem;
640  border-radius: 4px;
641  margin-top: 1rem;
642}
643
644.info p {
645  margin: 0.5rem 0;
646  color: #ccc;
647}
648
649.info code {
650  background: #0a0e27;
651  padding: 0.2rem 0.5rem;
652  border-radius: 3px;
653  color: #00ff88;
654}
655
656.chart-placeholder {
657  background: #1a1f3a;
658  padding: 2rem;
659  border-radius: 4px;
660  text-align: center;
661  margin: 1rem 0;
662}
663
664.chart-visual {
665  font-size: 3rem;
666  margin-bottom: 1rem;
667}
668
669.use-cases {
670  background: #1a1f3a;
671  padding: 1rem;
672  border-radius: 4px;
673  margin-top: 1rem;
674}
675
676.use-cases h4 {
677  color: #00b4d8;
678  margin-top: 0;
679}
680
681.use-cases ul {
682  color: #ccc;
683  padding-left: 1.5rem;
684}
685
686.use-cases li {
687  margin: 0.5rem 0;
688}
689
690.info-box {
691  background: #1a1f3a;
692  padding: 1.5rem;
693  border-radius: 8px;
694  border-left: 4px solid #00ff88;
695}
696
697.info-box h3 {
698  color: #00ff88;
699  margin-top: 0;
700}
701
702.use-cases-grid {
703  display: grid;
704  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
705  gap: 1rem;
706  margin: 1rem 0;
707}
708
709.use-case {
710  background: #0a0e27;
711  padding: 1rem;
712  border-radius: 4px;
713}
714
715.use-case h4 {
716  color: #00b4d8;
717  margin-top: 0;
718}
719
720.use-case p {
721  color: #ccc;
722  margin: 0;
723  font-size: 0.9rem;
724}
725
726.warning-box {
727  background: #2a1f1a;
728  border: 2px solid #ff8800;
729  padding: 1rem;
730  border-radius: 4px;
731  margin-top: 1rem;
732}
733
734.warning-box h4 {
735  color: #ffaa00;
736  margin-top: 0;
737}
738
739.warning-box p {
740  color: #ccc;
741  margin: 0.5rem 0;
742}
743
744.warning-box code {
745  background: #0a0e27;
746  padding: 0.2rem 0.5rem;
747  border-radius: 3px;
748  color: #00ff88;
749}
750</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. Która praktyka jest zalecana w Vue 3?

  2. 2. Na czym opiera się system reaktywności Vue 3?

To 2 z 11 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Użyj readonly() do zabezpieczenia stanu przed mutacją.

  • Układanie w pionie

    Ułóż wzorzec użycia readonly do ochrony stanu:

  • Klikanie w kolejności

    Ułóż kod markRaw() z biblioteką zewnętrzną:

  • Edytor kodu

    Użyj markRaw() żeby zapobiec reaktywności dla dużych lub zewnętrznych obiektów.

  • Układanie w pionie

    Ułóż kroki optymalizacji z shallowRef i triggerRef:

  • Klikanie w kolejności

    Ułóż import najbardziej potrzebnych API reaktywności:

  • Układanie w pionie

    Ułóż koncepcje reaktywności od podstawowych do zaawansowanych:

  • Układanie w pionie

    Ułóż składnię tworzenia shallowRef:

  • Układanie w pionie

    Ułóż architekturę stanu komponentu od danych do widoku:

  • Klikanie w kolejności

    Ułóż kod reaktywnego formularza:

Przydatne artykuły