├── .gitignore ├── tsconfig.json ├── src ├── index.tsx └── index.test.tsx ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | .rts2_cache_cjs 6 | .rts2_cache_esm 7 | .rts2_cache_umd 8 | dist 9 | yarn.lock 10 | package-lock.json 11 | coverage 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "strictFunctionTypes": true, 14 | "strictPropertyInitialization": true, 15 | "noImplicitThis": true, 16 | "alwaysStrict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "moduleResolution": "node", 22 | "baseUrl": "./", 23 | "paths": { 24 | "*": ["src/*", "node_modules/*"] 25 | }, 26 | "jsx": "react", 27 | "esModuleInterop": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | interface IState { 4 | whenOnline?: React.ReactNode; 5 | whenOffline?: React.ReactNode; 6 | startOnline?: boolean; 7 | } 8 | 9 | let defaultState: IState = { 10 | whenOnline: 'online', 11 | whenOffline: 'offline', 12 | startOnline: true, 13 | }; 14 | 15 | function useNavigatorOnline(state: IState = {}) { 16 | let { whenOnline, whenOffline, startOnline } = { ...defaultState, ...state }; 17 | let [value, setValue] = useState(startOnline); 18 | 19 | useEffect(() => { 20 | if (window.navigator.onLine !== value) { 21 | setValue(window.navigator.onLine); 22 | return; 23 | } 24 | 25 | function handleOnlineStatus() { 26 | setValue(window.navigator.onLine); 27 | } 28 | 29 | window.addEventListener('online', handleOnlineStatus); 30 | window.addEventListener('offline', handleOnlineStatus); 31 | 32 | return () => { 33 | window.removeEventListener('online', handleOnlineStatus); 34 | window.removeEventListener('offline', handleOnlineStatus); 35 | }; 36 | }, [value, setValue]); 37 | 38 | let isOnline = value === true; 39 | let isOffline = value === false; 40 | let status = isOnline ? whenOnline : whenOffline; 41 | 42 | return { status, isOnline, isOffline }; 43 | } 44 | 45 | export { useNavigatorOnline }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oieduardorabelo/use-navigator-online", 3 | "description": "React Hooks to detect when your browser is online/offline.", 4 | "license": "MIT", 5 | "author": "Eduardo Rabelo (https://github.com/oieduardorabelo)", 6 | "repository": { 7 | "url": "https://github.com/oieduardorabelo/use-navigator-online", 8 | "type": "git" 9 | }, 10 | "version": "6.2.2", 11 | "main": "dist/index.js", 12 | "module": "dist/use-navigator-online.esm.js", 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "build": "tsdx build", 18 | "fmt:p": "prettier-package-json --write", 19 | "start": "tsdx watch", 20 | "test": "tsdx test --env=jsdom", 21 | "test:w": "tsdx test --env=jsdom --watchAll" 22 | }, 23 | "typings": "dist/index.d.ts", 24 | "peerDependencies": { 25 | "react": "16.8 - 18", 26 | "react-dom": "16.8 - 18" 27 | }, 28 | "devDependencies": { 29 | "@testing-library/dom": "8.14.0", 30 | "@testing-library/react-hooks": "8.0.1", 31 | "@types/jest": "28.1.3", 32 | "@types/react": "18.0.14", 33 | "@types/react-dom": "18.0.5", 34 | "prettier": "2.7.1", 35 | "prettier-package-json": "2.6.4", 36 | "tsdx": "0.14.1", 37 | "tslib": "2.4.0", 38 | "typescript": "4.7.4" 39 | }, 40 | "keywords": [ 41 | "browser", 42 | "hooks", 43 | "navigator", 44 | "offline", 45 | "online", 46 | "react", 47 | "react-hooks" 48 | ], 49 | "publishConfig": { 50 | "access": "public" 51 | }, 52 | "prettier": { 53 | "printWidth": 120, 54 | "semi": true, 55 | "singleQuote": true, 56 | "trailingComma": "es5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @oieduardorabelo/use-navigator-online 2 | 3 | React Hooks to detect when your browser is online/offline using [`window.navigator.onLine` API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine). 4 | 5 | To install it: 6 | 7 | ``` 8 | yarn add @oieduardorabelo/use-navigator-online 9 | ``` 10 | 11 | ## Example 12 | 13 | An online demo is available at CodeSandbox: 14 | 15 | - **Live demo:** https://codesandbox.io/s/live-demo-use-navigator-online-1xy56 16 | 17 | If you've any issues, **open an issue with a CodeSandbox link** with your issue 18 | 19 | ## API Explained 20 | 21 | In your app, you can add: 22 | 23 | ```javascript 24 | import { useNavigatorOnline } from '@oieduardorabelo/use-navigator-online'; 25 | 26 | function App() { 27 | let details = useNavigatorOnline(options) 28 | ... 29 | } 30 | ``` 31 | 32 | ### `details` object is composed of: 33 | 34 | - `details.isOnline`: It is a `Boolean` value, `true` when online 35 | - `details.isOffline`: It is a `Boolean` value, `true` when offline 36 | - `details.status`: Returns a default `String`, when online it is `"online"` and when offline it is `"offline"`. You can customize it using `options` param 37 | 38 | ### `options` object is composed of: 39 | 40 | - `options.whenOnline`: Can be any valid React children. It will replace the `String` returned in `details.status` when online. 41 | - `options.whenOffline`: Can be any valid React children. It will replace the `String` returned in `details.status` when offline. 42 | - `options.startOnline`: To support SSR, you can control the initial rendering mode using this option. It is a boolean value to determine which state your application should use first: `true` for "online-first" or `false` for "offline-first". Defaults to `true`/online-first. The value will be synced with `window.navigator.onLine` inside an `useEffect` when your application is rendered in the browser. 43 | 44 | ## Examples 45 | 46 | Using `isOnline` and `isOffline` flags: 47 | 48 | ```javascript 49 | import { useNavigatorOnline } from '@oieduardorabelo/use-navigator-online'; 50 | 51 | function App() { 52 | let { isOnline, isOffline } = useNavigatorOnline(); 53 | 54 | return ( 55 |
56 | {isOnline && We are online!} 57 | {isOffline && We are offline!} 58 |
59 | ); 60 | } 61 | ``` 62 | 63 | Using default `status`: 64 | 65 | ```javascript 66 | import { useNavigatorOnline } from '@oieduardorabelo/use-navigator-online'; 67 | 68 | function App() { 69 | // will toggle between "online" and "offline" 70 | let { status } = useNavigatorOnline(); 71 | 72 | return
Browser now is {status}!
; 73 | } 74 | ``` 75 | 76 | Custom values for `status` with `whenOnline` and `whenOffline`: 77 | 78 | ```javascript 79 | import { useNavigatorOnline } from '@oieduardorabelo/use-navigator-online'; 80 | 81 | function App() { 82 | // you can pass any React children in "whenOnline" and "whenOffline" 83 | let { status } = useNavigatorOnline({ 84 | whenOnline:

WE ARE ONLINE!

, 85 | whenOffline:

Damn, offline :(

, 86 | }); 87 | 88 | return
{status}
; 89 | } 90 | ``` 91 | 92 | No extra configuration is needed to use it on SSR: 93 | 94 | ```javascript 95 | import { useNavigatorOnline } from '@oieduardorabelo/use-navigator-online'; 96 | 97 | function App() { 98 | let { status } = useNavigatorOnline(); 99 | 100 | return
{status}
; 101 | } 102 | ``` 103 | 104 | You can initialize your application offline-first. This **will sync** with `window.navigator.onLine` when your application starts, using a `useEffect`. Trigerring a re-render of your application. 105 | 106 | ```javascript 107 | import { useNavigatorOnline } from '@oieduardorabelo/use-navigator-online'; 108 | 109 | function App() { 110 | let { status } = useNavigatorOnline({ 111 | startOnline: false 112 | }); 113 | 114 | return
{status}
; 115 | } 116 | ``` 117 | 118 | ### License 119 | 120 | [MIT License](https://oss.ninja/mit/oieduardorabelo/) © [Eduardo Rabelo](https://eduardorabelo.me) 121 | -------------------------------------------------------------------------------- /src/index.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { renderHook, act } from '@testing-library/react-hooks'; 3 | import { fireEvent, createEvent } from '@testing-library/dom'; 4 | import { useNavigatorOnline } from './'; 5 | 6 | beforeEach(() => { 7 | overwriteGlobalNavigatorOnline(); 8 | }); 9 | 10 | test('use default `window.navigator`', () => { 11 | let { result } = renderHook(() => useNavigatorOnline()); 12 | 13 | expect(result.current.status).toBe('online'); 14 | expect(result.current.isOnline).toBe(true); 15 | expect(result.current.isOffline).toBe(false); 16 | 17 | act(() => { 18 | fireEvent(window, createEvent('offline', window, {})); 19 | }); 20 | 21 | expect(result.current.status).toBe('offline'); 22 | expect(result.current.isOnline).toBe(false); 23 | expect(result.current.isOffline).toBe(true); 24 | }); 25 | 26 | test('renders online-first with `startOnline: true`', () => { 27 | let { result } = renderHook(() => useNavigatorOnline({ startOnline: true })); 28 | 29 | expect(result.current.status).toBe('online'); 30 | expect(result.current.isOnline).toBe(true); 31 | expect(result.current.isOffline).toBe(false); 32 | 33 | act(() => { 34 | fireEvent(window, createEvent('offline', window, {})); 35 | }); 36 | 37 | expect(result.current.status).toBe('offline'); 38 | expect(result.current.isOnline).toBe(false); 39 | expect(result.current.isOffline).toBe(true); 40 | }); 41 | 42 | test('toggles offline/online', () => { 43 | let { result } = renderHook(() => useNavigatorOnline()); 44 | 45 | expect(result.current.status).toBe('online'); 46 | expect(result.current.isOnline).toBe(true); 47 | expect(result.current.isOffline).toBe(false); 48 | 49 | act(() => { 50 | fireEvent(window, createEvent('offline', window, {})); 51 | }); 52 | 53 | expect(result.current.status).toBe('offline'); 54 | expect(result.current.isOnline).toBe(false); 55 | expect(result.current.isOffline).toBe(true); 56 | 57 | act(() => { 58 | fireEvent(window, createEvent('online', window, {})); 59 | }); 60 | 61 | expect(result.current.status).toBe('online'); 62 | expect(result.current.isOnline).toBe(true); 63 | expect(result.current.isOffline).toBe(false); 64 | }); 65 | 66 | test('custom react element for "whenOnline" and "whenOffline"', () => { 67 | let { result } = renderHook(() => 68 | useNavigatorOnline({ whenOffline:

Hi Offline

, whenOnline:

Hi Online

}) 69 | ); 70 | 71 | // @ts-ignore 72 | expect(result.current.status.type).toBe('h1'); 73 | // @ts-ignore 74 | expect(result.current.status.props.children).toBe('Hi Online'); 75 | 76 | act(() => { 77 | fireEvent(window, createEvent('offline', window, {})); 78 | }); 79 | 80 | // @ts-ignore 81 | expect(result.current.status.type).toBe('h2'); 82 | // @ts-ignore 83 | expect(result.current.status.props.children).toBe('Hi Offline'); 84 | }); 85 | 86 | test('custom string/number for "whenOnline" and "whenOffline"', () => { 87 | let { result } = renderHook(() => useNavigatorOnline({ whenOffline: 'Hi Offline', whenOnline: 4444 })); 88 | 89 | expect(result.current.status).toBe(4444); 90 | 91 | act(() => { 92 | fireEvent(window, createEvent('offline', window, {})); 93 | }); 94 | 95 | expect(result.current.status).toBe('Hi Offline'); 96 | }); 97 | 98 | describe("syncs with window.navigator.onLine when rendering with different 'startOnline' value", () => { 99 | test('when window.navigator.onLine value is "true"', () => { 100 | let { result } = renderHook(() => useNavigatorOnline({ startOnline: false })); 101 | 102 | expect(result.current.status).toBe('online'); 103 | expect(result.current.isOnline).toBe(true); 104 | expect(result.current.isOffline).toBe(false); 105 | 106 | act(() => { 107 | fireEvent(window, createEvent('offline', window, {})); 108 | }); 109 | 110 | expect(result.current.status).toBe('offline'); 111 | expect(result.current.isOnline).toBe(false); 112 | expect(result.current.isOffline).toBe(true); 113 | }); 114 | 115 | test('when window.navigator.onLine value is "false"', () => { 116 | act(() => { 117 | fireEvent(window, createEvent('offline', window, {})); 118 | }); 119 | 120 | let { result } = renderHook(() => useNavigatorOnline({ startOnline: true })); 121 | 122 | expect(result.current.status).toBe('offline'); 123 | expect(result.current.isOnline).toBe(false); 124 | expect(result.current.isOffline).toBe(true); 125 | 126 | act(() => { 127 | fireEvent(window, createEvent('online', window, {})); 128 | }); 129 | 130 | expect(result.current.status).toBe('online'); 131 | expect(result.current.isOnline).toBe(true); 132 | expect(result.current.isOffline).toBe(false); 133 | }); 134 | }); 135 | 136 | function overwriteGlobalNavigatorOnline() { 137 | let online = true; 138 | 139 | Object.defineProperty(window.navigator.constructor.prototype, 'onLine', { 140 | get: () => { 141 | return online; 142 | }, 143 | }); 144 | 145 | window.addEventListener('offline', function () { 146 | online = false; 147 | }); 148 | window.addEventListener('online', function () { 149 | online = true; 150 | }); 151 | } 152 | --------------------------------------------------------------------------------