Vue.js course Β· Module 9: Composables & VueUse
VueUse - Composables Library
In this lesson8
VueUse is a collection of 200+ ready-made composables - like the NOVA LAB library of standard research protocols! Why do you need such a library? Every project brings back the same tasks: remember settings in localStorage, track the window size, the mouse position or dark mode, delay a search until the user stops typing. Each of them requires listening to browser events and cleaning up afterwards. You can write all that from scratch in every project, or reach for proven, tested functions from the @vueuse/core package. Only the functions you actually import end up in the application bundle.
Installing VueUse
You install the library like any npm package. The current version of VueUse requires Vue 3.5 or newer:
1npm install @vueuse/coreAfter installation you import individual functions from '@vueuse/core', exactly like ref or computed from 'vue'. No plugin registration is needed.
useLocalStorage
The first protocol saves the lab settings so that they survive a page refresh. useLocalStorage takes a key and a default value, and returns a ref synchronized with the browser's localStorage:
1<script setup>
2import { useLocalStorage } from '@vueuse/core'
3
4// Automatically syncs with localStorage
5const labSettings = useLocalStorage('lab-settings', {
6 mode: 'exploration',
7 sensorsPerView: 12,
8 sortBy: 'temperature'
9})
10
11// Changes are automatically saved to localStorage
12labSettings.value.mode = 'research'
13</script>
14
15<template>
16 <div>
17 <p>Mode: {{ labSettings.mode }}</p>
18 <button @click="labSettings.mode = 'maintenance'">
19 Change mode
20 </button>
21 </div>
22</template>The default value is used only when nothing is stored under the lab-settings key yet. Objects are saved as JSON, and a change to a single field is saved too, because VueUse watches the object deeply.
useMouseInElement
This composable tracks the mouse relative to a specific element. Among other things it returns elementX and elementY, the position measured from the element's top-left corner, and isOutside, which tells you whether the cursor is outside it. The plain x and y are coordinates on the whole page:
1<script setup>
2import { ref } from 'vue'
3import { useMouseInElement } from '@vueuse/core'
4
5const sensorPanel = ref(null)
6const { elementX, elementY, isOutside } = useMouseInElement(sensorPanel)
7</script>
8
9<template>
10 <div ref="sensorPanel" class="sensor-panel">
11 <img src="/sensor-display.jpg" />
12 <div v-if="!isOutside" class="tooltip" :style="{ left: elementX + 'px', top: elementY + 'px' }">
13 Position: {{ elementX }}, {{ elementY }}
14 </div>
15 </div>
16</template>The sensorPanel ref connects to the element through the ref attribute; since Vue 3.5 you can also use useTemplateRef('sensorPanel'). For the tooltip to follow the cursor, the panel needs position: relative in CSS and the tooltip position: absolute.
useIntersectionObserver
The next protocol checks whether an element is visible in the browser window. It is the basis of lazy-loading images and of animations triggered by scrolling. The callback receives a list of observed entries, and the isIntersecting field tells you whether the element has entered the viewport:
1<script setup>
2import { ref } from 'vue'
3import { useIntersectionObserver } from '@vueuse/core'
4
5const experimentPanel = ref(null)
6const isVisible = ref(false)
7
8useIntersectionObserver(
9 experimentPanel,
10 ([{ isIntersecting }]) => {
11 isVisible.value = isIntersecting
12 }
13)
14</script>
15
16<template>
17 <div ref="experimentPanel">
18 <Transition name="fade">
19 <img v-if="isVisible" src="/experiment-data.jpg" />
20 </Transition>
21 </div>
22</template>The image is downloaded only when the panel appears on screen. VueUse disconnects the observer by itself when the component unmounts, so you don't have to write your own cleanup.
useWindowSize
useWindowSize returns the window's reactive width and height, which update every time the window is resized. From them it's easy to derive view breakpoints with computed:
1<script setup>
2import { useWindowSize } from '@vueuse/core'
3import { computed } from 'vue'
4
5const { width, height } = useWindowSize()
6
7const isMobile = computed(() => width.value < 768)
8const isTablet = computed(() => width.value >= 768 && width.value < 1024)
9const isDesktop = computed(() => width.value >= 1024)
10</script>
11
12<template>
13 <div>
14 <p v-if="isMobile">Mobile view</p>
15 <p v-else-if="isTablet">Tablet view</p>
16 <p v-else>Desktop view</p>
17 <p>Dimensions: {{ width }} x {{ height }}</p>
18 </div>
19</template>The template switches the message without any manual resize listener. If you have more breakpoints, VueUse offers the specialized useBreakpoints for that.
useDark & useToggle
useDark returns a ref with the boolean value of dark mode. By default it starts from the system setting, adds the dark class to the <html> element and remembers the choice in localStorage. useToggle turns such a ref into a toggle function:
1<script setup>
2import { useDark, useToggle } from '@vueuse/core'
3
4const isDark = useDark()
5const toggleDark = useToggle(isDark)
6</script>
7
8<template>
9 <div :class="{ dark: isDark }">
10 <button @click="toggleDark()">
11 {{ isDark ? 'Light' : 'Dark' }} theme
12 </button>
13 </div>
14</template>
15
16<style>
17body {
18 transition: background 0.3s, color 0.3s;
19}
20
21.dark {
22 background: #1a1a1a;
23 color: #f5f5f5;
24}
25</style>After a click, the dark class appears on <html> and on the div with the :class binding. The binding isn't necessary, because useDark itself adds the class to <html>; we keep it to make it visible where the style comes from.
refDebounced and refThrottled
Debounce waits until a value stops changing for a given time, while throttle lets a change through at most once per interval. In VueUse this is done by refDebounced and refThrottled, which return a delayed copy of a ref. The older names useDebounce and useThrottle still work, but they are marked as deprecated:
1<script setup>
2import { ref } from 'vue'
3import { refDebounced, refThrottled } from '@vueuse/core'
4
5// Debounce - waits until the user stops typing
6const searchQuery = ref('')
7const debouncedQuery = refDebounced(searchQuery, 500)
8
9// Throttle - at most once every X ms
10const scrollPosition = ref(0)
11const throttledScroll = refThrottled(scrollPosition, 100)
12</script>
13
14<template>
15 <input v-model="searchQuery" placeholder="Search..." />
16 <p>Searching: {{ debouncedQuery }}</p>
17</template>When you type "oxy", debouncedQuery changes only half a second after the last key, so the request to the server goes out once instead of three times. You would update scrollPosition in a scroll handler. For delaying function calls, VueUse also has useDebounceFn and useThrottleFn.
Popular Composables from VueUse
Finally, a cheat sheet with the functions you will reach for most often:
| Composable | Description |
|---|---|
useLocalStorage | Reactive localStorage |
useFetch | Fetch with reactivity |
useIntersectionObserver | Visibility detection |
useMouseInElement | Mouse position in element |
useWindowSize | Window size |
useDark | Dark mode |
refDebounced | Delayed reactivity (debounce) |
useClipboard | Copy to clipboard |
My advice: before you write your own composable for a browser feature, check vueuse.org. If the function is already there, you get code with tests and correct cleanup. In the editor below you will see what such a composable looks like inside: useConnectionMonitor starts its timers in onMounted and cleans them up in onUnmounted - exactly the work VueUse does for you.
Remember: VueUse is a warehouse of ready-made station protocols - before you build a new instrument, check whether it is already on the shelf.
Code for this lesson: App.vue
1<script setup>
2import { ref, onMounted, onUnmounted } from 'vue'
3
4// Composable with lifecycle hooks: useConnectionMonitor
5function useConnectionMonitor(serverUrl) {
6 const isConnected = ref(false)
7 const connectionTime = ref(null)
8 const disconnectionTime = ref(null)
9 const reconnectAttempts = ref(0)
10 const latency = ref(0)
11 const messages = ref([])
12
13 let intervalId = null
14 let connectionCheckId = null
15
16 function addMessage(type, text) {
17 messages.value.unshift({
18 id: Date.now(),
19 type,
20 text,
21 timestamp: new Date().toLocaleTimeString()
22 })
23 if (messages.value.length > 10) {
24 messages.value.pop()
25 }
26 }
27
28 function connect() {
29 isConnected.value = true
30 connectionTime.value = new Date()
31 reconnectAttempts.value = 0
32 addMessage('success', `Connected to ${serverUrl}`)
33
34 // Simulate latency monitoring
35 intervalId = setInterval(() => {
36 latency.value = Math.floor(Math.random() * 100) + 20
37 }, 2000)
38 }
39
40 function disconnect() {
41 isConnected.value = false
42 disconnectionTime.value = new Date()
43 addMessage('warning', `Disconnected from ${serverUrl}`)
44
45 if (intervalId) {
46 clearInterval(intervalId)
47 intervalId = null
48 }
49 }
50
51 function attemptReconnect() {
52 if (isConnected.value) return
53
54 reconnectAttempts.value++
55 addMessage('info', `Reconnection attempt #${reconnectAttempts.value}`)
56
57 setTimeout(() => {
58 if (reconnectAttempts.value <= 3) {
59 connect()
60 addMessage('success', 'Reconnected successfully!')
61 } else {
62 addMessage('error', 'Max reconnection attempts reached')
63 }
64 }, 1500)
65 }
66
67 // Lifecycle: Mount
68 onMounted(() => {
69 addMessage('info', 'Connection monitor initialized')
70
71 // Auto-connect on mount
72 setTimeout(() => {
73 connect()
74 }, 1000)
75
76 // Simulate random disconnections
77 connectionCheckId = setInterval(() => {
78 if (isConnected.value && Math.random() < 0.1) {
79 addMessage('warning', 'Connection lost - random event')
80 disconnect()
81 setTimeout(attemptReconnect, 2000)
82 }
83 }, 5000)
84 })
85
86 // Lifecycle: Unmount
87 onUnmounted(() => {
88 addMessage('info', 'Connection monitor shutting down')
89 if (intervalId) clearInterval(intervalId)
90 if (connectionCheckId) clearInterval(connectionCheckId)
91 disconnect()
92 })
93
94 return {
95 isConnected,
96 connectionTime,
97 disconnectionTime,
98 reconnectAttempts,
99 latency,
100 messages,
101 connect,
102 disconnect,
103 attemptReconnect
104 }
105}
106
107// Create connection monitors for different servers
108const earthConnection = useConnectionMonitor('Earth Command Center')
109const marsConnection = useConnectionMonitor('Mars Base Alpha')
110const satelliteConnection = useConnectionMonitor('Orbital Satellite')
111</script>
112
113<template>
114 <div class="connection-lab">
115 <div class="header">
116 <h1>Connection Monitor - NOVA LAB</h1>
117 <p class="subtitle">Module 3: Composables with Lifecycle Hooks</p>
118 </div>
119
120 <div class="connections-grid">
121 <!-- Earth Connection -->
122 <div class="connection-card" :class="{ connected: earthConnection.isConnected }">
123 <div class="connection-header">
124 <h3>Earth Command</h3>
125 <div class="status-indicator" :class="{ active: earthConnection.isConnected }">
126 {{ earthConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
127 </div>
128 </div>
129
130 <div class="connection-stats">
131 <div class="stat-item">
132 <span class="stat-label">Latency:</span>
133 <span class="stat-value" :class="{ good: earthConnection.latency < 50 }">
134 {{ earthConnection.latency }}ms
135 </span>
136 </div>
137 <div class="stat-item">
138 <span class="stat-label">Reconnect Attempts:</span>
139 <span class="stat-value">{{ earthConnection.reconnectAttempts }}</span>
140 </div>
141 </div>
142
143 <div class="connection-times">
144 <div v-if="earthConnection.connectionTime" class="time-info">
145 Connected: {{ earthConnection.connectionTime.toLocaleTimeString() }}
146 </div>
147 <div v-if="earthConnection.disconnectionTime" class="time-info">
148 Disconnected: {{ earthConnection.disconnectionTime.toLocaleTimeString() }}
149 </div>
150 </div>
151
152 <div class="connection-controls">
153 <button
154 @click="earthConnection.connect"
155 :disabled="earthConnection.isConnected"
156 class="btn btn-success"
157 >
158 Connect
159 </button>
160 <button
161 @click="earthConnection.disconnect"
162 :disabled="!earthConnection.isConnected"
163 class="btn btn-danger"
164 >
165 Disconnect
166 </button>
167 <button
168 @click="earthConnection.attemptReconnect"
169 :disabled="earthConnection.isConnected"
170 class="btn btn-warning"
171 >
172 Reconnect
173 </button>
174 </div>
175
176 <div class="messages-log">
177 <div
178 v-for="msg in earthConnection.messages.slice(0, 5)"
179 :key="msg.id"
180 class="log-message"
181 :class="'msg-' + msg.type"
182 >
183 <span class="msg-time">{{ msg.timestamp }}</span>
184 <span class="msg-text">{{ msg.text }}</span>
185 </div>
186 </div>
187 </div>
188
189 <!-- Mars Connection -->
190 <div class="connection-card" :class="{ connected: marsConnection.isConnected }">
191 <div class="connection-header">
192 <h3>Mars Base Alpha</h3>
193 <div class="status-indicator" :class="{ active: marsConnection.isConnected }">
194 {{ marsConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
195 </div>
196 </div>
197
198 <div class="connection-stats">
199 <div class="stat-item">
200 <span class="stat-label">Latency:</span>
201 <span class="stat-value" :class="{ good: marsConnection.latency < 50 }">
202 {{ marsConnection.latency }}ms
203 </span>
204 </div>
205 <div class="stat-item">
206 <span class="stat-label">Reconnect Attempts:</span>
207 <span class="stat-value">{{ marsConnection.reconnectAttempts }}</span>
208 </div>
209 </div>
210
211 <div class="connection-times">
212 <div v-if="marsConnection.connectionTime" class="time-info">
213 Connected: {{ marsConnection.connectionTime.toLocaleTimeString() }}
214 </div>
215 <div v-if="marsConnection.disconnectionTime" class="time-info">
216 Disconnected: {{ marsConnection.disconnectionTime.toLocaleTimeString() }}
217 </div>
218 </div>
219
220 <div class="connection-controls">
221 <button
222 @click="marsConnection.connect"
223 :disabled="marsConnection.isConnected"
224 class="btn btn-success"
225 >
226 Connect
227 </button>
228 <button
229 @click="marsConnection.disconnect"
230 :disabled="!marsConnection.isConnected"
231 class="btn btn-danger"
232 >
233 Disconnect
234 </button>
235 <button
236 @click="marsConnection.attemptReconnect"
237 :disabled="marsConnection.isConnected"
238 class="btn btn-warning"
239 >
240 Reconnect
241 </button>
242 </div>
243
244 <div class="messages-log">
245 <div
246 v-for="msg in marsConnection.messages.slice(0, 5)"
247 :key="msg.id"
248 class="log-message"
249 :class="'msg-' + msg.type"
250 >
251 <span class="msg-time">{{ msg.timestamp }}</span>
252 <span class="msg-text">{{ msg.text }}</span>
253 </div>
254 </div>
255 </div>
256
257 <!-- Satellite Connection -->
258 <div class="connection-card" :class="{ connected: satelliteConnection.isConnected }">
259 <div class="connection-header">
260 <h3>Orbital Satellite</h3>
261 <div class="status-indicator" :class="{ active: satelliteConnection.isConnected }">
262 {{ satelliteConnection.isConnected ? 'ONLINE' : 'OFFLINE' }}
263 </div>
264 </div>
265
266 <div class="connection-stats">
267 <div class="stat-item">
268 <span class="stat-label">Latency:</span>
269 <span class="stat-value" :class="{ good: satelliteConnection.latency < 50 }">
270 {{ satelliteConnection.latency }}ms
271 </span>
272 </div>
273 <div class="stat-item">
274 <span class="stat-label">Reconnect Attempts:</span>
275 <span class="stat-value">{{ satelliteConnection.reconnectAttempts }}</span>
276 </div>
277 </div>
278
279 <div class="connection-times">
280 <div v-if="satelliteConnection.connectionTime" class="time-info">
281 Connected: {{ satelliteConnection.connectionTime.toLocaleTimeString() }}
282 </div>
283 <div v-if="satelliteConnection.disconnectionTime" class="time-info">
284 Disconnected: {{ satelliteConnection.disconnectionTime.toLocaleTimeString() }}
285 </div>
286 </div>
287
288 <div class="connection-controls">
289 <button
290 @click="satelliteConnection.connect"
291 :disabled="satelliteConnection.isConnected"
292 class="btn btn-success"
293 >
294 Connect
295 </button>
296 <button
297 @click="satelliteConnection.disconnect"
298 :disabled="!satelliteConnection.isConnected"
299 class="btn btn-danger"
300 >
301 Disconnect
302 </button>
303 <button
304 @click="satelliteConnection.attemptReconnect"
305 :disabled="satelliteConnection.isConnected"
306 class="btn btn-warning"
307 >
308 Reconnect
309 </button>
310 </div>
311
312 <div class="messages-log">
313 <div
314 v-for="msg in satelliteConnection.messages.slice(0, 5)"
315 :key="msg.id"
316 class="log-message"
317 :class="'msg-' + msg.type"
318 >
319 <span class="msg-time">{{ msg.timestamp }}</span>
320 <span class="msg-text">{{ msg.text }}</span>
321 </div>
322 </div>
323 </div>
324 </div>
325
326 <div class="info-panel">
327 <h3>Lifecycle Hooks in Composables</h3>
328 <div class="info-content">
329 <p><strong>useConnectionMonitor()</strong> shows how to use lifecycle hooks:</p>
330 <ul>
331 <li><strong>onMounted()</strong> - Auto-connect and monitoring initialization</li>
332 <li><strong>onUnmounted()</strong> - Cleaning up intervals and disconnect</li>
333 <li>Each composable instance has its own lifecycle hooks</li>
334 <li>Automatic resource management (timers, connections)</li>
335 <li>Preventing memory leaks through cleanup in onUnmounted</li>
336 </ul>
337 <div class="code-example">
338 <code>
339function useConnectionMonitor(url) {
340 onMounted(() => {
341 // Setup: auto-connect, start monitoring
342 })
343
344 onUnmounted(() => {
345 // Cleanup: clear timers, disconnect
346 })
347
348 return { isConnected, connect, disconnect }
349}
350 </code>
351 </div>
352 </div>
353 </div>
354 </div>
355</template>
356
357<style scoped>
358.connection-lab {
359 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
360 min-height: 100vh;
361 padding: 2rem;
362 color: #ffffff;
363 font-family: 'Courier New', monospace;
364}
365
366.header {
367 text-align: center;
368 margin-bottom: 2rem;
369 padding-bottom: 1rem;
370 border-bottom: 2px solid #00ff88;
371}
372
373.header h1 {
374 font-size: 2.5rem;
375 color: #00ff88;
376 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
377 margin: 0;
378}
379
380.subtitle {
381 color: #00b4d8;
382 font-size: 1.2rem;
383 margin-top: 0.5rem;
384}
385
386.connections-grid {
387 display: grid;
388 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
389 gap: 1.5rem;
390 margin-bottom: 2rem;
391}
392
393.connection-card {
394 background: rgba(255, 0, 85, 0.1);
395 border: 2px solid #ff0055;
396 border-radius: 10px;
397 padding: 1.5rem;
398 transition: all 0.3s ease;
399}
400
401.connection-card.connected {
402 background: rgba(0, 255, 136, 0.1);
403 border-color: #00ff88;
404}
405
406.connection-header {
407 display: flex;
408 justify-content: space-between;
409 align-items: center;
410 margin-bottom: 1.5rem;
411}
412
413.connection-header h3 {
414 color: #00ff88;
415 margin: 0;
416 font-size: 1.3rem;
417}
418
419.status-indicator {
420 padding: 0.5rem 1rem;
421 border-radius: 20px;
422 background: rgba(255, 0, 85, 0.3);
423 color: #ff0055;
424 font-weight: bold;
425 font-size: 0.9rem;
426}
427
428.status-indicator.active {
429 background: rgba(0, 255, 136, 0.3);
430 color: #00ff88;
431 animation: pulse 2s infinite;
432}
433
434.connection-stats {
435 display: grid;
436 grid-template-columns: 1fr 1fr;
437 gap: 1rem;
438 margin-bottom: 1rem;
439}
440
441.stat-item {
442 background: rgba(0, 0, 0, 0.3);
443 padding: 1rem;
444 border-radius: 5px;
445 text-align: center;
446}
447
448.stat-label {
449 display: block;
450 color: #00b4d8;
451 font-size: 0.8rem;
452 margin-bottom: 0.5rem;
453}
454
455.stat-value {
456 display: block;
457 color: #ffb400;
458 font-size: 1.5rem;
459 font-weight: bold;
460}
461
462.stat-value.good {
463 color: #00ff88;
464}
465
466.connection-times {
467 background: rgba(0, 0, 0, 0.3);
468 padding: 1rem;
469 border-radius: 5px;
470 margin-bottom: 1rem;
471 min-height: 60px;
472}
473
474.time-info {
475 color: #ffffff;
476 font-size: 0.9rem;
477 margin: 0.25rem 0;
478}
479
480.connection-controls {
481 display: flex;
482 gap: 0.5rem;
483 margin-bottom: 1rem;
484}
485
486.messages-log {
487 background: rgba(0, 0, 0, 0.5);
488 border-radius: 5px;
489 padding: 1rem;
490 max-height: 200px;
491 overflow-y: auto;
492}
493
494.log-message {
495 display: flex;
496 gap: 1rem;
497 padding: 0.5rem;
498 margin: 0.25rem 0;
499 border-left: 3px solid;
500 border-radius: 3px;
501 font-size: 0.85rem;
502}
503
504.msg-success {
505 border-color: #00ff88;
506 background: rgba(0, 255, 136, 0.1);
507}
508
509.msg-warning {
510 border-color: #ffb400;
511 background: rgba(255, 180, 0, 0.1);
512}
513
514.msg-error {
515 border-color: #ff0055;
516 background: rgba(255, 0, 85, 0.1);
517}
518
519.msg-info {
520 border-color: #00b4d8;
521 background: rgba(0, 180, 216, 0.1);
522}
523
524.msg-time {
525 color: #00b4d8;
526 min-width: 80px;
527 font-size: 0.75rem;
528}
529
530.msg-text {
531 color: #ffffff;
532 flex: 1;
533}
534
535.btn {
536 flex: 1;
537 padding: 0.75rem;
538 border: none;
539 border-radius: 5px;
540 font-weight: bold;
541 cursor: pointer;
542 font-family: 'Courier New', monospace;
543 transition: all 0.3s ease;
544 font-size: 0.9rem;
545}
546
547.btn:disabled {
548 opacity: 0.5;
549 cursor: not-allowed;
550}
551
552.btn-success {
553 background: #00ff88;
554 color: #0a0e27;
555}
556
557.btn-danger {
558 background: #ff0055;
559 color: #ffffff;
560}
561
562.btn-warning {
563 background: #ffb400;
564 color: #0a0e27;
565}
566
567.btn:not(:disabled):hover {
568 transform: scale(1.05);
569 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
570}
571
572.info-panel {
573 background: rgba(0, 180, 216, 0.05);
574 border: 2px solid #00b4d8;
575 border-radius: 10px;
576 padding: 2rem;
577}
578
579.info-panel h3 {
580 color: #00ff88;
581 margin-top: 0;
582 margin-bottom: 1.5rem;
583}
584
585.info-content ul {
586 list-style: none;
587 padding: 0;
588}
589
590.info-content li {
591 padding: 0.5rem 0;
592 color: #ffffff;
593 line-height: 1.6;
594}
595
596.code-example {
597 background: rgba(0, 0, 0, 0.5);
598 border-left: 4px solid #00ff88;
599 border-radius: 5px;
600 padding: 1rem;
601 margin-top: 1rem;
602}
603
604.code-example code {
605 color: #00ff88;
606 font-size: 0.9rem;
607 white-space: pre;
608 display: block;
609}
610
611@keyframes pulse {
612 0%, 100% { opacity: 1; }
613 50% { opacity: 0.6; }
614}
615</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. What does the VueUse library offer?
2. What is useLocalStorage from VueUse used for?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a responsive component with computed properties isMobile, isTablet, isDesktop.
- Vertical ordering
Arrange the steps for installing and using VueUse:
- Click in order
Arrange the import of a composable from VueUse:
- Code editor
Use useDark and useToggle to create a dark mode toggle with the appropriate CSS class.
- Horizontal ordering
Arrange the destructuring of useWindowSize: