Skip to content

Commit 255b166

Browse files
committed
[IMPL] useGeolocation hook
1 parent 1f5e105 commit 255b166

10 files changed

Lines changed: 236 additions & 21 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useState } from "react";
2+
import { useGeolocation } from "../../../../../../packages/react-tools/src"
3+
4+
/**
5+
The component displays geographic location info.
6+
*/
7+
export const UseGeolocation = () => {
8+
const [error, setError] = useState("")
9+
const [location, currentPosition] = useGeolocation({
10+
mode: "manual",
11+
onError(error) {
12+
setError(error.message);
13+
}
14+
});
15+
16+
return (<div style={{ textAlign: "left", width: 'fit-content', margin:'0 auto' }}>
17+
{
18+
error && <p style={{ color: 'red' }}>{error}</p>
19+
}
20+
<br/>
21+
<button onClick={currentPosition}>Get Location</button>
22+
<br />
23+
<p >isSupported: {location.isSupported ? "true" : "false"}</p>
24+
<p >Timestamp: {location?.position?.timestamp}</p>
25+
<p >Coords:</p>
26+
<div style={{paddingLeft: 10, textAlign: 'left', width: 'fit-content', margin: '0 auto'}}>
27+
<p>accuracy: {location.position?.coords.accuracy}</p>
28+
<p>altitude: {location.position?.coords.altitude}</p>
29+
<p>altitudeAccuracy: {location.position?.coords.altitudeAccuracy}</p>
30+
<p>heading: {location.position?.coords.heading}</p>
31+
<p>latitude: {location.position?.coords.latitude}</p>
32+
<p>longitude: {location.position?.coords.longitude}</p>
33+
<p>speed: {location.position?.coords.speed}</p>
34+
</div>
35+
</div>)
36+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export const COMPONENTS = [
7979
"useTitle",
8080
"useIdle",
8181
"useFullscreen",
82-
"useBattery"
82+
"useBattery",
83+
"useGeolocation"
8384
]
8485
],
8586
//UTILS

apps/react-tools-demo/src/index.css

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,4 @@ button:hover {
6060
button:focus,
6161
button:focus-visible {
6262
outline: 4px auto -webkit-focus-ring-color;
63-
}
64-
65-
@media (prefers-color-scheme: light) {
66-
:root {
67-
color: #213547;
68-
background-color: #ffffff;
69-
}
70-
71-
a:hover {
72-
color: #747bff;
73-
}
74-
75-
button {
76-
background-color: #f9f9f9;
77-
}
7863
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# useGeolocation
2+
Hook to use user's geographic location. Refer to [GeoLocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseGeolocation = () => {
8+
const [error, setError] = useState("")
9+
const [location] = useGeolocation({onError(error) {
10+
setError(error.message);
11+
}});
12+
13+
return (<div style={{ textAlign: "center" }}>
14+
{
15+
error && <p style={{ color: 'red' }}>{error}</p>
16+
}
17+
<p >isSupported: {location?.isSupported}</p>
18+
<p >Timestamp: {location?.position?.timestamp}</p>
19+
<p >Coords:</p>
20+
<ul>
21+
{
22+
Object.entries(location?.position?.coords ?? []).map(([key, value]) => {
23+
return <li>
24+
{key}: {value}
25+
</li>
26+
})
27+
}
28+
</ul>
29+
</div>)
30+
}
31+
```
32+
33+
> The component displays geographic location info.
34+
35+
36+
## API
37+
38+
```tsx
39+
useGeolocation * - _first element_: is the location object with two properties: __isSupported__ and __position__.
40+
```
41+
42+
> ### Params
43+
>
44+
> - __opts__: _Object_
45+
options to use geolocation.
46+
> - __opts.locationOptions?__: _PositionOptions_
47+
An optional object which provides options for retrieval of the position data.
48+
> - __opts.observe?__: _boolean_
49+
if true returns current position and observes position change, otherwise returns only position in that time.
50+
> - __opts.onError?__: _GeolocationPositionError_
51+
callback that will be executed if there will be errors.
52+
>
53+
54+
> ### Returns
55+
>
56+
> __results__: __Array__:
57+
- _GeoLocationObject|undefined_
58+
- _(successCallback: PositionCallback, errorCallback?: PositionErrorCallback|null|undefined, options?: PositionOptions|undefined) => number|void_
59+
- _()=>void_
60+
> Array with:
61+
> - _first element_: is the location object with two properties: __isSupported__ and __position__.
62+
> - _second element_: function to observe location changes and it receives one param __successCallback__ and two optional params __errorCallback__ and __options__.
63+
> - _third element_: function to cancel previous observing location changes.
64+
>

packages/react-tools/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
- [x] useIdle
9494
- [x] useFullscreen (check browser compatibility)
9595
- [x] useBattery
96-
- [ ] useGeolocation
96+
- [x] useGeolocation
9797
- [ ] useShare
9898
- [ ] useScreenShare
9999
- [ ] useFetch (with suspense ???)

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,5 @@ export { useReducedMotion } from './useReducedMotion';
6060
export { useScrollIntoView } from './useScrollIntoView';
6161
export { useMouse } from './useMouse';
6262
export { useLongPress } from './useLongPress';
63-
export { useBattery } from './useBattery';
63+
export { useBattery } from './useBattery';
64+
export { useGeolocation } from './useGeolocation';
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { useCallback, useRef } from "react"
2+
import { useSyncExternalStore } from ".";
3+
import { GeoLocationObject } from "../models";
4+
5+
/**
6+
* **`useGeolocation`**: Hook to use user's geographic location. Refer to [GeoLocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).
7+
* @param {Object} opts - options to use geolocation.
8+
* @param {PositionOptions} [opts.locationOptions] - An optional object which provides options for retrieval of the position data.
9+
* @param {boolean} [opts.mode] - it establishes how to obtain the geographic location:
10+
* - __current__: it gets the location when invoked.
11+
* - __observer__: it gets the current position every time it changes.
12+
* - __manual__: to obtain the location it need to invoke functions returned from hook.
13+
* @param {GeolocationPositionError} [opts.onError] - callback that will be executed if there will be errors.
14+
* @returns {[GeoLocationObject|undefined, (successCallback: PositionCallback, errorCallback?: PositionErrorCallback|null|undefined, options?: PositionOptions|undefined) => number|void, ()=>void]} results
15+
* Array with:
16+
* - _first element_: is the location object with two properties: __isSupported__ and __position__.
17+
* - _second element_: function to obtain manually current location.
18+
* - _third element_: function to obtain location on every changes.
19+
*/
20+
export const useGeolocation = ({mode, locationOptions, onError}: { locationOptions?: PositionOptions, mode: "observe" | "current" | "manual", onError?: (error: GeolocationPositionError) => void }): [GeoLocationObject, ()=>Promise<void>, ()=>Promise<()=>void>] => {
21+
const idWatch = useRef<number>();
22+
const cachedLocation = useRef<GeoLocationObject>({ isSupported: "geolocation" in navigator });
23+
const currentLocation = useRef<GeoLocationObject>({ isSupported: "geolocation" in navigator });
24+
const notifRef = useRef<() => void>();
25+
const successCallback = useCallback<PositionCallback>((position: GeolocationPosition) => {
26+
currentLocation.current = {position, isSupported: true};
27+
notifRef.current && notifRef.current();
28+
}, []);
29+
30+
const errorCallback = useCallback<PositionErrorCallback>((positionError: GeolocationPositionError) => {
31+
if (onError) {
32+
onError(positionError)
33+
} else {
34+
throw positionError;
35+
}
36+
}, [onError]);
37+
38+
const location = useSyncExternalStore(
39+
useCallback(notif => {
40+
if ("geolocation" in navigator) {
41+
notifRef.current = notif;
42+
mode === "observe"
43+
? (idWatch.current = navigator.geolocation.watchPosition(successCallback, errorCallback, locationOptions))
44+
: mode === "current"
45+
? navigator.geolocation.getCurrentPosition(successCallback, errorCallback, locationOptions)
46+
: notif();
47+
} else {
48+
currentLocation.current = {isSupported: false};
49+
}
50+
return () => {
51+
notifRef.current = undefined;
52+
mode === "observe" && idWatch.current && navigator.geolocation.clearWatch(idWatch.current);
53+
}
54+
}, [successCallback, errorCallback, locationOptions, mode]),
55+
useCallback(() => {
56+
if (cachedLocation.current?.isSupported !== cachedLocation.current?.isSupported || cachedLocation.current?.position?.timestamp !== currentLocation.current?.position?.timestamp || cachedLocation.current?.position?.coords.latitude !== currentLocation.current?.position?.coords.latitude || cachedLocation.current?.position?.coords.longitude !== currentLocation.current?.position?.coords.longitude) {
57+
cachedLocation.current = {...currentLocation.current};
58+
}
59+
return cachedLocation.current;
60+
}, [])
61+
);
62+
63+
const currentPosition = useCallback(() => {
64+
let resolve: () => void, reject: () => void;
65+
const promise = new Promise<void>((res, rej) => {
66+
resolve = res as ()=>void;
67+
reject = rej;
68+
})
69+
if ("geolocation" in navigator) {
70+
navigator.geolocation.getCurrentPosition(
71+
position => {
72+
successCallback(position);
73+
resolve();
74+
},
75+
error => {
76+
errorCallback(error);
77+
reject();
78+
},
79+
locationOptions
80+
);
81+
} else {
82+
return Promise.resolve();
83+
}
84+
return promise;
85+
}, [successCallback, errorCallback, locationOptions])
86+
87+
const observerPosition: () => Promise<() => void> = useCallback(() => {
88+
let watchId: number;
89+
let resolve: () => void, reject: () => void;
90+
let promise = new Promise((res, rej) => {
91+
resolve = res as () => void;
92+
reject = rej;
93+
})
94+
if ("geolocation" in navigator) {
95+
watchId = navigator.geolocation.watchPosition(
96+
position => {
97+
successCallback(position);
98+
resolve();
99+
},
100+
error => {
101+
errorCallback(error);
102+
reject();
103+
},
104+
locationOptions
105+
);
106+
} else {
107+
promise = Promise.resolve();
108+
}
109+
return promise.then(() => () => watchId && navigator.geolocation.clearWatch(watchId));
110+
}, [successCallback, errorCallback, locationOptions])
111+
112+
return [
113+
location,
114+
currentPosition,
115+
observerPosition
116+
]
117+
}

packages/react-tools/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export type {
88
TextSelection,
99
useResponsiveKeys,
1010
useResponsiveBreakpoints,
11-
ConnectionState
11+
ConnectionState,
12+
BatteryStatus,
13+
GeoLocationObject
1214
} from './models'
1315

1416
export {
@@ -74,7 +76,8 @@ export {
7476
useScrollIntoView,
7577
useMouse,
7678
useLongPress,
77-
useBattery
79+
useBattery,
80+
useGeolocation
7881
} from './hooks'
7982

8083
export {

packages/react-tools/src/models/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export type { UseScriptProps, UseScript, UseScriptStatus } from './useScript.mod
33
export type { TextSelection } from './useTextSelection.model';
44
export type { useResponsiveKeys, useResponsiveBreakpoints } from './useResponsive.model';
55
export type { ConnectionState } from './useNetwork.model';
6-
export type { BatteryStatus } from './useBattery.model';
6+
export type { BatteryStatus } from './useBattery.model';
7+
export type { GeoLocationObject } from './useGeolocation.model';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type GeoLocationObject = ({
2+
isSupported: true;
3+
position?: GeolocationPosition;
4+
}) | ({
5+
isSupported: false;
6+
position?: never;
7+
})

0 commit comments

Comments
 (0)