feat: refactor the useNetwork hook.

Improved hook performance, now works with safari.

BREAKING CHANGE: `useNetwork` hook renamed to `useNetworkState`.
This commit is contained in:
xobotyi 2021-01-31 03:14:20 +03:00
parent 723c588fef
commit 23037f207d
No known key found for this signature in database
GPG Key ID: 36FA5C1DA5FEB62D
9 changed files with 238 additions and 126 deletions

View File

@ -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

View File

@ -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.

View File

@ -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 (
<pre>
{JSON.stringify(state, null, 2)}
</pre>
);
};
```

77
docs/useNetworkState.md Normal file
View File

@ -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 (
<pre>
{JSON.stringify(state, null, 2)}
</pre>
);
};
```
#### 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;
```

View File

@ -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';

View File

@ -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;

118
src/useNetworkState.ts Normal file
View File

@ -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<Record<'connection' | 'mozConnection' | 'webkitConnection', INetworkInformation>>)
| 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>): 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;
}

View File

@ -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 <pre>{JSON.stringify(state, null, 2)}</pre>;
useEffect(() => {
console.log(state);
}, [state])
return <div>
<div>Since JSON do not output `undefined` fields look the console to see whole the state</div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>;
};
storiesOf('Sensors/useNetwork', module)
.add('Docs', () => <ShowDocs md={require('../docs/useNetwork.md')} />)
storiesOf('Sensors/useNetworkState', module)
.add('Docs', () => <ShowDocs md={require('../docs/useNetworkState.md')} />)
.add('Demo', () => <Demo />);

View File

@ -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',
]);
});
});