From 23037f207d07604dd2cd7e2cc4ba9475221be780 Mon Sep 17 00:00:00 2001 From: xobotyi Date: Sun, 31 Jan 2021 03:14:20 +0300 Subject: [PATCH] feat: refactor the useNetwork hook. Improved hook performance, now works with safari. BREAKING CHANGE: `useNetwork` hook renamed to `useNetworkState`. --- .editorconfig | 4 +- README.md | 2 +- docs/useNetwork.md | 29 --------- docs/useNetworkState.md | 77 ++++++++++++++++++++++ src/index.ts | 2 +- src/useNetwork.ts | 88 ------------------------- src/useNetworkState.ts | 118 ++++++++++++++++++++++++++++++++++ stories/useNetwork.story.tsx | 18 ++++-- tests/useNetworkState.test.ts | 26 ++++++++ 9 files changed, 238 insertions(+), 126 deletions(-) delete mode 100644 docs/useNetwork.md create mode 100644 docs/useNetworkState.md delete mode 100644 src/useNetwork.ts create mode 100644 src/useNetworkState.ts create mode 100644 tests/useNetworkState.test.ts diff --git a/.editorconfig b/.editorconfig index 15f1c6f82e..d5ca7cabe0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,8 +24,8 @@ ij_typescript_catch_on_new_line = false ij_typescript_spaces_within_interpolation_expressions = false [*.md] -max_line_length = 0 +max_line_length = 120 trim_trailing_whitespace = false [COMMIT_EDITMSG] -max_line_length = 0 +max_line_length = 80 diff --git a/README.md b/README.md index 6abc7b423d..18a751d297 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ - [`useMotion`](./docs/useMotion.md) — tracks state of device's motion sensor. - [`useMouse` and `useMouseHovered`](./docs/useMouse.md) — tracks state of mouse position. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemouse--docs) - [`useMouseWheel`](./docs/useMouseWheel.md) — tracks deltaY of scrolled mouse wheel. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemousewheel--docs) - - [`useNetwork`](./docs/useNetwork.md) — tracks state of user's internet connection. + - [`useNetworkState`](./docs/useNetworkState.md) — tracks the state of browser's network connection. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usenetworkstate--demo) - [`useOrientation`](./docs/useOrientation.md) — tracks state of device's screen orientation. - [`usePageLeave`](./docs/usePageLeave.md) — triggers when mouse leaves page boundaries. - [`useScratch`](./docs/useScratch.md) — tracks mouse click-and-scrub state. diff --git a/docs/useNetwork.md b/docs/useNetwork.md deleted file mode 100644 index f11c6039ca..0000000000 --- a/docs/useNetwork.md +++ /dev/null @@ -1,29 +0,0 @@ -# `useNetwork` - -React sensor hook that tracks connected hardware devices. Returns: - -```json -{ - "online": true, - "since": "2018-10-27T08:59:05.562Z", - "downlink": 10, - "effectiveType": "4g", - "rtt": 50 -} -``` - -## Usage - -```jsx -import {useNetwork} from 'react-use'; - -const Demo = () => { - const state = useNetwork(); - - return ( -
-      {JSON.stringify(state, null, 2)}
-    
- ); -}; -``` diff --git a/docs/useNetworkState.md b/docs/useNetworkState.md new file mode 100644 index 0000000000..d05dfd2368 --- /dev/null +++ b/docs/useNetworkState.md @@ -0,0 +1,77 @@ +# `useNetworkState` + +Tracks the state of browser's network connection. + +As of the standard it is not guaranteed that browser connected to the _Internet_, it only guarantees the network +connection. + +## Usage + +```jsx +import {useNetworkState} from 'react-use'; + +const Demo = () => { + const state = useNetworkState(); + + return ( +
+      {JSON.stringify(state, null, 2)}
+    
+ ); +}; +``` + +#### State interface: + +```typescript +interface IUseNetworkState { + /** + * @desc Whether browser connected to the network or not. + */ + online: boolean | undefined; + /** + * @desc Previous value of `online` property. Helps to identify if browser + * just connected or lost connection. + */ + previous: boolean | undefined; + /** + * @desc The {Date} object pointing to the moment when state change occurred. + */ + since: Date | undefined; + /** + * @desc Effective bandwidth estimate in megabits per second, rounded to the + * nearest multiple of 25 kilobits per seconds. + */ + downlink: number | undefined; + /** + * @desc Maximum downlink speed, in megabits per second (Mbps), for the + * underlying connection technology + */ + downlinkMax: number | undefined; + /** + * @desc Effective type of the connection meaning one of 'slow-2g', '2g', '3g', or '4g'. + * This value is determined using a combination of recently observed round-trip time + * and downlink values. + */ + effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | undefined; + /** + * @desc Estimated effective round-trip time of the current connection, rounded + * to the nearest multiple of 25 milliseconds + */ + rtt: number | undefined; + /** + * @desc Wheter user has set a reduced data usage option on the user agent. + */ + saveData: boolen | undefined; + /** + * @desc The type of connection a device is using to communicate with the network. + */ + type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown' | undefined; +} +``` + +#### Call signature + +```typescript +function useNetworkState(initialState?: IUseNetworkState | (() => IUseNetworkState)): IUseNetworkState; +``` diff --git a/src/index.ts b/src/index.ts index 1535fa05ee..4c8901a562 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,7 +61,7 @@ export { default as useMountedState } from './useMountedState'; export { default as useMouse } from './useMouse'; export { default as useMouseHovered } from './useMouseHovered'; export { default as useMouseWheel } from './useMouseWheel'; -export { default as useNetwork } from './useNetwork'; +export { default as useNetworkState } from './useNetworkState'; export { default as useNumber } from './useNumber'; export { default as useObservable } from './useObservable'; export { default as useOrientation } from './useOrientation'; diff --git a/src/useNetwork.ts b/src/useNetwork.ts deleted file mode 100644 index a2df381dcf..0000000000 --- a/src/useNetwork.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useEffect, useState } from 'react'; -import { off, on } from './misc/util'; - -export interface NetworkState { - online?: boolean; - since?: Date; - downlink?: number; - downlinkMax?: number; - effectiveType?: string; - rtt?: number; - type?: string; -} - -const getConnection = () => { - if (typeof navigator !== 'object') { - return null; - } - const nav = navigator as any; - return nav.connection || nav.mozConnection || nav.webkitConnection; -}; - -const getConnectionState = (): NetworkState => { - const connection = getConnection(); - if (!connection) { - return {}; - } - const { downlink, downlinkMax, effectiveType, type, rtt } = connection; - return { - downlink, - downlinkMax, - effectiveType, - type, - rtt, - }; -}; - -const useNetwork = (initialState: NetworkState = {}) => { - const [state, setState] = useState(initialState); - - useEffect(() => { - let localState = state; - const localSetState = (patch) => { - localState = { ...localState, ...patch }; - setState(localState); - }; - const connection = getConnection(); - - const onOnline = () => { - localSetState({ - online: true, - since: new Date(), - }); - }; - const onOffline = () => { - localSetState({ - online: false, - since: new Date(), - }); - }; - const onConnectionChange = () => { - localSetState(getConnectionState()); - }; - - on(window, 'online', onOnline); - on(window, 'offline', onOffline); - if (connection) { - on(connection, 'change', onConnectionChange); - localSetState({ - ...state, - online: navigator.onLine, - since: undefined, - ...getConnectionState(), - }); - } - - return () => { - off(window, 'online', onOnline); - off(window, 'offline', onOffline); - if (connection) { - off(connection, 'change', onConnectionChange); - } - }; - }, []); - - return state; -}; - -export default useNetwork; diff --git a/src/useNetworkState.ts b/src/useNetworkState.ts new file mode 100644 index 0000000000..dc17a4ff52 --- /dev/null +++ b/src/useNetworkState.ts @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react'; +import { off, on } from './misc/util'; +import { IHookStateInitAction } from './misc/hookState'; + +export interface INetworkInformation extends EventTarget { + readonly downlink: number; + readonly downlinkMax: number; + readonly effectiveType: 'slow-2g' | '2g' | '3g' | '4g'; + readonly rtt: number; + readonly saveData: boolean; + readonly type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown'; + + onChange: (event: Event) => void; +} + +export interface IUseNetworkState { + /** + * @desc Whether browser connected to the network or not. + */ + online: boolean | undefined; + /** + * @desc Previous value of `online` property. Helps to identify if browser + * just connected or lost connection. + */ + previous: boolean | undefined; + /** + * @desc The {Date} object pointing to the moment when state change occurred. + */ + since: Date | undefined; + /** + * @desc Effective bandwidth estimate in megabits per second, rounded to the + * nearest multiple of 25 kilobits per seconds. + */ + downlink: INetworkInformation['downlink'] | undefined; + /** + * @desc Maximum downlink speed, in megabits per second (Mbps), for the + * underlying connection technology + */ + downlinkMax: INetworkInformation['downlinkMax'] | undefined; + /** + * @desc Effective type of the connection meaning one of 'slow-2g', '2g', '3g', or '4g'. + * This value is determined using a combination of recently observed round-trip time + * and downlink values. + */ + effectiveType: INetworkInformation['effectiveType'] | undefined; + /** + * @desc Estimated effective round-trip time of the current connection, rounded + * to the nearest multiple of 25 milliseconds + */ + rtt: INetworkInformation['rtt'] | undefined; + /** + * @desc {true} if the user has set a reduced data usage option on the user agent. + */ + saveData: INetworkInformation['saveData'] | undefined; + /** + * @desc The type of connection a device is using to communicate with the network. + * It will be one of the following values: + * - bluetooth + * - cellular + * - ethernet + * - none + * - wifi + * - wimax + * - other + * - unknown + */ + type: INetworkInformation['type'] | undefined; +} + +const nav: + | (Navigator & Partial>) + | undefined = navigator; +const conn: INetworkInformation | undefined = nav && (nav.connection || nav.mozConnection || nav.webkitConnection); + +function getConnectionState(previousState?: IUseNetworkState): IUseNetworkState { + const online = nav?.onLine; + const previousOnline = previousState?.online; + + return { + online, + previous: previousOnline, + since: online !== previousOnline ? new Date() : previousState?.since, + downlink: conn?.downlink, + downlinkMax: conn?.downlinkMax, + effectiveType: conn?.effectiveType, + rtt: conn?.rtt, + saveData: conn?.saveData, + type: conn?.type, + }; +} + +export default function useNetworkState(initialState?: IHookStateInitAction): IUseNetworkState { + const [state, setState] = useState(initialState ?? getConnectionState); + + useEffect(() => { + const handleStateChange = () => { + setState(getConnectionState); + }; + + on(window, 'online', handleStateChange, { passive: true }); + on(window, 'offline', handleStateChange, { passive: true }); + + if (conn) { + on(conn, 'change', handleStateChange, { passive: true }); + } + + return () => { + off(window, 'online', handleStateChange); + off(window, 'offline', handleStateChange); + + if (conn) { + off(conn, 'change', handleStateChange); + } + }; + }, []); + + return state; +} diff --git a/stories/useNetwork.story.tsx b/stories/useNetwork.story.tsx index 5a9f36146a..72763df795 100644 --- a/stories/useNetwork.story.tsx +++ b/stories/useNetwork.story.tsx @@ -1,14 +1,22 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useNetwork } from '../src'; +import { useEffect } from 'react'; +import { useNetworkState } from '../src'; import ShowDocs from './util/ShowDocs'; const Demo = () => { - const state = useNetwork(); + const state = useNetworkState(); - return
{JSON.stringify(state, null, 2)}
; + useEffect(() => { + console.log(state); + }, [state]) + + return
+
Since JSON do not output `undefined` fields look the console to see whole the state
+
{JSON.stringify(state, null, 2)}
+
; }; -storiesOf('Sensors/useNetwork', module) - .add('Docs', () => ) +storiesOf('Sensors/useNetworkState', module) + .add('Docs', () => ) .add('Demo', () => ); diff --git a/tests/useNetworkState.test.ts b/tests/useNetworkState.test.ts new file mode 100644 index 0000000000..93c583aac0 --- /dev/null +++ b/tests/useNetworkState.test.ts @@ -0,0 +1,26 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { useNetworkState } from '../src'; + +//ToDo: improve tests +describe(`useNetworkState`, () => { + it('should be defined', () => { + expect(useNetworkState).toBeDefined(); + }); + + it('should return an object of certain structure', () => { + const hook = renderHook(() => useNetworkState(), { initialProps: false }); + + expect(typeof hook.result.current).toEqual('object'); + expect(Object.keys(hook.result.current)).toEqual([ + 'online', + 'previous', + 'since', + 'downlink', + 'downlinkMax', + 'effectiveType', + 'rtt', + 'saveData', + 'type', + ]); + }); +});