Kurs Vue.js · Moduł 3: Reaktywność
Reaktywność w Praktyce - Panel Kontrolny
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) // 2600Pró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. Która praktyka jest zalecana w Vue 3?
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: