Skip to content

Commit 40cfa29

Browse files
committed
[IMPL] useBluetooth hook
1 parent 73f4331 commit 40cfa29

14 files changed

Lines changed: 400 additions & 44 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useCallback, useState } from "react";
2+
import { useBluetooth } from "../../../../../../packages/react-tools/src"
3+
4+
/**
5+
The component uses _useBluetooth_ hook to detect if Bluetooth API is supported and show all available bluetooth devices and show renders its name after connection.
6+
*/
7+
export const UseBluetooth = () => {
8+
const [value, requestDevice] = useBluetooth();
9+
10+
const [error, setError] = useState("");
11+
12+
const getDevice = useCallback(() => {
13+
requestDevice().catch(err => {
14+
if (err instanceof Error) {
15+
setError(err.message);
16+
} else {
17+
setError(err as string);
18+
}
19+
})
20+
}, [requestDevice]);
21+
22+
return <>
23+
<p>
24+
Bluetooth supported: {value.isSupported ? "Yes" : "No"}
25+
</p>
26+
{
27+
value.isConnected &&
28+
<p>
29+
Device Name: {value.device?.name}
30+
</p>
31+
}
32+
<button type="button" onClick={getDevice} disabled={!value.isSupported}>Get Bluetooth device</button>
33+
{
34+
error &&
35+
<p style={{color: 'red'}}>
36+
{error}
37+
</p>
38+
}
39+
</>
40+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ export const COMPONENTS = [
9595
"useDialogBox",
9696
"useDeviceMotion",
9797
"useDeviceOrientation",
98-
"useVibrate"
98+
"useVibrate",
99+
"useBluetooth"
99100
]
100101
],
101102
//UTILS
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# useBluetooth
2+
Hook to use [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API).
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseBluetooth = () => {
8+
const [value, requestDevice] = useBluetooth();
9+
10+
const [error, setError] = useState("");
11+
12+
const getDevice = useCallback(() => {
13+
requestDevice().catch(err => {
14+
if (err instanceof Error) {
15+
setError(err.message);
16+
} else {
17+
setError(err as string);
18+
}
19+
})
20+
}, [requestDevice]);
21+
22+
return <>
23+
<p>
24+
Bluetooth supported: {value.isSupported ? "Yes" : "No"}
25+
</p>
26+
{
27+
value.isConnected &&
28+
<p>
29+
Device Name: {value.device?.name}
30+
</p>
31+
}
32+
<button type="button" onClick={getDevice} disabled={!value.isSupported}>Get Bluetooth device</button>
33+
{
34+
error &&
35+
<p style={{color: 'red'}}>
36+
{error}
37+
</p>
38+
}
39+
</>
40+
}
41+
```
42+
43+
> The component uses _useBluetooth_ hook to detect if Bluetooth API is supported and show all available bluetooth devices and show renders its name after connection.
44+
45+
46+
## API
47+
48+
```tsx
49+
useBluetooth():[{isSupported: boolean, isConnected: boolean, device: BluetoothDevice|null, server: BluetoothRemoteGATTServer|null}, (opts?: BluetoothDevicesOptions)=>Promise<void>]
50+
```
51+
52+
> ### Params
53+
>
54+
>
55+
>
56+
57+
> ### Returns
58+
>
59+
> __result__
60+
> - __Array__:
61+
> - __Object__:
62+
> - __isSupported__ : _boolean_
63+
> - __isConnected__ : _boolean_
64+
> - __device__ : _BluetoothDevice|null_
65+
> - __server__ : _BluetoothRemoteGATTServer|null_
66+
> - _(opts?: BluetoothDevicesOptions)=>Promise<void>_
67+
>

apps/react-tools-demo/src/markdown/useDerivedState.md

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@ Hook useful when the internal state of a component depends on one or more props.
44
## Usage
55

66
```tsx
7+
export const UseDerivedState = () => {
8+
const [state, setState] = useState("");
9+
const [state1, setState1] = useState("");
10+
const [state2, setState2] = useState("");
11+
12+
return <div style={{ display: "grid", gridTemplateColumns: "auto auto auto", justifyContent: "center", gap: 50, maxHeight: 350, overflow: "auto" }}>
13+
<div>
14+
<p>Without useDerivedState</p>
15+
<input type="text" placeholder="User.." value={state1} onChange={(e) => setState1(e.target.value)} />
16+
<WithoutUseDerivedState user={state1} />
17+
</div>
18+
<div>
19+
<p>With useDerivedState</p>
20+
<input type="text" placeholder="User.." value={state} onChange={(e) => setState(e.target.value)} />
21+
<WithUseDerivedState user={state} />
22+
</div>
23+
<div>
24+
<p>With useDerivedState and compute</p>
25+
<input type="text" placeholder="User.." value={state2} onChange={(e) => setState2(e.target.value)} />
26+
<WithUseDerivedStateAndCompute user={state2} />
27+
</div>
28+
</div>
29+
}
30+
731
const WithoutUseDerivedState = memo(({user}:{user:string}) => {
832
renders[1]++;
933
const [state, setState] = useState<{ loading: boolean, friends: string[] }>({ loading: true, friends: [] });
@@ -82,30 +106,6 @@ const WithUseDerivedState = memo(({ user }: { user: string }) => {
82106
}
83107
</>
84108
})
85-
86-
export const UseDerivedState = () => {
87-
const [state, setState] = useState("");
88-
const [state1, setState1] = useState("");
89-
const [state2, setState2] = useState("");
90-
91-
return <div style={{ display: "grid", gridTemplateColumns: "auto auto auto", justifyContent: "center", gap: 50 }}>
92-
<div>
93-
<p>Without useDerivedState</p>
94-
<input type="text" placeholder="User.." value={state1} onChange={(e) => setState1(e.target.value)} />
95-
<WithoutUseDerivedState user={state1} />
96-
</div>
97-
<div>
98-
<p>With useDerivedState</p>
99-
<input type="text" placeholder="User.." value={state} onChange={(e) => setState(e.target.value)} />
100-
<WithUseDerivedState user={state}/>
101-
</div>
102-
<div>
103-
<p>With useDerivedState and compute</p>
104-
<input type="text" placeholder="User.." value={state2} onChange={(e) => setState2(e.target.value)} />
105-
<WithUseDerivedStateAndCompute user={state2} />
106-
</div>
107-
</div>
108-
}
109109
```
110110

111111
> The component has _three internal string states_ and renders three input fields and three components that receive one state each. These three components have an object as internal state with two properties _loading_, initially set to __true__, and _friends_ which is an initially empty array.

packages/react-tools/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
- [x] useDeviceMotion
9797
- [x] useDeviceOrientation
9898
- [x] useVibrate (??? mobile)
99-
- [ ] useBluetooth (https://vueuse.org/core/useBluetooth/)
99+
- [x] useBluetooth
100100
- [ ] useFileSystem (https://vueuse.org/core/useFileSystemAccess/).
101101
- [ ] useGamePad (https://vueuse.org/core/useGamepad/)
102102
- [ ] useWakeLock (https://vueuse.org/core/useWakeLock/)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,5 @@ export { useLogger } from './useLogger';
7575
export { useDeviceMotion } from './useDeviceMotion';
7676
export { useDeviceOrientation } from './useDeviceOrientation';
7777
export { useVibrate } from './useVibrate';
78-
export { useDerivedState } from './useDerivedState';
78+
export { useDerivedState } from './useDerivedState';
79+
export { useBluetooth } from './useBluetooth';

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ let batteryCached: undefined | BatteryStatus & { onchargingchange: null | ((evt:
2828
*/
2929
export const useBattery = (opts?: { onChargingChange?: (evt: Event) => void, onChargingTimeChange?: (evt: Event) => void, onDischargingTimeChange?: (evt: Event) => void, onLevelChange?: (evt: Event) => void }): BatteryStatus => {
3030
const status = useRef<BatteryStatus>({
31-
isSupported: "getBattery" in navigator,
31+
isSupported: !!navigator && "getBattery" in navigator,
3232
charging: false,
3333
chargingTime: 0,
3434
dischargingTime: 0,
@@ -38,7 +38,7 @@ export const useBattery = (opts?: { onChargingChange?: (evt: Event) => void, onC
3838
return useSyncExternalStore(
3939
useCallback(notif => {
4040
listeners.add(notif);
41-
listeners.size === 1 && "getBattery" in navigator && ((navigator.getBattery as () => Promise<BatteryStatus & { [k in "onchargingchange" | "onlevelchange" | "onchargingtimechange" | "ondischargingtimechange" ]: (evt:Event)=>void}>)())
41+
listeners.size === 1 && !!navigator && "getBattery" in navigator && ((navigator.getBattery as () => Promise<BatteryStatus & { [k in "onchargingchange" | "onlevelchange" | "onchargingtimechange" | "ondischargingtimechange" ]: (evt:Event)=>void}>)())
4242
.then((battery: Exclude<typeof batteryCached, undefined>) => {
4343
batteryCached = battery;
4444
status.current = {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useCallback, useRef } from "react";
2+
import { Bluetooth, BluetoothDevice, BluetoothDevicesOptions, BluetoothRemoteGATTServer } from "../models";
3+
import { useSyncExternalStore } from ".";
4+
5+
/**
6+
* **`useBluetooth`**: Hook to use [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API).
7+
* @returns {[{isSupported: boolean, isConnected: boolean, device: BluetoothDevice|null, server: BluetoothRemoteGATTServer|null}, (opts?: BluetoothDevicesOptions)=>Promise<void>]} result
8+
*/
9+
export const useBluetooth = ():[{isSupported: boolean, isConnected: boolean, device: BluetoothDevice|null, server: BluetoothRemoteGATTServer|null}, (opts?: BluetoothDevicesOptions)=>Promise<void>] => {
10+
const notifRef = useRef<() => void>();
11+
const cachedState = useRef<{ isSupported: boolean, isConnected: boolean, device: BluetoothDevice|null, server: BluetoothRemoteGATTServer|null }>({
12+
isSupported: !!navigator && "bluetooth" in navigator,
13+
isConnected: false,
14+
device: null,
15+
server: null
16+
});
17+
const currentState = useRef<{ device?: BluetoothDevice, server?: BluetoothRemoteGATTServer }>({
18+
device: undefined,
19+
server: undefined
20+
});
21+
22+
const connectToServerGatt = useRef(async () => {
23+
if (currentState.current.device && currentState.current.device.gatt) {
24+
currentState.current.server = await currentState.current.device.gatt.connect();
25+
}
26+
});
27+
28+
const requestDevice = useCallback(async (opts?: BluetoothDevicesOptions) => {
29+
try {
30+
if (!!navigator && 'bluetooth' in navigator) {
31+
currentState.current.device = await (navigator.bluetooth as Bluetooth).requestDevice({
32+
...(opts ?? {}),
33+
...((!opts || !opts.filters || opts.filters.length === 0) && { acceptAllDevices: true })
34+
}) as BluetoothDevice;
35+
currentState.current.device && await connectToServerGatt.current();
36+
}
37+
} finally {
38+
notifRef.current && notifRef.current();
39+
}
40+
}, []);
41+
42+
const value = useSyncExternalStore(
43+
useCallback(notif => {
44+
notifRef.current = notif;
45+
return () => {
46+
notifRef.current = undefined;
47+
}
48+
}, []),
49+
useCallback(() => {
50+
const isSupportedCurrent = !!navigator && 'bluetooth' in navigator;
51+
const isConnectedCurrent = currentState.current.server?.connected ?? false;
52+
if (cachedState.current.isConnected !== isConnectedCurrent || cachedState.current.isSupported !== isSupportedCurrent || cachedState.current.device !== currentState.current.device || cachedState.current.server !== currentState.current.server) {
53+
cachedState.current = {
54+
isSupported: isSupportedCurrent,
55+
isConnected: isConnectedCurrent,
56+
device: currentState.current.device!,
57+
server: currentState.current.server!
58+
};
59+
}
60+
return cachedState.current;
61+
}, [])
62+
);
63+
64+
return [value, requestDevice];
65+
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { GeoLocationObject } from "../models";
1919
*/
2020
export const useGeolocation = ({mode, locationOptions, onError}: { locationOptions?: PositionOptions, mode: "observe" | "current" | "manual", onError?: (error: GeolocationPositionError) => void }): [GeoLocationObject, ()=>Promise<void>, ()=>Promise<()=>void>] => {
2121
const idWatch = useRef<number>();
22-
const cachedLocation = useRef<GeoLocationObject>({ isSupported: "geolocation" in navigator });
23-
const currentLocation = useRef<GeoLocationObject>({ isSupported: "geolocation" in navigator });
22+
const cachedLocation = useRef<GeoLocationObject>({ isSupported: !!navigator && "geolocation" in navigator });
23+
const currentLocation = useRef<GeoLocationObject>({ isSupported: !!navigator && "geolocation" in navigator });
2424
const notifRef = useRef<() => void>();
2525
const successCallback = useCallback<PositionCallback>((position: GeolocationPosition) => {
2626
currentLocation.current = {position, isSupported: true};
@@ -37,7 +37,7 @@ export const useGeolocation = ({mode, locationOptions, onError}: { locationOptio
3737

3838
const location = useSyncExternalStore(
3939
useCallback(notif => {
40-
if ("geolocation" in navigator) {
40+
if (!!navigator && "geolocation" in navigator) {
4141
notifRef.current = notif;
4242
mode === "observe"
4343
? (idWatch.current = navigator.geolocation.watchPosition(successCallback, errorCallback, locationOptions))
@@ -66,7 +66,7 @@ export const useGeolocation = ({mode, locationOptions, onError}: { locationOptio
6666
resolve = res as ()=>void;
6767
reject = rej;
6868
})
69-
if ("geolocation" in navigator) {
69+
if (!!navigator && "geolocation" in navigator) {
7070
navigator.geolocation.getCurrentPosition(
7171
position => {
7272
successCallback(position);
@@ -91,7 +91,7 @@ export const useGeolocation = ({mode, locationOptions, onError}: { locationOptio
9191
resolve = res as () => void;
9292
reject = rej;
9393
})
94-
if ("geolocation" in navigator) {
94+
if (!!navigator && "geolocation" in navigator) {
9595
watchId = navigator.geolocation.watchPosition(
9696
position => {
9797
successCallback(position);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { useCallback } from "react"
55
* @returns {{isSupported: boolean, share: (data?: ShareData) => Promise<void>}} object - __isSupported__ to known if share API is supported and __share__ function to use Web share API.
66
*/
77
export const useShare = (): {isSupported: boolean, share: (data?: ShareData)=>Promise<void>} => {
8-
const isSupported = "share" in navigator;
8+
const isSupported = !!navigator && "share" in navigator;
99

1010
const share = useCallback((data?: ShareData) => {
11-
if ("share" in navigator) {
11+
if (!!navigator && "share" in navigator) {
1212
return navigator.share(data);
1313
} else {
1414
return Promise.resolve();

0 commit comments

Comments
 (0)