Skip to content

Commit f0ce3fa

Browse files
committed
[IMPL] useBattery hook
1 parent bdf299c commit f0ce3fa

15 files changed

Lines changed: 251 additions & 46 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useBattery } from "../../../../../../packages/react-tools/src"
2+
3+
/**
4+
The component displays device battery information.
5+
*/
6+
export const UseBattery = () => {
7+
const status = useBattery();
8+
9+
return (<div style={{ textAlign: "center" }}>
10+
{
11+
Object.keys(status).map(el => (
12+
<p key={el}>{el}: {status[el as keyof typeof status]}</p>
13+
))
14+
}
15+
</div>)
16+
}

apps/react-tools-demo/src/components/hooks/useRaf/UseRaf.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ The component renders a textarea element and when it is resized, updates __state
66
*/
77
export const UseRaf = () => {
88
const [state, setState] = useState({ width: 0, height: 0 });
9-
const [start] = useRaf((timer: number, dim: DOMRectReadOnly) => {
10-
setState({width:dim.width, height: dim.height})
9+
const [start] = useRaf((timer: number, repeat:()=>void, dim: DOMRectReadOnly) => {
10+
setState({ width: dim.width, height: dim.height });
1111
});
1212
const [refCb] = useResizeObserver<HTMLTextAreaElement>(
1313
(entries: ResizeObserverEntry[]) => {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export const COMPONENTS = [
3737
"useMemoDeepCompare",
3838
"useCallbackCompare",
3939
"useCallbackDeepCompare",
40-
"useRaf",
4140
"useLazyRef",
4241
"useId"
4342
],
@@ -71,14 +70,16 @@ export const COMPONENTS = [
7170
"useActiveElement",
7271
"useTimeout",
7372
"useInterval",
73+
"useRaf",
7474
"useTextSelection",
7575
"useClipboard",
7676
"useMediaQuery",
7777
"useColorScheme",
7878
"useReducedMotion",
7979
"useTitle",
8080
"useIdle",
81-
"useFullscreen"
81+
"useFullscreen",
82+
"useBattery"
8283
]
8384
],
8485
//UTILS
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# useBattery
2+
Hook for accessing and monitoring device battery status. Refer to [Battery Status API](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API)
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseBattery = () => {
8+
const status = useBattery();
9+
10+
return (<div style={{ textAlign: "center" }}>
11+
{
12+
Object.keys(status).map(el => (
13+
<p key={el}>{el}: {JSON.stringify(status[el as keyof typeof status])}</p>
14+
))
15+
}
16+
</div>)
17+
}
18+
```
19+
20+
> The component displays device battery information.
21+
22+
23+
## API
24+
25+
```tsx
26+
useBattery (opts?: { onChargingChange?: (evt: Event) => void, onChargingTimeChange?: (evt: Event) => void, onDischargingTimeChange?: (evt: Event) => void, onLevelChange?: (evt: Event) => void }): BatteryStatus
27+
```
28+
29+
> ### Params
30+
>
31+
> - __opts?__: _Object_
32+
optional object parameter to listen battery events change.
33+
> - __opts.onChargingChange?__: _(evt: Event) => void_
34+
callback that will be executed when chargingchange event is fired.
35+
> - __opts.onChargingTimeChange?__: _(evt: Event) => void_
36+
callback that will be executed when chargingtimechange event is fired.
37+
> - __opts.onDischargingTimeChange?__: _(evt: Event) => void_
38+
callback that will be executed when dischargingtimechange event is fired.
39+
> - __opts.onLevelChange?__: _(evt: Event) => void_
40+
callback that will be executed when levelchange event is fired.
41+
>
42+
43+
> ### Returns
44+
>
45+
> __result__: object:
46+
> - _BatteryStatus_
47+
>

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Hook to execute a callback function with _requestAnimationFrame_ to optimize per
66
```tsx
77
export const UseRaf = () => {
88
const [state, setState] = useState({ width: 0, height: 0 });
9-
const [start] = useRaf((timer: number, dim: DOMRectReadOnly) => {
10-
setState({width:dim.width, height: dim.height})
9+
const [start] = useRaf((timer: number, repeat:()=>void, dim: DOMRectReadOnly) => {
10+
setState({ width: dim.width, height: dim.height });
1111
});
1212
const [refCb] = useResizeObserver<HTMLTextAreaElement>(
1313
(entries: ResizeObserverEntry[]) => {
@@ -28,13 +28,13 @@ export const UseRaf = () => {
2828
## API
2929

3030
```tsx
31-
useRaf <T extends unknown[]>(cb: (timer:number, ...args: T) => void): [(...args: T)=>void, ()=>void]
31+
useRaf <T extends unknown[]>(cb: (timer: number, repeat: ()=>void, ...args: T) => void): [(...args: T)=>void, ()=>void]
3232
```
3333
3434
> ### Params
3535
>
36-
> - __cb__: _(timer:number, ...args: T) => void_
37-
callback to execute prior to the next repaint.
36+
> - __cb__: _(timer:number, ()=>void, ...args: T) => void_
37+
callback to execute prior to the next repaint. In addition to the classic timeStamp parameter, which indicates the end time of rendering of the previous frame, the second parameter is a function which, if invoked, re-executes the requestAnimationFrame with the callback itself, and finally various parameters can be added, passed with the invocation function returned by the hook.
3838
>
3939
4040
> ### Returns

packages/react-tools/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
- [x] useMemoizedFunction
4141
- [x] useMemoCompare
4242
- [x] useMemoDeepCompare
43-
- [x] useRaf
4443
- [ ] useMergedRef
4544
- [ ] useObjectRef
4645
- [ ] useArrayRef
@@ -82,7 +81,7 @@
8281
- [x] useDebounce
8382
- [x] useThrottle
8483
- [x] useActiveElement
85-
- [ ] useRaf (with a reference to call itself again)
84+
- [x] useRaf (with a reference to call itself again)
8685
- [x] useTimeout
8786
- [x] useInterval
8887
- [x] useTextSelection
@@ -93,7 +92,7 @@
9392
- [x] useTitle (change document.title but also document.head.title nodeElement)
9493
- [x] useIdle
9594
- [x] useFullscreen (check browser compatibility)
96-
- [ ] useBattery
95+
- [x] useBattery
9796
- [ ] useGeolocation
9897
- [ ] useShare
9998
- [ ] useScreenShare
@@ -110,6 +109,7 @@
110109
- [ ] useDeviceMotion (??? mobile)
111110
- [ ] useVibrate (??? mobile)
112111
- [ ] useObservable — tracks latest value of an Observable
112+
- [ ] useLock - (https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request)
113113

114114
- __UTILS__
115115
- [x] isShallowEqual

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ export { useVisible } from './useVisible';
5959
export { useReducedMotion } from './useReducedMotion';
6060
export { useScrollIntoView } from './useScrollIntoView';
6161
export { useMouse } from './useMouse';
62-
export { useLongPress } from './useLongPress';
62+
export { useLongPress } from './useLongPress';
63+
export { useBattery } from './useBattery';
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { useCallback, useMemo, useRef } from "react";
2+
import { useSyncExternalStore } from "."
3+
import { BatteryStatus } from "../models";
4+
5+
const listeners = new Set<() => void>();
6+
const eventsListeners = {
7+
onCharging: new Set<(evt: Event) => void>(),
8+
onChargingTime: new Set<(evt: Event) => void>(),
9+
onDischargingTime: new Set<(evt: Event) => void>(),
10+
onLevel: new Set<(evt: Event) => void>()
11+
};
12+
let batteryCached: undefined | BatteryStatus & { onchargingchange: null | ((evt: Event) => void), onlevelchange: null | ((evt: Event) => void), onchargingtimechange: null | ((evt: Event) => void), ondischargingtimechange: null | ((evt: Event) => void) };
13+
14+
/**
15+
* **`useBattery`**: Hook for accessing and monitoring device battery status. Refer to [Battery Status API](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API)
16+
* @param {Object} [opts] - optional object parameter to listen battery events change.
17+
* @param {(evt: Event) => void} [opts.onChargingChange] - callback that will be executed when chargingchange event is fired.
18+
* @param {(evt: Event) => void} [opts.onChargingTimeChange] - callback that will be executed when chargingtimechange event is fired.
19+
* @param {(evt: Event) => void} [opts.onDischargingTimeChange] - callback that will be executed when dischargingtimechange event is fired.
20+
* @param {(evt: Event) => void} [opts.onLevelChange] - callback that will be executed when levelchange event is fired.
21+
* @returns {BatteryStatus} result - object:
22+
* - __isSupported__: boolean that indicates if Battery Status API is available.
23+
* - __level__: number that indicates battery level: is a number between 0 and 1.
24+
* - __charging__: boolean that indicates if battery is charging.
25+
* - __chargingTime__: number that indicates time in seconds remaining to full charge, or infinity.
26+
* - __dischargingTime__: number that indicates time in seconds remaining to empty charge, rounded in 15 minutes by API.
27+
*/
28+
export const useBattery = (opts?: { onChargingChange?: (evt: Event) => void, onChargingTimeChange?: (evt: Event) => void, onDischargingTimeChange?: (evt: Event) => void, onLevelChange?: (evt: Event) => void }): BatteryStatus => {
29+
const status = useRef<BatteryStatus>({
30+
isSupported: "getBattery" in navigator,
31+
charging: false,
32+
chargingTime: 0,
33+
dischargingTime: 0,
34+
level: 0
35+
});
36+
37+
return useSyncExternalStore(
38+
useCallback(notif => {
39+
listeners.add(notif);
40+
listeners.size === 1 && "getBattery" in navigator && ((navigator.getBattery as () => Promise<BatteryStatus & { [k in "onchargingchange" | "onlevelchange" | "onchargingtimechange" | "ondischargingtimechange" ]: (evt:Event)=>void}>)())
41+
.then((battery: Exclude<typeof batteryCached, undefined>) => {
42+
batteryCached = battery;
43+
status.current = {
44+
isSupported: true,
45+
level: battery.level,
46+
charging: battery.charging,
47+
chargingTime: battery.chargingTime,
48+
dischargingTime: battery.dischargingTime
49+
}
50+
listeners.forEach(l => l());
51+
opts?.onChargingChange && eventsListeners.onCharging.add(opts.onChargingChange);
52+
opts?.onChargingTimeChange && eventsListeners.onChargingTime.add(opts.onChargingTimeChange);
53+
opts?.onDischargingTimeChange && eventsListeners.onDischargingTime.add(opts.onDischargingTimeChange);
54+
opts?.onLevelChange && eventsListeners.onLevel.add(opts.onLevelChange);
55+
battery.onchargingchange = (evt) => {
56+
status.current = {
57+
isSupported: true,
58+
level: battery.level,
59+
charging: battery.charging,
60+
chargingTime: battery.chargingTime,
61+
dischargingTime: battery.dischargingTime
62+
}
63+
eventsListeners.onCharging.forEach(l => l(evt));
64+
listeners.forEach(l => l());
65+
};
66+
battery.onchargingtimechange = (evt) => {
67+
status.current = {
68+
isSupported: true,
69+
level: battery.level,
70+
charging: battery.charging,
71+
chargingTime: battery.chargingTime,
72+
dischargingTime: battery.dischargingTime
73+
}
74+
eventsListeners.onChargingTime.forEach(l => l(evt));
75+
listeners.forEach(l => l());
76+
}
77+
battery.ondischargingtimechange = (evt) => {
78+
status.current = {
79+
isSupported: true,
80+
level: battery.level,
81+
charging: battery.charging,
82+
chargingTime: battery.chargingTime,
83+
dischargingTime: battery.dischargingTime
84+
}
85+
eventsListeners.onDischargingTime.forEach(l => l(evt));
86+
listeners.forEach(l => l());
87+
}
88+
battery.onlevelchange = (evt) => {
89+
status.current = {
90+
isSupported: true,
91+
level: battery.level,
92+
charging: battery.charging,
93+
chargingTime: battery.chargingTime,
94+
dischargingTime: battery.dischargingTime
95+
}
96+
eventsListeners.onLevel.forEach(l => l(evt));
97+
listeners.forEach(l => l());
98+
}
99+
})
100+
return () => {
101+
opts?.onChargingChange && eventsListeners.onCharging.delete(opts.onChargingChange);
102+
opts?.onChargingTimeChange && eventsListeners.onChargingTime.delete(opts.onChargingTimeChange);
103+
opts?.onDischargingTimeChange && eventsListeners.onDischargingTime.delete(opts.onDischargingTimeChange);
104+
opts?.onLevelChange && eventsListeners.onLevel.delete(opts.onLevelChange);
105+
listeners.delete(notif);
106+
if (listeners.size === 0) {
107+
!!batteryCached && (batteryCached.onchargingchange = null);
108+
!!batteryCached && (batteryCached.onchargingtimechange = null);
109+
!!batteryCached && (batteryCached.ondischargingtimechange = null);
110+
!!batteryCached && (batteryCached.onlevelchange = null);
111+
batteryCached = undefined;
112+
}
113+
}
114+
}, [opts?.onChargingChange, opts?.onChargingTimeChange, opts?.onDischargingTimeChange, opts?.onLevelChange]),
115+
useMemo(() => {
116+
let cachedStatus: BatteryStatus = { ...status.current };
117+
return () => {
118+
if (status.current.isSupported !== cachedStatus.isSupported || status.current.level !== cachedStatus.level || status.current.charging !== cachedStatus.charging || status.current.chargingTime !== cachedStatus.chargingTime || status.current.dischargingTime !== cachedStatus.dischargingTime) {
119+
cachedStatus = status.current;
120+
}
121+
return cachedStatus;
122+
}
123+
}, [])
124+
);
125+
}

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { RefCallback, useCallback, useRef } from "react"
2+
import { useEffectOnce } from ".";
23

34
/**
45
* **`useIntersectionObserver`**: Hook to use Intersection Observer. Refer to [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
@@ -11,20 +12,19 @@ export const useIntersectionObserver = <T extends Element>(cb: IntersectionObser
1112
const working = useRef(true);
1213
const nodeRef = useRef<T>();
1314

15+
useEffectOnce(() => () => {
16+
nodeRef.current = undefined;
17+
observer.current?.disconnect();
18+
observer.current = undefined;
19+
});
20+
1421
return [
1522
useCallback((node: T) => {
16-
nodeRef.current = node;
17-
if (!working.current) {
23+
if (!working.current || !node) {
1824
return;
1925
}
20-
if (!node) {
21-
if (observer.current) {
22-
observer.current.disconnect();
23-
observer.current = undefined;
24-
nodeRef.current = undefined;
25-
working.current = true;
26-
}
27-
} else {
26+
if (node && (!nodeRef.current || nodeRef.current !== node)) {
27+
nodeRef.current = node;
2828
observer.current = new IntersectionObserver(cb, opts);
2929
observer.current.observe(node);
3030
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import { useCallback, useRef } from "react"
22

33
/**
44
* **`useRaf`**: Hook to execute a callback function with _requestAnimationFrame_ to optimize performance. Refer to (requestAnimationFrame)[https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame].
5-
* @param {(timer:number, ...args: T) => void} cb - callback to execute prior to the next repaint.
5+
* @param {(timer:number, ()=>void, ...args: T) => void} cb - callback to execute prior to the next repaint. In addition to the classic timeStamp parameter, which indicates the end time of rendering of the previous frame, the second parameter is a function which, if invoked, re-executes the requestAnimationFrame with the callback itself, and finally various parameters can be added, passed with the invocation function returned by the hook.
66
* @returns {[(...args: T)=>void, ()=>void]} results - array with __start__ function to invoke _requestAnimationFrame_ and __cancel__ function to invoke _cancelAnimationFrame_.
77
*/
8-
export const useRaf = <T extends unknown[]>(cb: (timer:number, ...args: T) => void): [(...args: T)=>void, ()=>void] => {
8+
export const useRaf = <T extends unknown[]>(cb: (timer: number, repeat: ()=>void, ...args: T) => void): [(...args: T)=>void, ()=>void] => {
99
const idRequest = useRef<number>();
1010

1111
return [
1212
useCallback((...args: T) => {
13-
idRequest.current = requestAnimationFrame((timer) => cb(timer, ...args));
13+
const repeat = () => {
14+
idRequest.current = requestAnimationFrame((timer) => cb(timer, repeat, ...args));
15+
}
16+
idRequest.current = requestAnimationFrame((timer) => cb(timer, repeat, ...args));
1417
}, [cb]),
1518
useCallback(() => {
1619
!!idRequest.current && cancelAnimationFrame(idRequest.current)

0 commit comments

Comments
 (0)