Vue.js course · Module 3: Reactivity

Reactivity in Practice - Control Panel

7 min read
In this lesson5

Let's combine everything in practical NOVA LAB system examples.

So far you have looked at each reactivity tool on its own. Real station panels combine them all at once: the configuration form has to validate data on the fly, the module monitor filters the list by the typed phrase, and the supply system adds up the order totals. All three panels below share the same layering. First the data in ref() or reactive(), then values derived with computed(), next the actions, meaning the functions that change the state, and finally the template with reactive bindings.

Reactor Configuration Panel with Validation

The form template uses v-model, the two-way binding between a field and the data, which you will learn in detail in module 5. The .number modifier turns the typed text into a number, and you know @submit.prevent and :disabled from module 2:

1<template>
2  <form @submit.prevent="submitConfig" class="reactor-config" style="background: #0a0e27; color: #00ff88;">
3    <div>
4      <label>Module name:</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>Maximum temperature (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;">Save configuration</button>
21  </form>
22</template>

Error messages appear through v-if only when there is something to show, and the save button stays disabled until the form is valid. The form data is a coherent group of fields, so we keep it in reactive():

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

Validation is a value derived from these fields. In the first version of this panel, the computed property itself wrote the messages into a separate errors object. The Vue docs forbid side effects in computed properties, though: a getter should only compute and return a result. That is why the errors themselves are computed here:

1// Validation as a derived value - no side effects
2const errors = computed(() => ({
3  // Name validation
4  name: config.name.length < 3 ? 'Name must be at least 3 characters' : '',
5  // Temperature validation
6  maxTemp: config.maxTemp && (config.maxTemp < 1000 || config.maxTemp > 10000)
7    ? 'Temperature must be between 1000 and 10000 K'
8    : ''
9}))
10
11const isValid = computed(() => !errors.value.name && !errors.value.maxTemp)

The template needed no change: errors is a top-level ref, so errors.name works in it without .value. Typing the third character of the name recalculates both computed properties and enables the button. The action that sends the configuration only reads the finished result:

1function submitConfig() {
2  if (isValid.value) {
3    console.log('Sending configuration to NOVA LAB:', config)
4  }
5}
6</script>

Module Monitoring System

The monitor combines a search field and a drop-down list with a filtered list of modules. v-model works on <select> as well:

1<template>
2  <div class="module-monitor" style="background: #0a0e27; color: #00ff88;">
3    <input v-model="searchQuery" placeholder="Search module..." style="background: #1a1e37; color: #00ff88;" />
4    <select v-model="filterStatus" style="background: #1a1e37; color: #00ff88;">
5      <option value="">All statuses</option>
6      <option value="ACTIVE">Active</option>
7      <option value="STANDBY">Standby</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>Found: {{ filteredModules.length }} modules</p>
17  </div>
18</template>

The template filters nothing on its own - it receives the ready list filteredModules. The state is two simple refs and an array of modules:

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])

The filter combines both conditions. The !filterStatus.value || part means: when no status is selected, let every module through:

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>

The original modules array stays untouched, and the list on screen refreshes with every typed character.

Mission Supply System

The supply order shows the number of items, the entries with a remove button and the total in credits:

1<template>
2  <div class="supply-cart" style="background: #0a0e27; color: #00ff88;">
3    <h2>Order ({{ supplies.itemCount }} items)</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;">Remove</button>
8    </div>
9
10    <p><strong>Total: {{ supplies.total }} CR (Credits)</strong></p>
11  </div>
12</template>

Here the derived values are written differently than in the previous panels: as getters inside the reactive object itself. A getter is a JavaScript property whose value is computed on every read by a function marked with the get keyword:

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 as property
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})

At the start itemCount is 15 and total is 1500 CR. A getter computes its result on every read and, unlike computed(), does not cache it, which does not matter for a short list. The remove action changes the array in place:

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>

After the plasma cells are removed, the total drops to 500 CR, because splice is a method Vue detects.

readonly() - A View Without Write Access

The monitor in the mess hall should show the reactor state, but it must not change it. readonly() returns a deep, read-only Proxy:

1import { reactive, readonly } from 'vue'
2
3const reactorState = reactive({ temperature: 2500 })
4const reactorView = readonly(reactorState)
5
6reactorView.temperature = 9999 // console warning, change rejected
7reactorState.temperature = 2600 // a change through the original works
8console.log(reactorView.temperature) // 2600

In development mode an attempt to write ends with a Vue warning, and the value stays unchanged. The read-only view is not a copy: it shows every change of the original. The pattern is simple - you keep the writable state to yourself, share the readonly version with the outside, and make changes only through your own functions.

Choosing the Right Tool

The Vue docs recommend ref() as the primary tool. reactive() works well for coherent groups of fields, shallowRef() with triggerRef() for huge lists, markRaw() for instances from external libraries, and readonly() for sharing state. Under the hood Vue 3 intercepts access to objects with the native Proxy, and access to refs with getters and setters. Stick to the split into data, derived values, actions and template and every panel will stay readable. In the module project you will build a complete reactor monitoring system from these layers.

Remember: a good station panel has a single source of truth, derived values instead of manual copies, and actions that are the only thing allowed to touch the state.

Code for this lesson: App.vue
1<script setup>
2import { ref, reactive, markRaw, isReactive, toRaw } from 'vue'
3
4// markRaw() - Plasma reactor control
5// Advanced reactivity control in NOVA LAB
6
7// Large object simulator (e.g. 3D library)
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    // Heavy computations
21    return this.data.reduce((sum, item) => sum + item.value, 0)
22  }
23
24  reset() {
25    this.iterations = 0
26  }
27}
28
29// Reactor with a normal object (reactive)
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 with markRaw (non-reactive engine)
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// Reactivity check
48const reactiveEngineIsReactive = isReactive(reactiveReactor.engine)
49const optimizedEngineIsReactive = isReactive(optimizedReactor.engine)
50
51// Counters for performance demonstration
52const reactiveSimulationCount = ref(0)
53const optimizedSimulationCount = ref(0)
54
55function runReactiveSimulation() {
56  const start = performance.now()
57
58  // Modification of the reactive engine
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  // Modification of the markRaw engine
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// Practical example - map with many objects
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  // We update the reactive counter
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() - access to the original object
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// Example with external library integration
116const chartConfig = reactive({
117  title: 'Reactor Power Output',
118  // Large chart library - we don't need it reactive
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>REACTOR CONTROL - NOVA LAB</h1>
144      <p class="subtitle">markRaw() and toRaw() - Advanced reactivity control</p>
145    </div>
146
147    <!-- Performance comparison -->
148    <div class="comparison-section">
149      <h2>Performance comparison: Reactive vs markRaw</h2>
150
151      <div class="reactors-grid">
152        <!-- Reactor A - Reactive -->
153        <div class="reactor-card">
154          <div class="reactor-header">
155            <h3>{{ reactiveReactor.id }}</h3>
156            <span class="badge badge-reactive">REACTIVE</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">Power</div>
166              <div class="value">{{ reactiveReactor.power }}%</div>
167            </div>
168            <div class="stat">
169              <div class="label">Engine iterations</div>
170              <div class="value">{{ reactiveReactor.engine.iterations }}</div>
171            </div>
172            <div class="stat">
173              <div class="label">Engine reactive?</div>
174              <div class="value">{{ reactiveEngineIsReactive ? 'YES' : 'NO' }}</div>
175            </div>
176          </div>
177
178          <div class="simulation-info">
179            <p>Simulations run: <strong>{{ reactiveSimulationCount }}</strong></p>
180            <p class="warning">Every change in the engine triggers reactivity!</p>
181          </div>
182
183          <button @click="runReactiveSimulation" class="btn btn-primary">
184            ▶Run simulation
185          </button>
186
187          <div class="code-box">
188            <pre>// Normal object - reactive
189const reactor = reactive({
190  engine: new Engine()
191})
192// engine and all its properties
193// are reactive Slower!</pre>
194          </div>
195        </div>
196
197        <!-- Reactor B - Optimized -->
198        <div class="reactor-card">
199          <div class="reactor-header">
200            <h3>{{ optimizedReactor.id }}</h3>
201            <span class="badge badge-optimized">OPTIMIZED</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">Power</div>
211              <div class="value">{{ optimizedReactor.power }}%</div>
212            </div>
213            <div class="stat">
214              <div class="label">Engine iterations</div>
215              <div class="value">{{ optimizedReactor.engine.iterations }}</div>
216            </div>
217            <div class="stat">
218              <div class="label">Engine reactive?</div>
219              <div class="value">{{ optimizedEngineIsReactive ? 'YES' : 'NO' }}</div>
220            </div>
221          </div>
222
223          <div class="simulation-info">
224            <p>Simulations run: <strong>{{ optimizedSimulationCount }}</strong></p>
225            <p class="success">The engine is not reactive - more efficient!</p>
226          </div>
227
228          <button @click="runOptimizedSimulation" class="btn btn-success">
229            Run simulation (faster)
230          </button>
231
232          <div class="code-box">
233            <pre>// markRaw - not reactive
234const reactor = reactive({
235  engine: markRaw(new Engine())
236})
237// engine is NOT reactive
238// Faster!</pre>
239          </div>
240        </div>
241      </div>
242    </div>
243
244    <!-- Sensor network -->
245    <div class="sensors-section">
246      <h2>Sensor network (practical markRaw example)</h2>
247
248      <div class="sensors-card">
249        <div class="sensors-header">
250          <h3>{{ sensorNetwork.location }}</h3>
251          <div class="sensor-count">
252            Active sensors: <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            Add sensor
271          </button>
272          <button @click="removeSensor" class="btn btn-danger">
273            Remove sensor
274          </button>
275        </div>
276
277        <div class="info">
278          <p>Sensor map uses <code>markRaw()</code></p>
279          <p>Only the <code>activeSensors</code> counter is reactive</p>
280        </div>
281
282        <div class="code-box">
283          <pre>const network = reactive({
284  activeSensors: 0,
285  // Map doesn't need to be reactive
286  sensors: markRaw(new Map())
287})
288
289// Update only the reactive counter
290network.activeSensors = network.sensors.size</pre>
291        </div>
292      </div>
293    </div>
294
295    <!-- Library integration -->
296    <div class="library-section">
297      <h2>Integration with external libraries</h2>
298
299      <div class="library-card">
300        <h3>{{ chartConfig.title }}</h3>
301
302        <div class="chart-placeholder">
303          <div class="chart-visual">
304            Reactor power chart
305          </div>
306          <p>Chart instance: {{ chartConfig.chartInstance.render() }}</p>
307        </div>
308
309        <button @click="updateChart" class="btn btn-info">
310          Update chart
311        </button>
312
313        <div class="code-box">
314          <pre>import Chart from 'chart-library'
315
316const config = reactive({
317  title: 'My Chart',
318  // Large library - we don't need reactivity
319  chartInstance: markRaw(new Chart())
320})
321
322// Manually update when needed
323config.chartInstance.update(newData)</pre>
324        </div>
325      </div>
326    </div>
327
328    <!-- toRaw() demo -->
329    <div class="toraw-section">
330      <h2>toRaw() - Access to the original object</h2>
331
332      <div class="toraw-card">
333        <p>Sometimes you need access to the original, non-reactive object</p>
334
335        <button @click="getRawReactor" class="btn btn-warning">
336          Get RAW reactor (check console)
337        </button>
338
339        <div class="code-box">
340          <pre>const reactive = reactive({ value: 1 })
341
342// Get the original object
343const raw = toRaw(reactive)
344
345// Changes in raw will NOT trigger reactivity
346raw.value = 2
347
348console.log(isReactive(raw)) // false</pre>
349        </div>
350
351        <div class="use-cases">
352          <h4>When to use toRaw()?</h4>
353          <ul>
354            <li>Passing to API that doesn't need reactivity</li>
355            <li>Operations on raw data without triggering reactivity</li>
356            <li>Debugging - checking original values</li>
357          </ul>
358        </div>
359      </div>
360    </div>
361
362    <!-- Info box -->
363    <div class="info-box">
364      <h3>When to use markRaw()?</h3>
365      <div class="use-cases-grid">
366        <div class="use-case">
367          <h4>Large data objects</h4>
368          <p>Gigantic data structures that don't need to be reactive</p>
369        </div>
370        <div class="use-case">
371          <h4>External libraries</h4>
372          <p>Chart.js, Three.js, D3.js instances - don't need reactivity</p>
373        </div>
374        <div class="use-case">
375          <h4>Map, Set, WeakMap</h4>
376          <p>Collections you change frequently, but don't need to track every change</p>
377        </div>
378        <div class="use-case">
379          <h4>Performance optimization</h4>
380          <p>When reactivity slows down the application unnecessarily</p>
381        </div>
382      </div>
383
384      <div class="warning-box">
385        <h4>Warning!</h4>
386        <p><code>markRaw()</code> is irreversible - the object will never become reactive!</p>
387        <p>Use only when you are sure that reactivity is not needed.</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>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the recommended approach to reactivity in Vue 3?

  2. 2. What mechanism does Vue 3 use to implement reactivity?

These are 2 of 11 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Create a reactive object and wrap it in readonly() to protect it from changes.

  • Vertical ordering

    Arrange the steps for using readonly in a component in the correct order.

  • Click in order

    Arrange the steps for using markRaw with an external library in the correct order.

  • Code editor

    Use markRaw() for an object that doesn't need reactivity (e.g., a library instance or configuration).

  • Vertical ordering

    Arrange the steps for optimizing with triggerRef() in the correct order.

  • Click in order

    Arrange the import statement for reactive functions in the correct order.

  • Vertical ordering

    Arrange the reactivity concepts from simplest to most advanced.

  • Vertical ordering

    Arrange the steps for using shallowRef in the correct order.

  • Vertical ordering

    Arrange the steps for designing state in a component in the correct order.

  • Click in order

    Arrange the steps for creating a reactive form in the correct order.

Useful articles