@usefy/use-geolocation 0.0.28

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 ADDED
@@ -0,0 +1,645 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">@usefy/use-geolocation</h1>
6
+
7
+ <p align="center">
8
+ <strong>A powerful React hook for accessing device geolocation with real-time tracking and distance calculation</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@usefy/use-geolocation">
13
+ <img src="https://img.shields.io/npm/v/@usefy/use-geolocation.svg?style=flat-square&color=007acc" alt="npm version" />
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@usefy/use-geolocation">
16
+ <img src="https://img.shields.io/npm/dm/@usefy/use-geolocation.svg?style=flat-square&color=007acc" alt="npm downloads" />
17
+ </a>
18
+ <a href="https://bundlephobia.com/package/@usefy/use-geolocation">
19
+ <img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-geolocation?style=flat-square&color=007acc" alt="bundle size" />
20
+ </a>
21
+ <a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE">
22
+ <img src="https://img.shields.io/npm/l/@usefy/use-geolocation.svg?style=flat-square&color=007acc" alt="license" />
23
+ </a>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="#installation">Installation</a> •
28
+ <a href="#quick-start">Quick Start</a> •
29
+ <a href="#api-reference">API Reference</a> •
30
+ <a href="#examples">Examples</a> •
31
+ <a href="#license">License</a>
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-usegeolocation--docs" target="_blank" rel="noopener noreferrer">
36
+ <strong>📚 View Storybook Demo</strong>
37
+ </a>
38
+ </p>
39
+
40
+ ---
41
+
42
+ ## Overview
43
+
44
+ `@usefy/use-geolocation` is a feature-rich React hook for accessing device geolocation with real-time tracking, distance calculation, and comprehensive error handling. It provides a simple API for getting current position, watching position changes, calculating distances, and tracking permission states.
45
+
46
+ **Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
47
+
48
+ ### Why use-geolocation?
49
+
50
+ - **Zero Dependencies** — Pure React implementation with no external dependencies
51
+ - **TypeScript First** — Full type safety with comprehensive type definitions
52
+ - **Real-Time Tracking** — Watch position changes as device moves
53
+ - **Distance Calculation** — Built-in Haversine formula for accurate distance calculations
54
+ - **Bearing Calculation** — Calculate direction/bearing between coordinates
55
+ - **Permission Tracking** — Monitor geolocation permission state changes
56
+ - **Error Handling** — Comprehensive error handling with typed error codes
57
+ - **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
58
+ - **Stable References** — Memoized functions for optimal performance
59
+ - **Well Tested** — Comprehensive test coverage with Vitest
60
+
61
+ ---
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ # npm
67
+ npm install @usefy/use-geolocation
68
+
69
+ # yarn
70
+ yarn add @usefy/use-geolocation
71
+
72
+ # pnpm
73
+ pnpm add @usefy/use-geolocation
74
+ ```
75
+
76
+ ### Peer Dependencies
77
+
78
+ This package requires React 18 or 19:
79
+
80
+ ```json
81
+ {
82
+ "peerDependencies": {
83
+ "react": "^18.0.0 || ^19.0.0"
84
+ }
85
+ }
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Quick Start
91
+
92
+ ```tsx
93
+ import { useGeolocation } from "@usefy/use-geolocation";
94
+
95
+ function MyLocation() {
96
+ const { position, loading, error } = useGeolocation();
97
+
98
+ if (loading) return <p>Loading location...</p>;
99
+ if (error) return <p>Error: {error.message}</p>;
100
+ if (!position) return <p>No position yet</p>;
101
+
102
+ return (
103
+ <div>
104
+ <p>Latitude: {position.coords.latitude}</p>
105
+ <p>Longitude: {position.coords.longitude}</p>
106
+ <p>Accuracy: {position.coords.accuracy}m</p>
107
+ </div>
108
+ );
109
+ }
110
+ ```
111
+
112
+ ---
113
+
114
+ ## API Reference
115
+
116
+ ### `useGeolocation(options?)`
117
+
118
+ A hook that manages geolocation state with real-time tracking and utility functions.
119
+
120
+ #### Parameters
121
+
122
+ | Parameter | Type | Description |
123
+ | --------- | ------------------------- | ----------------------------- |
124
+ | `options` | `UseGeolocationOptions` | Optional configuration object |
125
+
126
+ #### Options
127
+
128
+ | Option | Type | Default | Description |
129
+ | ------------------- | --------------------------------------- | --------- | ----------------------------------------------------------------- |
130
+ | `enableHighAccuracy` | `boolean` | `false` | Enable high accuracy mode (uses GPS, consumes more battery) |
131
+ | `maximumAge` | `number` | `0` | Maximum age of cached position in milliseconds |
132
+ | `timeout` | `number` | `30000` | Timeout in milliseconds for position request |
133
+ | `watch` | `boolean` | `false` | Start watching position immediately on mount |
134
+ | `immediate` | `boolean` | `true` | Get initial position immediately on mount |
135
+ | `onSuccess` | `(position: GeoPosition) => void` | — | Callback when position is successfully retrieved |
136
+ | `onError` | `(error: GeolocationError) => void` | — | Callback when an error occurs |
137
+ | `onPositionChange` | `(position: GeoPosition) => void` | — | Callback when position changes (during watch mode) |
138
+ | `onPermissionChange` | `(state: PermissionState) => void` | — | Callback when permission state changes |
139
+
140
+ #### Returns `UseGeolocationReturn`
141
+
142
+ | Property | Type | Description |
143
+ | ------------------- | --------------------------------------- | ---------------------------------------------------------------- |
144
+ | `position` | `GeoPosition \| null` | Current position data (null if not yet retrieved) |
145
+ | `loading` | `boolean` | Loading state (true while fetching position) |
146
+ | `error` | `GeolocationError \| null` | Error object (null if no error) |
147
+ | `permission` | `PermissionState` | Current permission state (`prompt`, `granted`, `denied`, `unavailable`) |
148
+ | `isSupported` | `boolean` | Whether geolocation is supported in this environment |
149
+ | `getCurrentPosition` | `() => void` | Manually get current position (one-time request) |
150
+ | `watchPosition` | `() => void` | Start watching position for real-time updates |
151
+ | `clearWatch` | `() => void` | Stop watching position |
152
+ | `distanceFrom` | `(lat: number, lon: number) => number \| null` | Calculate distance from current position to target coordinates in meters |
153
+ | `bearingTo` | `(lat: number, lon: number) => number \| null` | Calculate bearing/direction from current position to target coordinates (0-360 degrees) |
154
+
155
+ #### Error Codes
156
+
157
+ | Code | Description |
158
+ | --------------------- | ---------------------------------------------- |
159
+ | `PERMISSION_DENIED` | User denied geolocation permission |
160
+ | `POSITION_UNAVAILABLE` | Position information unavailable |
161
+ | `TIMEOUT` | Position request timed out |
162
+ | `NOT_SUPPORTED` | Geolocation is not supported in this environment |
163
+
164
+ ---
165
+
166
+ ## Examples
167
+
168
+ ### Basic Usage
169
+
170
+ ```tsx
171
+ import { useGeolocation } from "@usefy/use-geolocation";
172
+
173
+ function CurrentLocation() {
174
+ const { position, loading, error } = useGeolocation();
175
+
176
+ if (loading) return <div>Loading location...</div>;
177
+ if (error) return <div>Error: {error.message}</div>;
178
+ if (!position) return <div>No position available</div>;
179
+
180
+ return (
181
+ <div>
182
+ <p>Latitude: {position.coords.latitude}</p>
183
+ <p>Longitude: {position.coords.longitude}</p>
184
+ <p>Accuracy: {position.coords.accuracy}m</p>
185
+ </div>
186
+ );
187
+ }
188
+ ```
189
+
190
+ ### Manual Control
191
+
192
+ ```tsx
193
+ import { useGeolocation } from "@usefy/use-geolocation";
194
+
195
+ function ManualLocation() {
196
+ const {
197
+ position,
198
+ loading,
199
+ error,
200
+ getCurrentPosition,
201
+ watchPosition,
202
+ clearWatch,
203
+ } = useGeolocation({ immediate: false, watch: false });
204
+
205
+ return (
206
+ <div>
207
+ <button onClick={getCurrentPosition} disabled={loading}>
208
+ Get Location
209
+ </button>
210
+ <button onClick={watchPosition}>Start Tracking</button>
211
+ <button onClick={clearWatch}>Stop Tracking</button>
212
+
213
+ {position && (
214
+ <p>
215
+ {position.coords.latitude}, {position.coords.longitude}
216
+ </p>
217
+ )}
218
+ </div>
219
+ );
220
+ }
221
+ ```
222
+
223
+ ### Real-Time Tracking
224
+
225
+ ```tsx
226
+ import { useGeolocation } from "@usefy/use-geolocation";
227
+
228
+ function LiveTracking() {
229
+ const { position, watchPosition, clearWatch } = useGeolocation({
230
+ immediate: false,
231
+ watch: false,
232
+ onPositionChange: (pos) => {
233
+ console.log("Position updated:", pos);
234
+ },
235
+ });
236
+
237
+ const [isTracking, setIsTracking] = useState(false);
238
+
239
+ const handleStart = () => {
240
+ watchPosition();
241
+ setIsTracking(true);
242
+ };
243
+
244
+ const handleStop = () => {
245
+ clearWatch();
246
+ setIsTracking(false);
247
+ };
248
+
249
+ return (
250
+ <div>
251
+ <button onClick={handleStart} disabled={isTracking}>
252
+ Start Tracking
253
+ </button>
254
+ <button onClick={handleStop} disabled={!isTracking}>
255
+ Stop Tracking
256
+ </button>
257
+
258
+ {isTracking && <p>🔴 Live tracking active</p>}
259
+
260
+ {position && (
261
+ <p>
262
+ {position.coords.latitude.toFixed(6)},{" "}
263
+ {position.coords.longitude.toFixed(6)}
264
+ </p>
265
+ )}
266
+ </div>
267
+ );
268
+ }
269
+ ```
270
+
271
+ ### Distance Calculation
272
+
273
+ ```tsx
274
+ import { useGeolocation } from "@usefy/use-geolocation";
275
+
276
+ function DistanceToDestination() {
277
+ const { position, distanceFrom } = useGeolocation();
278
+
279
+ // New York City coordinates
280
+ const nyLat = 40.7128;
281
+ const nyLon = -74.0060;
282
+
283
+ const distance = distanceFrom(nyLat, nyLon);
284
+
285
+ return (
286
+ <div>
287
+ {distance && (
288
+ <p>Distance to NYC: {(distance / 1000).toFixed(2)} km</p>
289
+ )}
290
+ </div>
291
+ );
292
+ }
293
+ ```
294
+
295
+ ### Bearing/Direction Calculation
296
+
297
+ ```tsx
298
+ import { useGeolocation } from "@usefy/use-geolocation";
299
+
300
+ function DirectionToDestination() {
301
+ const { position, bearingTo } = useGeolocation();
302
+
303
+ // London coordinates
304
+ const londonLat = 51.5074;
305
+ const londonLon = -0.1278;
306
+
307
+ const bearing = bearingTo(londonLat, londonLon);
308
+
309
+ const getDirection = (bearing: number | null) => {
310
+ if (bearing === null) return "—";
311
+ if (bearing < 22.5 || bearing >= 337.5) return "North";
312
+ if (bearing < 67.5) return "Northeast";
313
+ if (bearing < 112.5) return "East";
314
+ if (bearing < 157.5) return "Southeast";
315
+ if (bearing < 202.5) return "South";
316
+ if (bearing < 247.5) return "Southwest";
317
+ if (bearing < 292.5) return "West";
318
+ return "Northwest";
319
+ };
320
+
321
+ return (
322
+ <div>
323
+ {bearing !== null && (
324
+ <p>
325
+ Direction to London: {getDirection(bearing)} ({bearing.toFixed(0)}°)
326
+ </p>
327
+ )}
328
+ </div>
329
+ );
330
+ }
331
+ ```
332
+
333
+ ### High Accuracy Mode
334
+
335
+ ```tsx
336
+ import { useGeolocation } from "@usefy/use-geolocation";
337
+
338
+ function HighAccuracyLocation() {
339
+ const { position, loading } = useGeolocation({
340
+ enableHighAccuracy: true,
341
+ timeout: 10000,
342
+ });
343
+
344
+ if (loading) return <div>Getting high accuracy location...</div>;
345
+
346
+ return (
347
+ <div>
348
+ {position && (
349
+ <p>
350
+ High accuracy: {position.coords.accuracy.toFixed(1)}m
351
+ </p>
352
+ )}
353
+ </div>
354
+ );
355
+ }
356
+ ```
357
+
358
+ ### Permission State Tracking
359
+
360
+ ```tsx
361
+ import { useGeolocation } from "@usefy/use-geolocation";
362
+
363
+ function PermissionStatus() {
364
+ const { permission, onPermissionChange } = useGeolocation({
365
+ onPermissionChange: (state) => {
366
+ console.log("Permission changed:", state);
367
+ },
368
+ });
369
+
370
+ return (
371
+ <div>
372
+ <p>Permission: {permission}</p>
373
+ {permission === "denied" && (
374
+ <p>Please enable location permissions in your browser settings.</p>
375
+ )}
376
+ </div>
377
+ );
378
+ }
379
+ ```
380
+
381
+ ### Error Handling
382
+
383
+ ```tsx
384
+ import { useGeolocation } from "@usefy/use-geolocation";
385
+
386
+ function RobustLocation() {
387
+ const { position, error, getCurrentPosition } = useGeolocation({
388
+ immediate: false,
389
+ onError: (err) => {
390
+ console.error("Geolocation error:", err.code, err.message);
391
+
392
+ if (err.code === "PERMISSION_DENIED") {
393
+ alert("Please allow location access");
394
+ } else if (err.code === "TIMEOUT") {
395
+ alert("Location request timed out. Please try again.");
396
+ }
397
+ },
398
+ });
399
+
400
+ return (
401
+ <div>
402
+ <button onClick={getCurrentPosition}>Get Location</button>
403
+
404
+ {error && (
405
+ <div className="error">
406
+ <p>Error: {error.code}</p>
407
+ <p>{error.message}</p>
408
+ </div>
409
+ )}
410
+
411
+ {position && (
412
+ <p>
413
+ {position.coords.latitude}, {position.coords.longitude}
414
+ </p>
415
+ )}
416
+ </div>
417
+ );
418
+ }
419
+ ```
420
+
421
+ ### Auto-Watch Mode
422
+
423
+ ```tsx
424
+ import { useGeolocation } from "@usefy/use-geolocation";
425
+
426
+ function AutoTracking() {
427
+ const { position, clearWatch } = useGeolocation({
428
+ watch: true, // Automatically start watching on mount
429
+ immediate: false,
430
+ });
431
+
432
+ return (
433
+ <div>
434
+ <p>Auto-tracking is active</p>
435
+ <button onClick={clearWatch}>Stop</button>
436
+
437
+ {position && (
438
+ <p>
439
+ {position.coords.latitude.toFixed(6)},{" "}
440
+ {position.coords.longitude.toFixed(6)}
441
+ </p>
442
+ )}
443
+ </div>
444
+ );
445
+ }
446
+ ```
447
+
448
+ ---
449
+
450
+ ## TypeScript
451
+
452
+ This hook is written in TypeScript and exports comprehensive type definitions.
453
+
454
+ ```tsx
455
+ import {
456
+ useGeolocation,
457
+ type UseGeolocationOptions,
458
+ type UseGeolocationReturn,
459
+ type GeoPosition,
460
+ type GeolocationError,
461
+ type PermissionState,
462
+ } from "@usefy/use-geolocation";
463
+
464
+ // Full type inference
465
+ const geolocation: UseGeolocationReturn = useGeolocation({
466
+ enableHighAccuracy: true,
467
+ timeout: 10000,
468
+ onSuccess: (position: GeoPosition) => {
469
+ console.log("Got position:", position);
470
+ },
471
+ onError: (error: GeolocationError) => {
472
+ console.error("Error:", error.code, error.message);
473
+ },
474
+ });
475
+
476
+ // Permission state is typed as union
477
+ const permission: PermissionState = geolocation.permission;
478
+ // "prompt" | "granted" | "denied" | "unavailable"
479
+ ```
480
+
481
+ ---
482
+
483
+ ## Performance
484
+
485
+ - **Stable Function References** — All control functions are memoized with `useCallback`
486
+ - **Smart Re-renders** — Only re-renders when position, loading, or error state changes
487
+ - **Automatic Cleanup** — Watch is automatically cleared on unmount
488
+ - **Options Auto-Restart** — Watch automatically restarts when options change
489
+
490
+ ```tsx
491
+ const { getCurrentPosition, watchPosition, clearWatch } = useGeolocation();
492
+
493
+ // These references remain stable across renders
494
+ useEffect(() => {
495
+ // Safe to use as dependencies
496
+ }, [getCurrentPosition, watchPosition, clearWatch]);
497
+ ```
498
+
499
+ ---
500
+
501
+ ## Testing
502
+
503
+ This package maintains comprehensive test coverage to ensure reliability and stability.
504
+
505
+ ### Test Coverage
506
+
507
+ 📊 <a href="https://mirunamu00.github.io/usefy/coverage/use-geolocation/src/index.html" target="_blank" rel="noopener noreferrer"><strong>View Detailed Coverage Report</strong></a> (GitHub Pages)
508
+
509
+ ### Test Categories
510
+
511
+ <details>
512
+ <summary><strong>Initialization Tests</strong></summary>
513
+
514
+ - Initialize with correct default state
515
+ - Detect unsupported environment
516
+ - Set loading state when immediate is true
517
+
518
+ </details>
519
+
520
+ <details>
521
+ <summary><strong>getCurrentPosition Tests</strong></summary>
522
+
523
+ - Get current position successfully
524
+ - Handle PERMISSION_DENIED error
525
+ - Handle POSITION_UNAVAILABLE error
526
+ - Handle TIMEOUT error
527
+ - Handle NOT_SUPPORTED error when geolocation unavailable
528
+
529
+ </details>
530
+
531
+ <details>
532
+ <summary><strong>watchPosition Tests</strong></summary>
533
+
534
+ - Watch position and receive updates
535
+ - Clear watch on clearWatch call
536
+ - Clear existing watch before starting new one
537
+ - Call onPositionChange callback on updates
538
+
539
+ </details>
540
+
541
+ <details>
542
+ <summary><strong>Permission State Tests</strong></summary>
543
+
544
+ - Track permission state changes
545
+ - Set permission to unavailable when permissions API fails
546
+ - Call onPermissionChange callback when permission changes
547
+
548
+ </details>
549
+
550
+ <details>
551
+ <summary><strong>Utility Functions Tests</strong></summary>
552
+
553
+ - Calculate distance correctly using Haversine formula
554
+ - Return null when no position available for distanceFrom
555
+ - Calculate bearing correctly
556
+ - Return null when no position available for bearingTo
557
+
558
+ </details>
559
+
560
+ <details>
561
+ <summary><strong>Options Tests</strong></summary>
562
+
563
+ - Use custom options (enableHighAccuracy, timeout, maximumAge)
564
+ - Use default timeout of 30000ms
565
+ - Call onSuccess callback
566
+ - Call onError callback
567
+
568
+ </details>
569
+
570
+ <details>
571
+ <summary><strong>Auto-Watch and Immediate Tests</strong></summary>
572
+
573
+ - Start watching immediately when watch: true
574
+ - Get position immediately when immediate: true
575
+ - Not fetch position when immediate: false
576
+
577
+ </details>
578
+
579
+ <details>
580
+ <summary><strong>Options Auto-Restart Tests</strong></summary>
581
+
582
+ - Restart watch when enableHighAccuracy changes
583
+ - Restart watch when timeout changes
584
+ - Not restart watch when watch is false
585
+
586
+ </details>
587
+
588
+ <details>
589
+ <summary><strong>Function Reference Stability Tests</strong></summary>
590
+
591
+ - Maintain stable function references across renders
592
+ - Update distanceFrom and bearingTo when position changes
593
+
594
+ </details>
595
+
596
+ <details>
597
+ <summary><strong>Cleanup Tests</strong></summary>
598
+
599
+ - Clear watch on unmount
600
+ - Clear watch multiple times safely
601
+
602
+ </details>
603
+
604
+ ### Running Tests
605
+
606
+ ```bash
607
+ # Run all tests
608
+ pnpm test
609
+
610
+ # Run tests in watch mode
611
+ pnpm test:watch
612
+
613
+ # Run tests with coverage report
614
+ pnpm test --coverage
615
+ ```
616
+
617
+ ---
618
+
619
+ ## Browser Compatibility
620
+
621
+ This hook uses the [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API), which is supported in:
622
+
623
+ - ✅ Chrome 5+
624
+ - ✅ Firefox 3.5+
625
+ - ✅ Safari 5+
626
+ - ✅ Edge 12+
627
+ - ✅ Opera 10.6+
628
+ - ✅ Mobile browsers (iOS Safari, Chrome Mobile)
629
+
630
+ **Note:** HTTPS is required for geolocation in most modern browsers (except localhost).
631
+
632
+ ---
633
+
634
+ ## License
635
+
636
+ MIT © [mirunamu](https://github.com/mirunamu00)
637
+
638
+ This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
639
+
640
+ ---
641
+
642
+ <p align="center">
643
+ <sub>Built with care by the usefy team</sub>
644
+ </p>
645
+