Repository: streamich/react-use
Pull request #1775 (2021-01-31 00:16:17)

feat: refactor the useNetwork hook.
# Description
Improved hook performance, now works with safari.

BREAKING CHANGE: `useNetwork` hook renamed to `useNetworkState`.

Fix: #1773
Fix: #855

## Type of change

<!-- Check all relevant options. -->
- [ ] Bug fix _(non-breaking change which fixes an issue)_
- [ ] New feature _(non-breaking change which adds functionality)_
- [x] **Breaking change** _(fix or feature that would cause existing functionality to not work as before)_

# Checklist
- [x] Read the [Contributing Guide](https://github.com/streamich/react-use/blob/master/CONTRIBUTING.md)
- [x] Perform a code self-review
- [x] Comment the code, particularly in hard-to-understand areas
- [x] Add documentation
- [x] Add hook's story at Storybook
- [ ] Cover changes with tests
- [x] Ensure the test suite passes (`yarn test`)
- [ ] Provide 100% tests coverage
- [x] Make sure code lints (`yarn lint`). Fix it with `yarn lint:fix` in case of failure.
- [x] Make sure types are fine (`yarn lint:types`).

<!-- If you can't check all the checkboxes right now - check what you can, create a Draft PR, make some changes if needed and get back to it when you will be able to put some marks in list. -->



Patch:
diff --git a/.editorconfig b/.editorconfig
index 15f1c6f8..d5ca7cab 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/CHANGELOG.md b/CHANGELOG.md
index bdcd3813..e98114a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,23 +1,3 @@
-# [16.1.0](https://github.com/streamich/react-use/compare/v16.0.0...v16.1.0) (2021-01-31)
-
-
-### Features
-
-* improve `on` and `off` util functions typing. ([723c588](https://github.com/streamich/react-use/commit/723c588fef6aba9f10ea9f5ea7bc444532519f9a))
-
-# [16.0.0](https://github.com/streamich/react-use/compare/v15.3.8...v16.0.0) (2021-01-30)
-
-
-### chore
-
-* refactoring and rearrangement. ([a27f09f](https://github.com/streamich/react-use/commit/a27f09fd367f8b172866b5fcbaf66f9a5a3481bb))
-
-
-### BREAKING CHANGES
-
-* all `create*` factories been moved to `factory` subdirectory and in case direct import should be imported like `react-use/esm/factory/createBreakpoint`
-* `comps` directory renamed to `component`
-
 ## [15.3.8](https://github.com/streamich/react-use/compare/v15.3.7...v15.3.8) (2021-01-08)
 
 
diff --git a/README.md b/README.md
index 6abc7b42..18a751d2 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@
   - [`useMotion`](./docs/useMotion.md) &mdash; tracks state of device's motion sensor.
   - [`useMouse` and `useMouseHovered`](./docs/useMouse.md) &mdash; tracks state of mouse position. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemouse--docs)
   - [`useMouseWheel`](./docs/useMouseWheel.md) &mdash; tracks deltaY of scrolled mouse wheel. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemousewheel--docs)
-  - [`useNetwork`](./docs/useNetwork.md) &mdash; tracks state of user's internet connection.
+  - [`useNetworkState`](./docs/useNetworkState.md) &mdash; 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) &mdash; tracks state of device's screen orientation.
   - [`usePageLeave`](./docs/usePageLeave.md) &mdash; triggers when mouse leaves page boundaries.
   - [`useScratch`](./docs/useScratch.md) &mdash; tracks mouse click-and-scrub state.
diff --git a/docs/useNetwork.md b/docs/useNetwork.md
deleted file mode 100644
index f11c6039..00000000
--- 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 (
-    <pre>
-      {JSON.stringify(state, null, 2)}
-    </pre>
-  );
-};
-```
diff --git a/docs/useNetworkState.md b/docs/useNetworkState.md
new file mode 100644
index 00000000..d05dfd23
--- /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 (
+    <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;
+```
diff --git a/package.json b/package.json
index 278585a8..dba66fd8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-use",
-  "version": "16.1.0",
+  "version": "15.3.8",
   "description": "Collection of React Hooks",
   "main": "lib/index.js",
   "module": "esm/index.js",
diff --git a/src/index.ts b/src/index.ts
index 1535fa05..4c8901a5 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 a2df381d..00000000
--- 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 00000000..dc17a4ff
--- /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<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;
+}
diff --git a/stories/useNetwork.story.tsx b/stories/useNetwork.story.tsx
index 5a9f3614..72763df7 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 <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 />);


Test patch:
diff --git a/tests/useNetworkState.test.ts b/tests/useNetworkState.test.ts
new file mode 100644
index 00000000..93c583aa
--- /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',
+    ]);
+  });
+});