From 310fb6dc55cab56b93a6d112c4ec52bf74804e7a Mon Sep 17 00:00:00 2001 From: xobotyi Date: Mon, 19 Aug 2019 11:09:59 +0300 Subject: [PATCH 1/5] Reworked useBattery hook; Fix: #308; --- docs/useBattery.md | 33 ++++++- src/__stories__/useBattery.story.tsx | 24 ++++- src/useBattery.ts | 142 ++++++++++++++++++--------- 3 files changed, 147 insertions(+), 52 deletions(-) diff --git a/docs/useBattery.md b/docs/useBattery.md index 4ddbcc1e..69f0403c 100644 --- a/docs/useBattery.md +++ b/docs/useBattery.md @@ -2,6 +2,9 @@ React sensor hook that tracks battery status. +>**Note:** current `BatteryManager` API state is obsolete. +>Although it may still work in some browsers, its use is discouraged since it could be removed at any time. + ## Usage @@ -9,12 +12,34 @@ React sensor hook that tracks battery status. import {useBattery} from 'react-use'; const Demo = () => { - const state = useBattery(); + const batteryState = useBattery(); + if (!batteryState.isSupported) { + return ( +
+ Battery sensor:
not supported
+
+ ); + } return ( -
-      {JSON.stringify(state, null, 2)}
-    
+
+ Battery sensor:  supported
+ Charge level:  {(batteryState.level * 100).toFixed(0)}%
+ Charging:  {batteryState.charging ? 'yes' : 'no'}
+ Charging time:  {batteryState.chargingTime ? batteryState.chargingTime : 'finished'}
+ Discharging time:  {batteryState.dischargingTime} +
); }; ``` + +## Reference + +```ts +const {isSupported, level, charging, dischargingTime, chargingTime} = useBattery(); +``` +- **`isSupported`**_`: boolean`_ - wheter browser/devise supports BatteryManager; +- **`level`**_`: number`_ - representing the system's battery charge level scaled to a value between 0.0 and 1.0. +- **`charging`**_`: boolean`_ - indicating whether or not the battery is currently being charged. +- **`dischargingTime`**_`: number`_ - remaining time in seconds until the battery is completely discharged and the system will suspend. +- **`chargingTime`**_`: number`_ - remaining time in seconds until the battery is fully charged, or 0 if the battery is already fully charged. diff --git a/src/__stories__/useBattery.story.tsx b/src/__stories__/useBattery.story.tsx index 5c6ae635..922b1442 100644 --- a/src/__stories__/useBattery.story.tsx +++ b/src/__stories__/useBattery.story.tsx @@ -4,9 +4,29 @@ import { useBattery } from '..'; import ShowDocs from './util/ShowDocs'; const Demo = () => { - const state = useBattery(); + const batteryState = useBattery(); - return
{JSON.stringify(state, null, 2)}
; + if (!batteryState.isSupported) { + return ( +
+ Battery sensor:
not supported
+
+ ); + } + return ( +
+ Battery sensor:  supported +
+ Charge level:  {(batteryState.level * 100).toFixed(0)}% +
+ Charging:  {batteryState.charging ? 'yes' : 'no'} +
+ Charging time:   + {batteryState.chargingTime ? batteryState.chargingTime : 'finished'} +
+ Discharging time:  {batteryState.dischargingTime} +
+ ); }; storiesOf('Sensors|useBattery', module) diff --git a/src/useBattery.ts b/src/useBattery.ts index a5cfd5c2..fdba50ca 100644 --- a/src/useBattery.ts +++ b/src/useBattery.ts @@ -1,56 +1,106 @@ -import { useEffect, useState } from 'react'; +import * as React from 'react'; import { off, on } from './util'; -export interface BatterySensorState { - charging: boolean; - level: number; - chargingTime: number; - dischargingTime: number; +enum BatteryManagerEvents { + levelChange = 'levelchange', + dischargingTimeChange = 'dischargingtimechange', + chargingTimeChange = 'chargingtimechange', + chargingChange = 'chargingchange', } -const useBattery = () => { - const [state, setState] = useState({}); - let mounted = true; - let battery: any = null; +export interface BatteryState { + charging: boolean; + chargingTime: number; + dischargingTime: number; + level: number; +} - const onChange = () => { - const { charging, level, chargingTime, dischargingTime } = battery; - setState({ - charging, - level, - chargingTime, - dischargingTime, - }); - }; +interface BatteryManager extends Readonly, EventTarget { + onchargingchange: () => void; + onchargingtimechange: () => void; + ondischargingtimechange: () => void; + onlevelchange: () => void; +} - const onBattery = () => { - onChange(); - on(battery, 'chargingchange', onChange); - on(battery, 'levelchange', onChange); - on(battery, 'chargingtimechange', onChange); - on(battery, 'dischargingtimechange', onChange); - }; +interface NavigatorWithPossibleBattery extends Navigator { + getBattery?: () => Promise; +} - useEffect(() => { - (navigator as any).getBattery().then((bat: any) => { - if (mounted) { - battery = bat; - onBattery(); - } - }); +const nav: NavigatorWithPossibleBattery = navigator; - return () => { - mounted = false; - if (battery) { - off(battery, 'chargingchange', onChange); - off(battery, 'levelchange', onChange); - off(battery, 'chargingtimechange', onChange); - off(battery, 'dischargingtimechange', onChange); - } - }; - }, []); - - return state; +type UseBatteryState = BatteryState & { + isSupported: boolean; }; -export default useBattery; +export default function useBattery() { + const [state, setState] = React.useState({ + isSupported: nav && typeof nav.getBattery !== 'undefined', + level: 1, + charging: true, + dischargingTime: Infinity, + chargingTime: 0, + }); + const battery = React.useRef(); + let isMounted = true; + + if (state.isSupported) { + const onChange = React.useCallback(() => { + if (isMounted && battery.current) { + setState({ + isSupported: true, + level: battery.current.level, + charging: battery.current.charging, + dischargingTime: battery.current.dischargingTime, + chargingTime: battery.current.chargingTime, + }); + } + }, [setState]); + + const bindBatteryEvents = React.useCallback( + (bat: BatteryManager) => { + on(bat, BatteryManagerEvents.chargingChange, onChange); + on(bat, BatteryManagerEvents.chargingTimeChange, onChange); + on(bat, BatteryManagerEvents.dischargingTimeChange, onChange); + on(bat, BatteryManagerEvents.levelChange, onChange); + }, + [onChange] + ); + + const unbindBatteryEvents = React.useCallback( + (bat: BatteryManager) => { + off(bat, BatteryManagerEvents.chargingChange, onChange); + off(bat, BatteryManagerEvents.chargingTimeChange, onChange); + off(bat, BatteryManagerEvents.dischargingTimeChange, onChange); + off(bat, BatteryManagerEvents.levelChange, onChange); + }, + [onChange] + ); + + React.useEffect(() => { + battery.current && bindBatteryEvents(battery.current); + + return () => { + battery.current && unbindBatteryEvents(battery.current); + }; + }, [onChange]); + + React.useEffect(() => { + nav.getBattery!().then((bat: BatteryManager) => { + if (!isMounted) { + return; + } + + battery.current = bat; + bindBatteryEvents(bat); + }); + + return () => { + isMounted = false; + + battery.current && unbindBatteryEvents(battery.current); + }; + }, []); + } + + return state; +} From 0e9894d28e014c3fb0e2bf87ef664204e6021db9 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Tue, 20 Aug 2019 00:47:06 +0300 Subject: [PATCH 2/5] Rework vol.2 =) Added fetching indicator; Fixed isMounted state; Mock function returned for browsers not supporting the Battery API; Using react-fast-compare to avoid useless updates; --- docs/useBattery.md | 27 ++++-- src/__stories__/useBattery.story.tsx | 27 ++++-- src/useBattery.ts | 138 +++++++++++++-------------- 3 files changed, 105 insertions(+), 87 deletions(-) diff --git a/docs/useBattery.md b/docs/useBattery.md index 69f0403c..32498d86 100644 --- a/docs/useBattery.md +++ b/docs/useBattery.md @@ -17,17 +17,29 @@ const Demo = () => { if (!batteryState.isSupported) { return (
- Battery sensor:
not supported
+ Battery sensor: not supported
); } + + if (!batteryState.fetched) { + return ( +
+ Battery sensor: supported
+ Battery state: fetching +
+ ); + } + return (
- Battery sensor:  supported
- Charge level:  {(batteryState.level * 100).toFixed(0)}%
- Charging:  {batteryState.charging ? 'yes' : 'no'}
- Charging time:  {batteryState.chargingTime ? batteryState.chargingTime : 'finished'}
- Discharging time:  {batteryState.dischargingTime} + Battery sensor:   supported
+ Battery state: fetched
+ Charge level:   { (batteryState.level * 100).toFixed(0) }%
+ Charging:   { batteryState.charging ? 'yes' : 'no' }
+ Charging time:   + { batteryState.chargingTime ? batteryState.chargingTime : 'finished' }
+ Discharging time:   { batteryState.dischargingTime }
); }; @@ -38,7 +50,8 @@ const Demo = () => { ```ts const {isSupported, level, charging, dischargingTime, chargingTime} = useBattery(); ``` -- **`isSupported`**_`: boolean`_ - wheter browser/devise supports BatteryManager; +- **`isSupported`**_`: boolean`_ - whether browser/devise supports BatteryManager; +- **`fetched`**_`: boolean`_ - whether battery state is fetched; - **`level`**_`: number`_ - representing the system's battery charge level scaled to a value between 0.0 and 1.0. - **`charging`**_`: boolean`_ - indicating whether or not the battery is currently being charged. - **`dischargingTime`**_`: number`_ - remaining time in seconds until the battery is completely discharged and the system will suspend. diff --git a/src/__stories__/useBattery.story.tsx b/src/__stories__/useBattery.story.tsx index 922b1442..9a38321a 100644 --- a/src/__stories__/useBattery.story.tsx +++ b/src/__stories__/useBattery.story.tsx @@ -9,22 +9,29 @@ const Demo = () => { if (!batteryState.isSupported) { return (
- Battery sensor:
not supported
+ Battery sensor: not supported
); } + + if (!batteryState.fetched) { + return ( +
+ Battery sensor: supported
+ Battery state: fetching +
+ ); + } + return (
- Battery sensor:  supported -
- Charge level:  {(batteryState.level * 100).toFixed(0)}% -
- Charging:  {batteryState.charging ? 'yes' : 'no'} -
+ Battery sensor:   supported
+ Battery state: fetched
+ Charge level:   {(batteryState.level * 100).toFixed(0)}%
+ Charging:   {batteryState.charging ? 'yes' : 'no'}
Charging time:   - {batteryState.chargingTime ? batteryState.chargingTime : 'finished'} -
- Discharging time:  {batteryState.dischargingTime} + {batteryState.chargingTime ? batteryState.chargingTime : 'finished'}
+ Discharging time:   {batteryState.dischargingTime}
); }; diff --git a/src/useBattery.ts b/src/useBattery.ts index fdba50ca..6c8057b4 100644 --- a/src/useBattery.ts +++ b/src/useBattery.ts @@ -1,13 +1,7 @@ import * as React from 'react'; +import * as isEqual from 'react-fast-compare'; import { off, on } from './util'; -enum BatteryManagerEvents { - levelChange = 'levelchange', - dischargingTimeChange = 'dischargingtimechange', - chargingTimeChange = 'chargingtimechange', - chargingChange = 'chargingchange', -} - export interface BatteryState { charging: boolean; chargingTime: number; @@ -26,81 +20,85 @@ interface NavigatorWithPossibleBattery extends Navigator { getBattery?: () => Promise; } +type UseBatteryState = + | { isSupported: false } // Battery API is not supported + | { isSupported: true; fetched: false } // battery API supported but not fetched yet + | BatteryState & { isSupported: true; fetched: true }; // battery API supported and fetched + const nav: NavigatorWithPossibleBattery = navigator; +const isBatteryApiSupported = nav && typeof nav.getBattery !== 'undefined'; -type UseBatteryState = BatteryState & { - isSupported: boolean; -}; +function useBatteryMock(): UseBatteryState { + return { isSupported: false }; +} -export default function useBattery() { - const [state, setState] = React.useState({ - isSupported: nav && typeof nav.getBattery !== 'undefined', - level: 1, - charging: true, - dischargingTime: Infinity, - chargingTime: 0, - }); - const battery = React.useRef(); - let isMounted = true; +function useBattery(): UseBatteryState { + const [state, setState] = React.useState({ isSupported: true, fetched: false }); + const battery = React.useRef(); + const isMounted = React.useRef(false); - if (state.isSupported) { - const onChange = React.useCallback(() => { - if (isMounted && battery.current) { - setState({ - isSupported: true, - level: battery.current.level, - charging: battery.current.charging, - dischargingTime: battery.current.dischargingTime, - chargingTime: battery.current.chargingTime, - }); - } - }, [setState]); + const handleChange = React.useCallback(() => { + if (!isMounted.current || !battery.current) { + return; + } - const bindBatteryEvents = React.useCallback( - (bat: BatteryManager) => { - on(bat, BatteryManagerEvents.chargingChange, onChange); - on(bat, BatteryManagerEvents.chargingTimeChange, onChange); - on(bat, BatteryManagerEvents.dischargingTimeChange, onChange); - on(bat, BatteryManagerEvents.levelChange, onChange); - }, - [onChange] - ); + const newState: UseBatteryState = { + isSupported: true, + fetched: true, + level: battery.current.level, + charging: battery.current.charging, + dischargingTime: battery.current.dischargingTime, + chargingTime: battery.current.chargingTime, + }; - const unbindBatteryEvents = React.useCallback( - (bat: BatteryManager) => { - off(bat, BatteryManagerEvents.chargingChange, onChange); - off(bat, BatteryManagerEvents.chargingTimeChange, onChange); - off(bat, BatteryManagerEvents.dischargingTimeChange, onChange); - off(bat, BatteryManagerEvents.levelChange, onChange); - }, - [onChange] - ); + !isEqual(state, newState) && setState(newState); + }, [state, setState]); - React.useEffect(() => { - battery.current && bindBatteryEvents(battery.current); + const bindBatteryEvents = React.useCallback(() => { + if (!battery.current || !isMounted.current) { + return; + } - return () => { - battery.current && unbindBatteryEvents(battery.current); - }; - }, [onChange]); + on(battery.current, 'chargingchange', handleChange); + on(battery.current, 'chargingtimechange', handleChange); + on(battery.current, 'dischargingtimechange', handleChange); + on(battery.current, 'levelchange', handleChange); + }, [handleChange]); + const unbindBatteryEvents = React.useCallback(() => { + if (!battery.current) { + return; + } - React.useEffect(() => { - nav.getBattery!().then((bat: BatteryManager) => { - if (!isMounted) { - return; - } + off(battery.current, 'chargingchange', handleChange); + off(battery.current, 'chargingtimechange', handleChange); + off(battery.current, 'dischargingtimechange', handleChange); + off(battery.current, 'levelchange', handleChange); + }, [handleChange]); - battery.current = bat; - bindBatteryEvents(bat); - }); + React.useEffect(() => { + bindBatteryEvents(); + handleChange(); // this one is for case when update performed between unbind and bind, extremely rare, but better to handle - return () => { - isMounted = false; + return unbindBatteryEvents; + }, [handleChange]); - battery.current && unbindBatteryEvents(battery.current); - }; - }, []); - } + React.useEffect(() => { + isMounted.current = true; + + nav.getBattery!().then((bat: BatteryManager) => { + battery.current = bat; + + bindBatteryEvents(); + handleChange(); + }); + + return () => { + isMounted.current = false; + unbindBatteryEvents(); + }; + }, []); return state; } + +export default isBatteryApiSupported ? useBattery : useBatteryMock; From 5d31cf0dbc43cd1d24a1fc911f4583dc208e0c51 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 20 Aug 2019 19:40:22 +0200 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20support=20useBattery?= =?UTF-8?q?=20hook=20on=20server=20side?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/useBattery.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/useBattery.ts b/src/useBattery.ts index 6c8057b4..c2f6834f 100644 --- a/src/useBattery.ts +++ b/src/useBattery.ts @@ -25,8 +25,8 @@ type UseBatteryState = | { isSupported: true; fetched: false } // battery API supported but not fetched yet | BatteryState & { isSupported: true; fetched: true }; // battery API supported and fetched -const nav: NavigatorWithPossibleBattery = navigator; -const isBatteryApiSupported = nav && typeof nav.getBattery !== 'undefined'; +const isBatteryApiSupported = + typeof navigator === 'object' && typeof (navigator as NavigatorWithPossibleBattery).getBattery === 'function'; function useBatteryMock(): UseBatteryState { return { isSupported: false }; @@ -85,7 +85,7 @@ function useBattery(): UseBatteryState { React.useEffect(() => { isMounted.current = true; - nav.getBattery!().then((bat: BatteryManager) => { + (navigator as NavigatorWithPossibleBattery).getBattery!().then((bat: BatteryManager) => { battery.current = bat; bindBatteryEvents(); From 2d0fabf9accf2a3baa673e06e53eca345b62d52a Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 20 Aug 2019 20:17:55 +0200 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20use=20only=20one=20u?= =?UTF-8?q?seState=20and=20one=20useEffect=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/useBattery.ts | 87 ++++++++++++++++++----------------------------- 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/src/useBattery.ts b/src/useBattery.ts index c2f6834f..3f675665 100644 --- a/src/useBattery.ts +++ b/src/useBattery.ts @@ -2,6 +2,8 @@ import * as React from 'react'; import * as isEqual from 'react-fast-compare'; import { off, on } from './util'; +const { useState, useEffect } = React; + export interface BatteryState { charging: boolean; chargingTime: number; @@ -33,68 +35,47 @@ function useBatteryMock(): UseBatteryState { } function useBattery(): UseBatteryState { - const [state, setState] = React.useState({ isSupported: true, fetched: false }); - const battery = React.useRef(); - const isMounted = React.useRef(false); + const [state, setState] = useState({ isSupported: true, fetched: false }); - const handleChange = React.useCallback(() => { - if (!isMounted.current || !battery.current) { - return; - } + useEffect(() => { + let isMounted = true; + let battery: BatteryManager | null = null; - const newState: UseBatteryState = { - isSupported: true, - fetched: true, - level: battery.current.level, - charging: battery.current.charging, - dischargingTime: battery.current.dischargingTime, - chargingTime: battery.current.chargingTime, + const handleChange = () => { + if (!isMounted || !battery) { + return; + } + const newState: UseBatteryState = { + isSupported: true, + fetched: true, + level: battery.level, + charging: battery.charging, + dischargingTime: battery.dischargingTime, + chargingTime: battery.chargingTime, + }; + !isEqual(state, newState) && setState(newState); }; - !isEqual(state, newState) && setState(newState); - }, [state, setState]); - - const bindBatteryEvents = React.useCallback(() => { - if (!battery.current || !isMounted.current) { - return; - } - - on(battery.current, 'chargingchange', handleChange); - on(battery.current, 'chargingtimechange', handleChange); - on(battery.current, 'dischargingtimechange', handleChange); - on(battery.current, 'levelchange', handleChange); - }, [handleChange]); - const unbindBatteryEvents = React.useCallback(() => { - if (!battery.current) { - return; - } - - off(battery.current, 'chargingchange', handleChange); - off(battery.current, 'chargingtimechange', handleChange); - off(battery.current, 'dischargingtimechange', handleChange); - off(battery.current, 'levelchange', handleChange); - }, [handleChange]); - - React.useEffect(() => { - bindBatteryEvents(); - handleChange(); // this one is for case when update performed between unbind and bind, extremely rare, but better to handle - - return unbindBatteryEvents; - }, [handleChange]); - - React.useEffect(() => { - isMounted.current = true; - (navigator as NavigatorWithPossibleBattery).getBattery!().then((bat: BatteryManager) => { - battery.current = bat; - - bindBatteryEvents(); + if (!isMounted) { + return; + } + battery = bat; + on(battery, 'chargingchange', handleChange); + on(battery, 'chargingtimechange', handleChange); + on(battery, 'dischargingtimechange', handleChange); + on(battery, 'levelchange', handleChange); handleChange(); }); return () => { - isMounted.current = false; - unbindBatteryEvents(); + isMounted = false; + if (battery) { + off(battery, 'chargingchange', handleChange); + off(battery, 'chargingtimechange', handleChange); + off(battery, 'dischargingtimechange', handleChange); + off(battery, 'levelchange', handleChange); + } }; }, []); From ed5f277b0bfc832c996d063fd1fb6fff4ff608c9 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Tue, 20 Aug 2019 21:31:35 +0300 Subject: [PATCH 5/5] Simpler navigator detection (still SSR compatible); --- src/useBattery.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/useBattery.ts b/src/useBattery.ts index 3f675665..2b22b6d1 100644 --- a/src/useBattery.ts +++ b/src/useBattery.ts @@ -27,8 +27,8 @@ type UseBatteryState = | { isSupported: true; fetched: false } // battery API supported but not fetched yet | BatteryState & { isSupported: true; fetched: true }; // battery API supported and fetched -const isBatteryApiSupported = - typeof navigator === 'object' && typeof (navigator as NavigatorWithPossibleBattery).getBattery === 'function'; +const nav: NavigatorWithPossibleBattery | undefined = typeof navigator === 'object' ? navigator : undefined; +const isBatteryApiSupported = nav && typeof nav.getBattery === 'function'; function useBatteryMock(): UseBatteryState { return { isSupported: false }; @@ -56,7 +56,7 @@ function useBattery(): UseBatteryState { !isEqual(state, newState) && setState(newState); }; - (navigator as NavigatorWithPossibleBattery).getBattery!().then((bat: BatteryManager) => { + nav!.getBattery!().then((bat: BatteryManager) => { if (!isMounted) { return; }