-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new hook, useNetworkState (#35)
* feat: new hook, useNetworkState * fix: add MDN link to the docs page. * feat: add tests for on and off helpers. * feat: improve useNetworkState tests
- Loading branch information
Showing
10 changed files
with
355 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { useEffect } from 'react'; | ||
import { isBrowser, noop } from './util/const'; | ||
import { IInitialState } from './util/resolveHookState'; | ||
import { useSafeState } from './useSafeState'; | ||
import { off, on } from './util/misc'; | ||
|
||
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'; | ||
} | ||
|
||
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 navigator: | ||
| (Navigator & | ||
Partial<Record<'connection' | 'mozConnection' | 'webkitConnection', INetworkInformation>>) | ||
| undefined = isBrowser ? window.navigator : undefined; | ||
|
||
const conn: INetworkInformation | undefined = | ||
navigator && (navigator.connection || navigator.mozConnection || navigator.webkitConnection); | ||
|
||
function getConnectionState(previousState?: IUseNetworkState): IUseNetworkState { | ||
const online = navigator?.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, | ||
}; | ||
} | ||
|
||
/** | ||
* Tracks the state of browser's network connection. | ||
*/ | ||
export const useNetworkState: typeof navigator.connection extends undefined | ||
? undefined | ||
: (initialState?: IInitialState<IUseNetworkState>) => IUseNetworkState = isBrowser | ||
? function useNetworkState(initialState?: IInitialState<IUseNetworkState>): IUseNetworkState { | ||
const [state, setState] = useSafeState(initialState ?? getConnectionState); | ||
|
||
useEffect(() => { | ||
const handleStateChange = () => { | ||
setState(getConnectionState); | ||
}; | ||
|
||
on(window, 'online', handleStateChange, { passive: true }); | ||
on(window, 'offline', handleStateChange, { passive: true }); | ||
|
||
// it is quite hard to test it in jsdom environment maybe will be improved in future | ||
/* istanbul ignore next */ | ||
if (conn) { | ||
on(conn, 'change', handleStateChange, { passive: true }); | ||
} | ||
|
||
return () => { | ||
off(window, 'online', handleStateChange); | ||
off(window, 'offline', handleStateChange); | ||
|
||
/* istanbul ignore next */ | ||
if (conn) { | ||
off(conn, 'change', handleStateChange); | ||
} | ||
}; | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
return state; | ||
} | ||
: (noop as () => undefined); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export function on<T extends EventTarget>( | ||
obj: T | null, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
...args: Parameters<T['addEventListener']> | [string, CallableFunction | null, ...any] | ||
): void { | ||
if (obj && obj.addEventListener) { | ||
obj.addEventListener(...(args as Parameters<HTMLElement['addEventListener']>)); | ||
} | ||
} | ||
|
||
export function off<T extends EventTarget>( | ||
obj: T | null, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
...args: Parameters<T['removeEventListener']> | [string, CallableFunction | null, ...any] | ||
): void { | ||
if (obj && obj.removeEventListener) { | ||
obj.removeEventListener(...(args as Parameters<HTMLElement['removeEventListener']>)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
import { useNetworkState } from '../src/useNetworkState'; | ||
|
||
export const Example: React.FC = () => { | ||
const onlineState = useNetworkState(); | ||
|
||
return ( | ||
<div> | ||
<div>Your current internet connection state:</div> | ||
<pre>{JSON.stringify(onlineState, null, 2)}</pre> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { Example } from './useNetworkState.stories'; | ||
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; | ||
|
||
<Meta title={'Sensor/useNetwork'} component={Example} /> | ||
|
||
# useNetwork | ||
|
||
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. [[MDN Docs]](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine) | ||
#### Example | ||
|
||
<Canvas> | ||
<Story story={Example} inline /> | ||
</Canvas> | ||
|
||
## Reference | ||
|
||
```ts | ||
export interface IUseNetworkState { | ||
online: boolean | undefined; | ||
previous: boolean | undefined; | ||
since: Date | undefined; | ||
downlink: number | undefined; | ||
downlinkMax: number | undefined; | ||
effectiveType: 'slow-2g' | '2g' | '3g' | '4g' | undefined; | ||
rtt: number | undefined; | ||
saveData: boolean | undefined; | ||
type: | ||
| 'bluetooth' | ||
| 'cellular' | ||
| 'ethernet' | ||
| 'none' | ||
| 'wifi' | ||
| 'wimax' | ||
| 'other' | ||
| 'unknown' | ||
| undefined; | ||
} | ||
|
||
export function useNetworkState(initialState?: IInitialState<IUseNetworkState>): IUseNetworkState; | ||
``` | ||
|
||
#### Arguments | ||
|
||
- _**initialState**_ _`IInitialState<IUseNetworkState>`_ - the value that will be used as default, | ||
unless real network state is resolved. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { act, renderHook } from '@testing-library/react-hooks/dom'; | ||
import { useRef } from 'react'; | ||
import { useNetworkState } from '../../src/useNetworkState'; | ||
|
||
describe(`useNetworkState`, () => { | ||
it('should be defined', () => { | ||
expect(useNetworkState).toBeDefined(); | ||
}); | ||
it('should render', () => { | ||
renderHook(() => useNetworkState()); | ||
}); | ||
|
||
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', | ||
]); | ||
}); | ||
|
||
it('should rerender in case of online or offline events emitted on window', () => { | ||
const hook = renderHook( | ||
() => { | ||
const renderCount = useRef(0); | ||
return [useNetworkState(), ++renderCount.current]; | ||
}, | ||
{ initialProps: false } | ||
); | ||
|
||
expect(hook.result.current[1]).toBe(1); | ||
const prevNWState = hook.result.current[0]; | ||
|
||
act(() => { | ||
window.dispatchEvent(new Event('online')); | ||
}); | ||
expect(hook.result.current[1]).toBe(2); | ||
expect(hook.result.current[0]).not.toBe(prevNWState); | ||
}); | ||
}); |
Oops, something went wrong.