Skip to content

Commit 5483ef1

Browse files
mlampedxaddyosmani
authored andcommitted
[core] Add optional initial value arguments to useNetworkStatus, useSaveData; useMemoryStatus (#23)
* Update useNetworkStatus to accept initialEffectiveConnectionType arg and consistently return unsupported property; add tests * Update useHardwareConcurrency to consistently return unsupported property; add tests * Update useMemoryStatus to accept initialMemoryStatus arg and consistently return unsupported property; add tests * Update useSaveData to accept initialSaveDataStatus; add tests * Update package.json with test coverage script * Update readme with documentation for new hook arguments and use cases * Update readme and network spec to address CR comments
1 parent ce9b22f commit 5483ef1

File tree

10 files changed

+265
-39
lines changed

10 files changed

+265
-39
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ const MyComponent = () => {
6868
};
6969
```
7070

71+
This hook accepts an optional `initialEffectiveConnectionType` string argument, which can be used to provide a `effectiveConnectionType` state value when the user's browser does not support the relevant [NetworkInformation API](https://wicg.github.io/netinfo/). Passing an initial value can also prove useful for server-side rendering, where the developer can pass an [ECT Client Hint](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/client-hints#ect) to detect the effective network connection type.
72+
73+
```js
74+
// Inside of a functional React component
75+
const initialEffectiveConnectionType = '4g';
76+
const { effectiveConnectionType } = useNetworkStatus(initialEffectiveConnectionType);
77+
```
78+
7179
### Save Data
7280

7381
`useSaveData` utility for adapting based on the user's browser Data Saver preferences.
@@ -87,6 +95,14 @@ const MyComponent = () => {
8795
};
8896
```
8997

98+
This hook accepts an optional `initialSaveDataStatus` boolean argument, which can be used to provide a `saveData` state value when the user's browser does not support the relevant [NetworkInformation API](https://wicg.github.io/netinfo/). Passing an initial value can also prove useful for server-side rendering, where the developer can pass a server [Save-Data Client Hint](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/client-hints#save-data) that has been converted to a boolean to detect the user's data saving preference.
99+
100+
```js
101+
// Inside of a functional React component
102+
const initialSaveDataStatus = true;
103+
const { saveData } = useSaveData(initialSaveDataStatus);
104+
```
105+
90106
### CPU Cores / Hardware Concurrency
91107

92108
`useHardwareConcurrency` utility for adapting to the number of logical CPU processor cores on the user's device.
@@ -125,6 +141,14 @@ const MyComponent = () => {
125141
};
126142
```
127143

144+
This hook accepts an optional `initialMemoryStatus` object argument, which can be used to provide a `deviceMemory` state value when the user's browser does not support the relevant [DeviceMemory API](https://github.com/w3c/device-memory). Passing an initial value can also prove useful for server-side rendering, where the developer can pass a server [Device-Memory Client Hint](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/client-hints#save-data) to detect the memory capacity of the user's device.
145+
146+
```js
147+
// Inside of a functional React component
148+
const initialMemoryStatus = { deviceMemory: 4 };
149+
const { deviceMemory } = useMemoryStatus(initialMemoryStatus);
150+
```
151+
128152
### Adaptive Code-loading & Code-splitting
129153

130154
#### Code-loading

hardware-concurrency/hardware-concurrency.test.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,33 @@ afterEach(function() {
2222
});
2323

2424
describe('useHardwareConcurrency', () => {
25+
const navigator = window.navigator;
26+
27+
afterEach(() => {
28+
if (!window.navigator) window.navigator = navigator;
29+
});
30+
31+
test(`should return "true" for unsupported case`, () => {
32+
Object.defineProperty(window, 'navigator', {
33+
value: undefined,
34+
configurable: true,
35+
writable: true
36+
});
37+
38+
const { useHardwareConcurrency } = require('./');
39+
const { result } = renderHook(() => useHardwareConcurrency());
40+
41+
expect(result.current.unsupported).toBe(true);
42+
});
43+
2544
test(`should return window.navigator.hardwareConcurrency`, () => {
2645
const { useHardwareConcurrency } = require('./');
2746
const { result } = renderHook(() => useHardwareConcurrency());
28-
expect(result.current.numberOfLogicalProcessors).toBe(window.navigator.hardwareConcurrency);
47+
48+
expect(result.current.numberOfLogicalProcessors).toBe(
49+
window.navigator.hardwareConcurrency
50+
);
51+
expect(result.current.unsupported).toBe(false);
2952
});
3053

3154
test('should return 4 for device of hardwareConcurrency = 4', () => {
@@ -38,6 +61,7 @@ describe('useHardwareConcurrency', () => {
3861
const { result } = renderHook(() => useHardwareConcurrency());
3962

4063
expect(result.current.numberOfLogicalProcessors).toEqual(4);
64+
expect(result.current.unsupported).toBe(false);
4165
});
4266

4367
test('should return 2 for device of hardwareConcurrency = 2', () => {
@@ -50,5 +74,6 @@ describe('useHardwareConcurrency', () => {
5074
const { result } = renderHook(() => useHardwareConcurrency());
5175

5276
expect(result.current.numberOfLogicalProcessors).toEqual(2);
77+
expect(result.current.unsupported).toBe(false);
5378
});
5479
});

hardware-concurrency/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
let initialHardwareConcurrency;
1818
if (typeof navigator !== 'undefined' && 'hardwareConcurrency' in navigator) {
19-
initialHardwareConcurrency = { numberOfLogicalProcessors: navigator.hardwareConcurrency };
19+
initialHardwareConcurrency = {
20+
unsupported: false,
21+
numberOfLogicalProcessors: navigator.hardwareConcurrency
22+
};
2023
} else {
2124
initialHardwareConcurrency = { unsupported: true };
2225
}

memory/index.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,28 @@ if (typeof navigator !== 'undefined' && 'deviceMemory' in navigator) {
2020
} else {
2121
unsupported = true;
2222
}
23-
let initialMemoryStatus;
23+
let memoryStatus;
2424
if (!unsupported) {
2525
const performanceMemory = 'memory' in performance ? performance.memory : null;
26-
initialMemoryStatus = {
26+
memoryStatus = {
27+
unsupported,
2728
deviceMemory: navigator.deviceMemory,
28-
totalJSHeapSize: performanceMemory ? performanceMemory.totalJSHeapSize : null,
29+
totalJSHeapSize: performanceMemory
30+
? performanceMemory.totalJSHeapSize
31+
: null,
2932
usedJSHeapSize: performanceMemory ? performanceMemory.usedJSHeapSize : null,
30-
jsHeapSizeLimit: performanceMemory ? performanceMemory.jsHeapSizeLimit : null
33+
jsHeapSizeLimit: performanceMemory
34+
? performanceMemory.jsHeapSizeLimit
35+
: null
3136
};
3237
} else {
33-
initialMemoryStatus = { unsupported };
38+
memoryStatus = { unsupported };
3439
}
3540

36-
const useMemoryStatus = () => {
37-
return { ...initialMemoryStatus };
41+
const useMemoryStatus = initialMemoryStatus => {
42+
return unsupported && initialMemoryStatus
43+
? { ...memoryStatus, ...initialMemoryStatus }
44+
: { ...memoryStatus };
3845
};
3946

4047
export { useMemoryStatus };

memory/memory.test.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ afterEach(function() {
2222
});
2323

2424
const getMemoryStatus = currentResult => ({
25+
unsupported: false,
2526
deviceMemory: currentResult.deviceMemory,
2627
totalJSHeapSize: currentResult.totalJSHeapSize,
2728
usedJSHeapSize: currentResult.usedJSHeapSize,
@@ -36,6 +37,21 @@ describe('useMemoryStatus', () => {
3637
expect(result.current.unsupported).toBe(true);
3738
});
3839

40+
test('should return initialMemoryStatus for unsupported case', () => {
41+
const mockInitialMemoryStatus = {
42+
deviceMemory: 4
43+
};
44+
const { deviceMemory } = mockInitialMemoryStatus;
45+
46+
const { useMemoryStatus } = require('./');
47+
const { result } = renderHook(() =>
48+
useMemoryStatus(mockInitialMemoryStatus)
49+
);
50+
51+
expect(result.current.unsupported).toBe(true);
52+
expect(result.current.deviceMemory).toEqual(deviceMemory);
53+
});
54+
3955
test('should return mockMemory status', () => {
4056
const mockMemoryStatus = {
4157
deviceMemory: 4,
@@ -55,6 +71,54 @@ describe('useMemoryStatus', () => {
5571
const { useMemoryStatus } = require('./');
5672
const { result } = renderHook(() => useMemoryStatus());
5773

58-
expect(getMemoryStatus(result.current)).toEqual(mockMemoryStatus);
74+
expect(getMemoryStatus(result.current)).toEqual({
75+
...mockMemoryStatus,
76+
unsupported: false
77+
});
78+
});
79+
80+
test('should return mockMemory status without performance memory data', () => {
81+
const mockMemoryStatus = {
82+
deviceMemory: 4
83+
};
84+
85+
global.navigator.deviceMemory = mockMemoryStatus.deviceMemory;
86+
delete global.window.performance.memory;
87+
88+
const { useMemoryStatus } = require('./');
89+
const { result } = renderHook(() => useMemoryStatus());
90+
91+
expect(result.current.deviceMemory).toEqual(mockMemoryStatus.deviceMemory);
92+
expect(result.current.unsupported).toEqual(false);
93+
});
94+
95+
test('should not return initialMemoryStatus for supported case', () => {
96+
const mockMemoryStatus = {
97+
deviceMemory: 4,
98+
totalJSHeapSize: 60,
99+
usedJSHeapSize: 40,
100+
jsHeapSizeLimit: 50
101+
};
102+
const mockInitialMemoryStatus = {
103+
deviceMemory: 4
104+
};
105+
106+
global.navigator.deviceMemory = mockMemoryStatus.deviceMemory;
107+
108+
global.window.performance.memory = {
109+
totalJSHeapSize: mockMemoryStatus.totalJSHeapSize,
110+
usedJSHeapSize: mockMemoryStatus.usedJSHeapSize,
111+
jsHeapSizeLimit: mockMemoryStatus.jsHeapSizeLimit
112+
};
113+
114+
const { useMemoryStatus } = require('./');
115+
const { result } = renderHook(() =>
116+
useMemoryStatus(mockInitialMemoryStatus)
117+
);
118+
119+
expect(getMemoryStatus(result.current)).toEqual({
120+
...mockMemoryStatus,
121+
unsupported: false
122+
});
59123
});
60124
});

network/index.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,29 @@ import { useState, useEffect } from 'react';
1818

1919
let unsupported;
2020

21-
const useNetworkStatus = () => {
21+
const useNetworkStatus = initialEffectiveConnectionType => {
2222
if ('connection' in navigator && 'effectiveType' in navigator.connection) {
2323
unsupported = false;
2424
} else {
2525
unsupported = true;
2626
}
2727

28-
const initialNetworkStatus = !unsupported ? {
29-
effectiveConnectionType: navigator.connection.effectiveType
30-
} : {
31-
unsupported
32-
};
28+
const initialNetworkStatus = {
29+
unsupported,
30+
effectiveConnectionType: unsupported
31+
? initialEffectiveConnectionType
32+
: navigator.connection.effectiveType
33+
};
3334

3435
const [networkStatus, setNetworkStatus] = useState(initialNetworkStatus);
3536

3637
useEffect(() => {
3738
if (!unsupported) {
3839
const navigatorConnection = navigator.connection;
3940
const updateECTStatus = () => {
40-
setNetworkStatus({ effectiveConnectionType: navigatorConnection.effectiveType });
41+
setNetworkStatus({
42+
effectiveConnectionType: navigatorConnection.effectiveType
43+
});
4144
};
4245
navigatorConnection.addEventListener('change', updateECTStatus);
4346
return () => {

network/network.test.js

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,91 @@ import { renderHook, act } from '@testing-library/react-hooks';
1919
import { useNetworkStatus } from './';
2020

2121
describe('useNetworkStatus', () => {
22+
const map = {};
23+
24+
const ectStatusListeners = {
25+
addEventListener: jest.fn().mockImplementation((event, callback) => {
26+
map[event] = callback;
27+
}),
28+
removeEventListener: jest.fn()
29+
};
30+
31+
afterEach(() => {
32+
Object.values(ectStatusListeners).forEach(listener => listener.mockClear());
33+
});
34+
35+
/**
36+
* Tests that addEventListener or removeEventListener was called during the
37+
* lifecycle of the useEffect hook within useNetworkStatus
38+
*/
39+
const testEctStatusEventListenerMethod = method => {
40+
expect(method).toBeCalledTimes(1);
41+
expect(method.mock.calls[0][0]).toEqual('change');
42+
expect(method.mock.calls[0][1].constructor).toEqual(Function);
43+
};
44+
45+
test(`should return "true" for unsupported case`, () => {
46+
const { result } = renderHook(() => useNetworkStatus());
47+
48+
expect(result.current.unsupported).toBe(true);
49+
});
50+
51+
test('should return initialEffectiveConnectionType for unsupported case', () => {
52+
const initialEffectiveConnectionType = '4g';
53+
54+
const { result } = renderHook(() =>
55+
useNetworkStatus(initialEffectiveConnectionType)
56+
);
57+
58+
expect(result.current.unsupported).toBe(true);
59+
expect(result.current.effectiveConnectionType).toBe(
60+
initialEffectiveConnectionType
61+
);
62+
});
63+
2264
test('should return 4g of effectiveConnectionType', () => {
2365
global.navigator.connection = {
24-
effectiveType: '4g',
25-
addEventListener: jest.fn(),
26-
removeEventListener: jest.fn()
66+
...ectStatusListeners,
67+
effectiveType: '4g'
2768
};
2869

2970
const { result } = renderHook(() => useNetworkStatus());
30-
71+
72+
testEctStatusEventListenerMethod(ectStatusListeners.addEventListener);
73+
expect(result.current.unsupported).toBe(false);
74+
expect(result.current.effectiveConnectionType).toEqual('4g');
75+
});
76+
77+
test('should not return initialEffectiveConnectionType for supported case', () => {
78+
const initialEffectiveConnectionType = '2g';
79+
global.navigator.connection = {
80+
...ectStatusListeners,
81+
effectiveType: '4g'
82+
};
83+
84+
const { result } = renderHook(() =>
85+
useNetworkStatus(initialEffectiveConnectionType)
86+
);
87+
88+
testEctStatusEventListenerMethod(ectStatusListeners.addEventListener);
89+
expect(result.current.unsupported).toBe(false);
3190
expect(result.current.effectiveConnectionType).toEqual('4g');
3291
});
3392

3493
test('should update the effectiveConnectionType state', () => {
3594
const { result } = renderHook(() => useNetworkStatus());
3695

37-
act(() => result.current.setNetworkStatus({effectiveConnectionType: '2g'}));
38-
96+
act(() =>
97+
result.current.setNetworkStatus({ effectiveConnectionType: '2g' })
98+
);
99+
39100
expect(result.current.effectiveConnectionType).toEqual('2g');
40101
});
41102

42103
test('should update the effectiveConnectionType state when navigator.connection change event', () => {
43-
const map = {};
44104
global.navigator.connection = {
45-
effectiveType: '2g',
46-
addEventListener: jest.fn().mockImplementation((event, callback) => {
47-
map[event] = callback;
48-
}),
49-
removeEventListener: jest.fn()
105+
...ectStatusListeners,
106+
effectiveType: '2g'
50107
};
51108

52109
const { result } = renderHook(() => useNetworkStatus());
@@ -55,4 +112,17 @@ describe('useNetworkStatus', () => {
55112

56113
expect(result.current.effectiveConnectionType).toEqual('4g');
57114
});
115+
116+
test('should remove the listener for the navigator.connection change event on unmount', () => {
117+
global.navigator.connection = {
118+
...ectStatusListeners,
119+
effectiveType: '2g'
120+
};
121+
122+
const { unmount } = renderHook(() => useNetworkStatus());
123+
124+
testEctStatusEventListenerMethod(ectStatusListeners.addEventListener);
125+
unmount();
126+
testEctStatusEventListenerMethod(ectStatusListeners.removeEventListener);
127+
});
58128
});

0 commit comments

Comments
 (0)