@dcl/hooks 1.2.1 → 1.2.2-20260330212234.commit-f13404f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,794 +10,48 @@ A collection of React hooks commonly used in Decentraland projects.
10
10
  npm install @dcl/hooks
11
11
  ```
12
12
 
13
- ## Available Hooks
14
-
15
- - `useAdvancedUserAgentData`: Enhanced user agent information
16
- - `useAsyncState`: Async state management with dependencies
17
- - `useAsyncTask`: Single async task management
18
- - `useAsyncTasks`: Multiple async tasks management
19
- - `usePatchState`: Partial state updates for complex objects
20
- - `useAsyncEffect`: Async version of useEffect
21
- - `useAsyncMemo`: Async version of useMemo
22
- - `useInfiniteScroll`: Infinite scroll functionality for loading more content
23
- - `useTranslation`: Simple and lightweight translation management
24
- - `useNotifications`: Notification polling and modal state management
25
-
26
- ## Examples
27
-
28
- ### useAsyncState
29
-
30
- ```typescript
31
- import { useAsyncState } from '@dcl/hooks'
32
-
33
- function Example() {
34
- const [data, { loading, error }] = useAsyncState(
35
- async () => {
36
- const response = await fetch('https://api.example.com/data')
37
- return response.json()
38
- },
39
- [] // dependencies
40
- )
41
-
42
- if (loading) return <div>Loading...</div>
43
- if (error) return <div>Error: {error.message}</div>
44
- return <div>{JSON.stringify(data)}</div>
45
- }
46
- ```
47
-
48
- ### useAdvancedUserAgentData
49
-
50
- ```typescript
51
- import { useAdvancedUserAgentData } from '@dcl/hooks'
52
-
53
- function BrowserInfo() {
54
- const [isLoading, data] = useAdvancedUserAgentData()
55
-
56
- if (isLoading) return <div>Loading browser info...</div>
57
-
58
- return (
59
- <div>
60
- <h3>Browser Information</h3>
61
- <ul>
62
- <li>Browser: {data?.browser.name} {data?.browser.version}</li>
63
- <li>OS: {data?.os.name} {data?.os.version}</li>
64
- <li>CPU Architecture: {data?.cpu.architecture}</li>
65
- <li>Mobile Device: {data?.mobile ? 'Yes' : 'No'}</li>
66
- </ul>
67
- </div>
68
- )
69
- }
70
- ```
71
-
72
- ### useInfiniteScroll
73
-
74
- ```typescript
75
- import { useInfiniteScroll } from '@dcl/hooks'
76
- import { useState, useEffect } from 'react'
77
-
78
- function InfiniteList() {
79
- const [items, setItems] = useState<string[]>([])
80
- const [hasMore, setHasMore] = useState(true)
81
- const [isLoading, setIsLoading] = useState(false)
82
-
83
- const loadMore = async () => {
84
- if (isLoading) return
85
-
86
- setIsLoading(true)
87
- try {
88
- // Simulate API call
89
- const newItems = await fetchMoreItems(items.length)
90
- setItems((prev) => [...prev, ...newItems])
91
-
92
- // Check if there's more data
93
- setHasMore(newItems.length > 0)
94
- } catch (error) {
95
- console.error('Failed to load more items:', error)
96
- } finally {
97
- setIsLoading(false)
98
- }
99
- }
100
-
101
- useInfiniteScroll({
102
- onLoadMore: loadMore,
103
- hasMore,
104
- isLoading,
105
- threshold: 500, // Trigger when 500px from bottom
106
- debounceMs: 500, // Minimum time between triggers (default: 500ms)
107
- })
108
-
109
- return (
110
- <div>
111
- {items.map((item, index) => (
112
- <div key={index}>{item}</div>
113
- ))}
114
- {isLoading && <div>Loading more...</div>}
115
- {!hasMore && <div>No more items</div>}
116
- </div>
117
- )
118
- }
119
- ```
120
-
121
- ### Analytics Hooks
122
-
123
- The package provides a set of hooks for analytics tracking using Segment. Here's how to use them:
124
-
125
- #### Setting up the AnalyticsProvider
126
-
127
- First, wrap your app with the AnalyticsProvider:
128
-
129
- ```typescript
130
- import { AnalyticsProvider } from '@dcl/hooks'
131
-
132
- function App() {
133
- return (
134
- <AnalyticsProvider
135
- writeKey="xyz1234"
136
- userId="user-123" // Optional
137
- traits={{ // Optional
138
- name: 'John Doe',
139
- email: 'john@example.com'
140
- }}
141
- >
142
- <Main />
143
- </AnalyticsProvider>
144
- )
145
- }
146
- ```
147
-
148
- #### Using the useAnalytics Hook
149
-
150
- The `useAnalytics` hook provides access to tracking functions:
151
-
152
- ```typescript
153
- import { useAnalytics } from '@dcl/hooks'
154
-
155
- function MyComponent() {
156
- const analytics = useAnalytics()
157
-
158
- const handleButtonClick = () => {
159
- if (analytics.isInitialized) {
160
- // Track an event
161
- analytics.track('Button Clicked', {
162
- buttonId: 'submit',
163
- timestamp: new Date().toISOString()
164
- })
165
-
166
- // Identify a user
167
- analytics.identify('user-123', {
168
- name: 'John Doe',
169
- email: 'john@example.com'
170
- })
171
- }
172
- }
173
-
174
- return (
175
- <button onClick={handleButtonClick}>
176
- Click me
177
- </button>
178
- )
179
- }
180
- ```
181
-
182
- #### Using the usePageTracking Hook
183
-
184
- The `usePageTracking` hook tracks page views when the pathname changes. You need to pass the current pathname as a parameter from your router:
185
-
186
- ```typescript
187
- import { usePageTracking } from '@dcl/hooks'
188
- import { useLocation } from 'react-router-dom'
13
+ ### Peer Dependencies
189
14
 
190
- function MyPage() {
191
- const location = useLocation()
192
-
193
- // Tracks page view when pathname changes
194
- usePageTracking(location.pathname)
195
-
196
- return (
197
- <div>
198
- <h1>My Page</h1>
199
- {/* Your page content */}
200
- </div>
201
- )
202
- }
203
- ```
204
-
205
- If you need to track additional page properties, you can use the `useAnalytics` hook directly:
206
-
207
- ```typescript
208
- import { useAnalytics } from '@dcl/hooks'
209
-
210
- function MyPage() {
211
- const analytics = useAnalytics()
212
-
213
- useEffect(() => {
214
- if (analytics.isInitialized) {
215
- analytics.page('My Page', {
216
- category: 'Content',
217
- section: 'Main',
218
- timestamp: new Date().toISOString()
219
- })
220
- }
221
- }, [analytics])
222
-
223
- return (
224
- <div>
225
- <h1>My Page</h1>
226
- {/* Your page content */}
227
- </div>
228
- )
229
- }
230
- ```
231
-
232
- #### Complete Example
233
-
234
- Here's a complete example showing how to use all analytics features together:
235
-
236
- ```typescript
237
- import { AnalyticsProvider, useAnalytics, usePageTracking } from '@dcl/hooks'
238
- import { useLocation } from 'react-router-dom'
239
-
240
- function MyPage() {
241
- const location = useLocation()
242
-
243
- // Track page views
244
- usePageTracking(location.pathname)
245
-
246
- return (
247
- <div>
248
- <h1>My Page</h1>
249
- <UserProfile />
250
- </div>
251
- )
252
- }
253
-
254
- function UserProfile() {
255
- const analytics = useAnalytics()
256
-
257
- const handleProfileUpdate = () => {
258
- if (analytics.isInitialized) {
259
- // Track profile update event
260
- analytics.track('Profile Updated', {
261
- timestamp: new Date().toISOString(),
262
- updateType: 'information'
263
- })
264
-
265
- // Update user traits
266
- analytics.identify('user-123', {
267
- lastUpdated: new Date().toISOString()
268
- })
269
- }
270
- }
271
-
272
- return (
273
- <button onClick={handleProfileUpdate}>
274
- Update Profile
275
- </button>
276
- )
277
- }
278
-
279
- function App() {
280
- return (
281
- <AnalyticsProvider
282
- writeKey="xyz1234"
283
- userId="user-123"
284
- traits={{
285
- name: 'John Doe',
286
- email: 'john@example.com'
287
- }}
288
- >
289
- <MyPage />
290
- </AnalyticsProvider>
291
- )
292
- }
293
- ```
294
-
295
- ### Analytics Hooks
296
-
297
- The package provides a set of hooks for analytics tracking using Segment. Here's how to use them:
298
-
299
- #### Setting up the AnalyticsProvider
300
-
301
- First, wrap your app with the AnalyticsProvider:
302
-
303
- ```typescript
304
- import { AnalyticsProvider } from '@dcl/hooks'
305
-
306
- function App() {
307
- return (
308
- <AnalyticsProvider
309
- writeKey="xyz1234"
310
- userId="user-123" // Optional
311
- traits={{ // Optional
312
- name: 'John Doe',
313
- email: 'john@example.com'
314
- }}
315
- >
316
- <Main />
317
- </AnalyticsProvider>
318
- )
319
- }
320
- ```
321
-
322
- #### Using the useAnalytics Hook
323
-
324
- The `useAnalytics` hook provides access to tracking functions:
325
-
326
- ```typescript
327
- import { useAnalytics } from '@dcl/hooks'
328
-
329
- function MyComponent() {
330
- const analytics = useAnalytics()
331
-
332
- const handleButtonClick = () => {
333
- if (analytics.isInitialized) {
334
- // Track an event
335
- analytics.track('Button Clicked', {
336
- buttonId: 'submit',
337
- timestamp: new Date().toISOString()
338
- })
339
-
340
- // Identify a user
341
- analytics.identify('user-123', {
342
- name: 'John Doe',
343
- email: 'john@example.com'
344
- })
345
- }
346
- }
347
-
348
- return (
349
- <button onClick={handleButtonClick}>
350
- Click me
351
- </button>
352
- )
353
- }
354
- ```
355
-
356
- #### Using the usePageTracking Hook
357
-
358
- The `usePageTracking` hook tracks page views when the pathname changes. You need to pass the current pathname as a parameter from your router:
359
-
360
- ```typescript
361
- import { usePageTracking } from '@dcl/hooks'
362
- import { useLocation } from 'react-router-dom'
363
-
364
- function MyPage() {
365
- const location = useLocation()
366
-
367
- // Tracks page view when pathname changes
368
- usePageTracking(location.pathname)
369
-
370
- return (
371
- <div>
372
- <h1>My Page</h1>
373
- {/* Your page content */}
374
- </div>
375
- )
376
- }
377
- ```
15
+ - `react` >= 18.0.0
16
+ - `decentraland-crypto-fetch` >= 2.0.1 (only needed for `useNotifications`)
378
17
 
379
- If you need to track additional page properties, you can use the `useAnalytics` hook directly:
380
-
381
- ```typescript
382
- import { useAnalytics } from '@dcl/hooks'
383
-
384
- function MyPage() {
385
- const analytics = useAnalytics()
386
-
387
- useEffect(() => {
388
- if (analytics.isInitialized) {
389
- analytics.page('My Page', {
390
- category: 'Content',
391
- section: 'Main',
392
- timestamp: new Date().toISOString()
393
- })
394
- }
395
- }, [analytics])
396
-
397
- return (
398
- <div>
399
- <h1>My Page</h1>
400
- {/* Your page content */}
401
- </div>
402
- )
403
- }
404
- ```
405
-
406
- #### Complete Example
407
-
408
- Here's a complete example showing how to use all analytics features together:
409
-
410
- ```typescript
411
- import { AnalyticsProvider, useAnalytics, usePageTracking } from '@dcl/hooks'
412
- import { useLocation } from 'react-router-dom'
413
-
414
- function MyPage() {
415
- const location = useLocation()
416
-
417
- // Track page views
418
- usePageTracking(location.pathname)
419
-
420
- return (
421
- <div>
422
- <h1>My Page</h1>
423
- <UserProfile />
424
- </div>
425
- )
426
- }
427
-
428
- function UserProfile() {
429
- const analytics = useAnalytics()
430
-
431
- const handleProfileUpdate = () => {
432
- if (analytics.isInitialized) {
433
- // Track profile update event
434
- analytics.track('Profile Updated', {
435
- timestamp: new Date().toISOString(),
436
- updateType: 'information'
437
- })
438
-
439
- // Update user traits
440
- analytics.identify('user-123', {
441
- lastUpdated: new Date().toISOString()
442
- })
443
- }
444
- }
445
-
446
- return (
447
- <button onClick={handleProfileUpdate}>
448
- Update Profile
449
- </button>
450
- )
451
- }
452
-
453
- function App() {
454
- return (
455
- <AnalyticsProvider
456
- writeKey="xyz1234"
457
- userId="user-123"
458
- traits={{
459
- name: 'John Doe',
460
- email: 'john@example.com'
461
- }}
462
- >
463
- <MyPage />
464
- </AnalyticsProvider>
465
- )
466
- }
467
- ```
468
-
469
- ### useTranslation
470
-
471
- The `useTranslation` hook provides i18n capabilities powered by `@formatjs/intl`, giving you access to advanced formatting functions for numbers, dates, currencies, and more.
472
-
473
- Basic usage:
474
-
475
- ```typescript
476
- import { useTranslation } from '@dcl/hooks'
477
-
478
- const translations = {
479
- en: {
480
- greeting: 'Hello, {name}!',
481
- welcome: 'Welcome to our app',
482
- items: '{count, plural, =0 {No items} one {# item} other {# items}}'
483
- },
484
- es: {
485
- greeting: 'Hola, {name}!',
486
- welcome: 'Bienvenido a nuestra aplicación',
487
- items: '{count, plural, =0 {Sin elementos} one {# elemento} other {# elementos}}'
488
- }
489
- }
490
-
491
- function MyComponent() {
492
- const { t, intl, locale, setLocale } = useTranslation({
493
- locale: 'en',
494
- translations
495
- })
496
-
497
- return (
498
- <div>
499
- <p>{t('greeting', { name: 'John' })}</p>
500
- <p>{t('items', { count: 5 })}</p>
501
- <button onClick={() => setLocale('es')}>
502
- Switch to Spanish
503
- </button>
504
- </div>
505
- )
506
- }
507
- ```
508
-
509
- Nested translations (dot notation):
510
-
511
- ```typescript
512
- const translations = {
513
- en: {
514
- components: {
515
- blog: {
516
- related_post: {
517
- title: 'Related posts'
518
- }
519
- }
520
- }
521
- }
522
- }
523
-
524
- function MyComponent() {
525
- const { t } = useTranslation({
526
- locale: 'en',
527
- translations
528
- })
529
-
530
- return <p>{t('components.blog.related_post.title')}</p>
531
- }
532
- ```
533
-
534
- Using the `intl` object for advanced formatting:
535
-
536
- ```typescript
537
- function AdvancedFormattingExample() {
538
- const { t, intl } = useTranslation({
539
- locale: 'en',
540
- translations: {
541
- en: {
542
- product_price: 'Price: {price}'
543
- }
544
- }
545
- })
546
-
547
- return (
548
- <div>
549
- {/* Format numbers */}
550
- <p>Count: {intl.formatNumber(1000)}</p>
551
-
552
- {/* Format dates */}
553
- <p>Today: {intl.formatDate(new Date(), {
554
- year: 'numeric',
555
- month: 'long',
556
- day: 'numeric'
557
- })}</p>
558
-
559
- {/* Format currency */}
560
- <p>{intl.formatNumber(99.99, {
561
- style: 'currency',
562
- currency: 'USD'
563
- })}</p>
564
-
565
- {/* Format relative time */}
566
- <p>{intl.formatRelativeTime(-1, 'day')}</p>
567
-
568
- {/* Use formatMessage directly */}
569
- <p>{intl.formatMessage({ id: 'product_price' }, { price: '$99' })}</p>
570
- </div>
571
- )
572
- }
573
- ```
574
-
575
- With fallback locale:
576
-
577
- ```typescript
578
- const translations = {
579
- en: {
580
- greeting: 'Hello!',
581
- welcome: 'Welcome!'
582
- },
583
- es: {
584
- greeting: 'Hola!'
585
- // 'welcome' is missing in Spanish
586
- }
587
- }
588
-
589
- function MyComponent() {
590
- const { t } = useTranslation({
591
- locale: 'es',
592
- translations,
593
- fallbackLocale: 'en' // Will use English if translation is missing
594
- })
595
-
596
- return (
597
- <div>
598
- <p>{t('greeting')}</p> {/* Shows: "Hola!" */}
599
- <p>{t('welcome')}</p> {/* Shows: "Welcome!" (from fallback) */}
600
- </div>
601
- )
602
- }
603
- ```
604
-
605
- Using TranslationProvider for context-based translations:
606
-
607
- ```typescript
608
- import { TranslationProvider, useTranslation } from '@dcl/hooks'
609
-
610
- const translations = {
611
- en: {
612
- greeting: 'Hello!',
613
- welcome: 'Welcome to our app'
614
- },
615
- es: {
616
- greeting: 'Hola!',
617
- welcome: 'Bienvenido a nuestra aplicación'
618
- }
619
- }
620
-
621
- function App() {
622
- return (
623
- <TranslationProvider
624
- locale="en"
625
- translations={translations}
626
- fallbackLocale="en"
627
- >
628
- <MyComponent />
629
- </TranslationProvider>
630
- )
631
- }
632
-
633
- function MyComponent() {
634
- // Can be used without options when inside TranslationProvider
635
- const { t, locale, setLocale } = useTranslation()
636
-
637
- return (
638
- <div>
639
- <p>{t('greeting')}</p>
640
- <p>{t('welcome')}</p>
641
- <button onClick={() => setLocale('es')}>
642
- Switch to Spanish
643
- </button>
644
- </div>
645
- )
646
- }
647
- ```
648
-
649
- Using ICU Message Syntax:
650
-
651
- ```typescript
652
- const translations = {
653
- en: {
654
- // Pluralization
655
- items: '{count, plural, =0 {No items} one {# item} other {# items}}',
656
- // Select syntax
657
- gender: '{gender, select, male {He} female {She} other {They}}',
658
- // Complex ICU
659
- notification: '{count, plural, =0 {No notifications} =1 {You have one notification} other {You have # notifications}}'
660
- }
661
- }
662
-
663
- function MyComponent() {
664
- const { t } = useTranslation({
665
- locale: 'en',
666
- translations
667
- })
668
-
669
- return (
670
- <div>
671
- <p>{t('items', { count: 0 })}</p> {/* "No items" */}
672
- <p>{t('items', { count: 1 })}</p> {/* "1 item" */}
673
- <p>{t('items', { count: 5 })}</p> {/* "5 items" */}
674
- <p>{t('gender', { gender: 'male' })}</p> {/* "He" */}
675
- <p>{t('notification', { count: 3 })}</p> {/* "You have 3 notifications" */}
676
- </div>
677
- )
678
- }
679
- ```
680
-
681
- Additional intl formatting functions:
682
-
683
- ```typescript
684
- function MyComponent() {
685
- const { intl } = useTranslation({
686
- locale: 'en',
687
- translations: { en: {} }
688
- })
689
-
690
- return (
691
- <div>
692
- {/* Format lists */}
693
- <p>{intl.formatList(['apple', 'banana', 'orange'])}</p>
694
- {/* "apple, banana, and orange" */}
695
-
696
- {/* Format display names */}
697
- <p>{intl.formatDisplayName('es', { type: 'language' })}</p>
698
- {/* "Spanish" */}
699
-
700
- <p>{intl.formatDisplayName('US', { type: 'region' })}</p>
701
- {/* "United States" */}
702
- </div>
703
- )
704
- }
705
- ```
706
-
707
- ### useNotifications
708
-
709
- The `useNotifications` hook manages notification polling, modal state, and onboarding flow. It uses Decentraland's notifications API by default.
710
-
711
- Basic usage:
712
-
713
- ```typescript
714
- import { useNotifications } from '@dcl/hooks'
715
- import type { AuthIdentity } from 'decentraland-crypto-fetch'
716
-
717
- function NotificationsComponent() {
718
- const identity: AuthIdentity = useAuthIdentity() // From your auth context
719
-
720
- const {
721
- notifications,
722
- isLoading,
723
- isModalOpen,
724
- isNotificationsOnboarding,
725
- handleNotificationsOpen,
726
- handleOnBegin
727
- } = useNotifications({
728
- identity,
729
- isNotificationsEnabled: !!identity,
730
- notificationsUrl: 'https://notifications.decentraland.org' // or .zone for dev
731
- })
732
-
733
- if (isNotificationsOnboarding) {
734
- return (
735
- <div>
736
- <h2>Welcome to Notifications!</h2>
737
- <button onClick={handleOnBegin}>Get Started</button>
738
- </div>
739
- )
740
- }
741
-
742
- return (
743
- <div>
744
- <button onClick={handleNotificationsOpen}>
745
- Notifications ({notifications.filter(n => !n.read).length})
746
- </button>
747
-
748
- {isModalOpen && (
749
- <ul>
750
- {notifications.map(notification => (
751
- <li key={notification.id}>
752
- {notification.type} - {notification.read ? 'Read' : 'Unread'}
753
- </li>
754
- ))}
755
- </ul>
756
- )}
757
- </div>
758
- )
759
- }
760
- ```
761
-
762
- With custom polling interval:
763
-
764
- ```typescript
765
- const { notifications } = useNotifications({
766
- identity,
767
- isNotificationsEnabled: !!identity,
768
- notificationsUrl: 'https://notifications.decentraland.org',
769
- queryIntervalMs: 30000 // Poll every 30 seconds (default: 60000)
770
- })
771
- ```
772
-
773
- With notification type filtering:
774
-
775
- ```typescript
776
- const { notifications } = useNotifications({
777
- identity,
778
- isNotificationsEnabled: !!identity,
779
- notificationsUrl: 'https://notifications.decentraland.org',
780
- availableNotificationTypes: ['bid', 'sale', 'royalties']
781
- })
782
- ```
783
-
784
- With error handling:
785
-
786
- `onError` is called when fetching fails, marking as read fails, or when
787
- `notificationsUrl` is missing.
18
+ ## Available Hooks
788
19
 
789
- ```typescript
790
- const { notifications } = useNotifications({
791
- identity,
792
- isNotificationsEnabled: !!identity,
793
- notificationsUrl: 'https://notifications.decentraland.org',
794
- onError: (error) => {
795
- console.error('Notification error:', error)
796
- Sentry.captureException(error)
797
- }
798
- })
799
- ```
20
+ | Hook | Description | Docs |
21
+ | -------------------------- | ------------------------------------------------------------- | ---------------------------------------- |
22
+ | `useAsyncState` | Async state management with auto-reload on dependency changes | [docs](docs/useAsyncState.md) |
23
+ | `useAsyncMemo` | Alias for `useAsyncState` | [docs](docs/useAsyncState.md) |
24
+ | `useAsyncEffect` | Async version of `useEffect` with error tracking | [docs](docs/useAsyncEffect.md) |
25
+ | `useAsyncTask` | Single imperative async task with loading state | [docs](docs/useAsyncTask.md) |
26
+ | `useAsyncTasks` | Multiple concurrent async tasks managed by ID | [docs](docs/useAsyncTasks.md) |
27
+ | `usePatchState` | Partial state updates for complex objects | [docs](docs/usePatchState.md) |
28
+ | `useInfiniteScroll` | Infinite scroll with debounce and threshold | [docs](docs/useInfiniteScroll.md) |
29
+ | `useAdvancedUserAgentData` | Browser, OS, CPU, and device detection | [docs](docs/useAdvancedUserAgentData.md) |
30
+ | `useAnalytics` | Segment analytics tracking (requires `AnalyticsProvider`) | [docs](docs/useAnalytics.md) |
31
+ | `usePageTracking` | Page view tracking (requires `AnalyticsProvider`) | [docs](docs/useAnalytics.md) |
32
+ | `useTranslation` | i18n with ICU message syntax via `@formatjs/intl` | [docs](docs/useTranslation.md) |
33
+ | `useNotifications` | Decentraland notification polling and modal state | [docs](docs/useNotifications.md) |
34
+
35
+ ## Providers
36
+
37
+ | Provider | Description | Docs |
38
+ | --------------------- | --------------------------------- | ------------------------------ |
39
+ | `AnalyticsProvider` | Segment analytics context | [docs](docs/useAnalytics.md) |
40
+ | `TranslationProvider` | i18n context for `useTranslation` | [docs](docs/useTranslation.md) |
41
+
42
+ ## Utilities
43
+
44
+ | Export | Description | Docs |
45
+ | --------------------------------------------------------- | -------------------------------------------------- | -------------------------------- |
46
+ | `getStorageItem` / `setStorageItem` / `removeStorageItem` | Typed localStorage helpers with JSON serialization | [docs](docs/utilities.md) |
47
+ | `createNotificationsClient` | Decentraland notifications API client | [docs](docs/useNotifications.md) |
48
+
49
+ ## Documentation
50
+
51
+ - **[docs/](docs/)** -- Detailed per-hook documentation with examples
52
+ - **[AGENTS.md](AGENTS.md)** -- Compact API reference for LLM/AI consumption
53
+ - **[docs/contributing.md](docs/contributing.md)** -- Contribution guide and internal patterns
800
54
 
801
55
  ## License
802
56
 
803
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
57
+ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
@@ -1,4 +1,6 @@
1
1
  import { AdvancedNavigatorUAData } from "./useAdvancedUserAgentData.type";
2
+ /** @internal Reset module-level cache — only for testing. */
3
+ export declare function resetUserAgentCache(): void;
2
4
  /**
3
5
  * extract or infer the [UserAgentData](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData)
4
6
  * that is an object which can be used to access the User-Agent Client Hints API.
@@ -3,14 +3,31 @@ import { UAParser } from "ua-parser-js";
3
3
  import { isAppleSilicon } from "ua-parser-js/device-detection";
4
4
  import { useAsyncEffect } from "../useAsyncEffect";
5
5
  const DEFAULT_VALUE = "Unknown";
6
+ // Module-level cache: the user agent never changes during a session, so once
7
+ // resolved the result is reused across component remounts (React StrictMode,
8
+ // Suspense boundaries, lazy chunks, etc.). Without this cache every remount
9
+ // re-runs the async Client Hints call, resetting state to undefined in between
10
+ // and causing a visible flash in any UI that depends on the detected OS.
11
+ let _cachedData;
12
+ let _cacheResolved = false;
13
+ /** @internal Reset module-level cache — only for testing. */
14
+ export function resetUserAgentCache() {
15
+ _cachedData = undefined;
16
+ _cacheResolved = false;
17
+ }
6
18
  /**
7
19
  * extract or infer the [UserAgentData](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData)
8
20
  * that is an object which can be used to access the User-Agent Client Hints API.
9
21
  */
10
22
  export function useAdvancedUserAgentData() {
11
- const [isLoading, setLoading] = useState(true);
12
- const [data, setData] = useState();
23
+ const [isLoading, setLoading] = useState(!_cacheResolved);
24
+ const [data, setData] = useState(_cachedData);
13
25
  useAsyncEffect(async () => {
26
+ // useState initializers already picked up the cached values,
27
+ // so no setter calls needed — just skip the async work.
28
+ if (_cacheResolved) {
29
+ return;
30
+ }
14
31
  setLoading(true);
15
32
  const ua = new UAParser(navigator.userAgent);
16
33
  const uaData = ua.getResult();
@@ -41,7 +58,7 @@ export function useAdvancedUserAgentData() {
41
58
  else {
42
59
  architecture = cpuData.architecture;
43
60
  }
44
- setData({
61
+ const result = {
45
62
  browser,
46
63
  engine,
47
64
  os,
@@ -50,9 +67,12 @@ export function useAdvancedUserAgentData() {
50
67
  },
51
68
  mobile: ua.getDevice().is("mobile"),
52
69
  tablet: ua.getDevice().is("tablet"),
53
- });
70
+ };
71
+ _cachedData = result;
72
+ _cacheResolved = true;
73
+ setData(result);
54
74
  setLoading(false);
55
75
  }, []);
56
- return [isLoading, data];
76
+ return [isLoading, _cachedData ?? data];
57
77
  }
58
78
  //# sourceMappingURL=useAdvancedUserAgentData.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAdvancedUserAgentData.js","sourceRoot":"","sources":["../../../src/hooks/useAdvancedUserAgentData/useAdvancedUserAgentData.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AAE9D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,MAAM,aAAa,GAAG,SAAS,CAAA;AAE/B;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IAItC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;IAC9C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,EAA2B,CAAA;IAE3D,cAAc,CAAC,KAAK,IAAI,EAAE;QACxB,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;QAC5C,MAAM,MAAM,GAAG,EAAE,CAAC,SAAS,EAAE,CAAA;QAE7B,MAAM,OAAO,GAAG;YACd,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,aAAa;YAC1C,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,aAAa;SACjD,CAAA;QACD,MAAM,MAAM,GAAG;YACb,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,aAAa;YACzC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,aAAa;SAChD,CAAA;QACD,MAAM,CAAC,qBAAqB,EAAE,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACjE,MAAM,CAAC,eAAe,EAAE;YACxB,EAAE,CAAC,KAAK,EAAE,CAAC,eAAe,EAAE;YAC5B,EAAE,CAAC,MAAM,EAAE,CAAC,eAAe,EAAE;SAC9B,CAAC,CAAA;QAEF,MAAM,EAAE,GAAG;YACT,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,aAAa;YAClC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,aAAa;SACzC,CAAA;QAED,IAAI,YAAoB,CAAA;QACxB,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;YAC1B,YAAY;gBACV,EAAE,CAAC,IAAI,KAAK,OAAO,IAAI,cAAc,CAAC,qBAAqB,CAAC;oBAC1D,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,SAAS,CAAA;QACjB,CAAC;aAAM,CAAC;YACN,YAAY,GAAG,OAAO,CAAC,YAAY,CAAA;QACrC,CAAC;QAED,OAAO,CAAC;YACN,OAAO;YACP,MAAM;YACN,EAAE;YACF,GAAG,EAAE;gBACH,YAAY;aACb;YACD,MAAM,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC;YACnC,MAAM,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC;SACpC,CAAC,CAAA;QAEF,UAAU,CAAC,KAAK,CAAC,CAAA;IACnB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;AAC1B,CAAC"}
1
+ {"version":3,"file":"useAdvancedUserAgentData.js","sourceRoot":"","sources":["../../../src/hooks/useAdvancedUserAgentData/useAdvancedUserAgentData.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcl/hooks",
3
- "version": "1.2.1",
3
+ "version": "1.2.2-20260330212234.commit-f13404f",
4
4
  "type": "module",
5
5
  "main": "esm/index.js",
6
6
  "module": "esm/index.js",
@@ -80,5 +80,5 @@
80
80
  "<rootDir>/test/setup.ts"
81
81
  ]
82
82
  },
83
- "commit": "7c0554a473b47b689d45deb12401664a820ab35c"
83
+ "commit": "f13404fa4a4a9988fcc805b9fe95ee583025eec5"
84
84
  }