@dhis2/app-service-offline 3.11.0-alpha.1 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ var _reactHooks = require("@testing-library/react-hooks");
4
+
5
+ var _react = _interopRequireDefault(require("react"));
6
+
7
+ var _testMocks = require("../../utils/test-mocks");
8
+
9
+ var _cacheableSectionState = require("../cacheable-section-state");
10
+
11
+ var _offlineProvider = require("../offline-provider");
12
+
13
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
14
+
15
+ const wrapper = (_ref) => {
16
+ let {
17
+ children
18
+ } = _ref;
19
+ return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
20
+ offlineInterface: _testMocks.mockOfflineInterface
21
+ }, children);
22
+ };
23
+
24
+ test('useRecordingState has stable references', () => {
25
+ const {
26
+ result,
27
+ rerender
28
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSectionState.useRecordingState)('one'), {
29
+ wrapper
30
+ });
31
+ const origRecordingState = result.current.recordingState;
32
+ const origSetRecordingState = result.current.setRecordingState;
33
+ const origRemoveRecordingState = result.current.removeRecordingState;
34
+ rerender();
35
+ expect(result.current.recordingState).toBe(origRecordingState);
36
+ expect(result.current.setRecordingState).toBe(origSetRecordingState);
37
+ expect(result.current.removeRecordingState).toBe(origRemoveRecordingState);
38
+ });
39
+ test('useCachedSection has stable references', () => {
40
+ const {
41
+ result,
42
+ rerender
43
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSectionState.useCachedSection)('one'), {
44
+ wrapper
45
+ });
46
+ const origIsCached = result.current.isCached;
47
+ const origLastUpdated = result.current.lastUpdated;
48
+ const origRemove = result.current.remove;
49
+ const origSyncCachedSections = result.current.syncCachedSections;
50
+ rerender();
51
+ expect(result.current.isCached).toBe(origIsCached);
52
+ expect(result.current.lastUpdated).toBe(origLastUpdated);
53
+ expect(result.current.remove).toBe(origRemove);
54
+ expect(result.current.syncCachedSections).toBe(origSyncCachedSections);
55
+ });
@@ -53,6 +53,34 @@ it('renders in the default state initially', () => {
53
53
  expect(result.current.isCached).toBe(false);
54
54
  expect(result.current.lastUpdated).toBeUndefined();
55
55
  });
56
+ it('has stable references', () => {
57
+ const wrapper = (_ref2) => {
58
+ let {
59
+ children
60
+ } = _ref2;
61
+ return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
62
+ offlineInterface: _testMocks.mockOfflineInterface
63
+ }, children);
64
+ };
65
+
66
+ const {
67
+ result,
68
+ rerender
69
+ } = (0, _reactHooks.renderHook)(() => (0, _cacheableSection.useCacheableSection)('one'), {
70
+ wrapper
71
+ });
72
+ const origRecordingState = result.current.recordingState;
73
+ const origStartRecording = result.current.startRecording;
74
+ const origLastUpdated = result.current.lastUpdated;
75
+ const origIsCached = result.current.isCached;
76
+ const origRemove = result.current.remove;
77
+ rerender();
78
+ expect(result.current.recordingState).toBe(origRecordingState);
79
+ expect(result.current.startRecording).toBe(origStartRecording);
80
+ expect(result.current.lastUpdated).toBe(origLastUpdated);
81
+ expect(result.current.isCached).toBe(origIsCached);
82
+ expect(result.current.remove).toBe(origRemove);
83
+ });
56
84
  it('handles a successful recording', async done => {
57
85
  const [sectionId, timeoutDelay] = ['one', 1234];
58
86
  const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
@@ -62,10 +90,10 @@ it('handles a successful recording', async done => {
62
90
  }])
63
91
  };
64
92
 
65
- const wrapper = (_ref2) => {
93
+ const wrapper = (_ref3) => {
66
94
  let {
67
95
  children
68
- } = _ref2;
96
+ } = _ref3;
69
97
  return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
70
98
  offlineInterface: testOfflineInterface
71
99
  }, children);
@@ -134,10 +162,10 @@ it('handles a recording that encounters an error', async done => {
134
162
  startRecording: _testMocks.errorRecordingMock
135
163
  };
136
164
 
137
- const wrapper = (_ref3) => {
165
+ const wrapper = (_ref4) => {
138
166
  let {
139
167
  children
140
- } = _ref3;
168
+ } = _ref4;
141
169
  return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
142
170
  offlineInterface: testOfflineInterface
143
171
  }, children);
@@ -180,10 +208,10 @@ it('handles an error starting the recording', async () => {
180
208
  startRecording: _testMocks.failedMessageRecordingMock
181
209
  };
182
210
 
183
- const wrapper = (_ref4) => {
211
+ const wrapper = (_ref5) => {
184
212
  let {
185
213
  children
186
- } = _ref4;
214
+ } = _ref5;
187
215
  return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
188
216
  offlineInterface: testOfflineInterface
189
217
  }, children);
@@ -206,10 +234,10 @@ it('handles remove and updates sections', async () => {
206
234
  }]).mockResolvedValueOnce([])
207
235
  };
208
236
 
209
- const wrapper = (_ref5) => {
237
+ const wrapper = (_ref6) => {
210
238
  let {
211
239
  children
212
- } = _ref5;
240
+ } = _ref6;
213
241
  return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
214
242
  offlineInterface: testOfflineInterface
215
243
  }, children);
@@ -242,10 +270,10 @@ it('handles a change in ID', async () => {
242
270
  }])
243
271
  };
244
272
 
245
- const wrapper = (_ref6) => {
273
+ const wrapper = (_ref7) => {
246
274
  let {
247
275
  children
248
- } = _ref6;
276
+ } = _ref7;
249
277
  return /*#__PURE__*/_react.default.createElement(_offlineProvider.OfflineProvider, {
250
278
  offlineInterface: testOfflineInterface
251
279
  }, children);
@@ -11,12 +11,16 @@ exports.useRecordingState = useRecordingState;
11
11
 
12
12
  var _propTypes = _interopRequireDefault(require("prop-types"));
13
13
 
14
- var _react = _interopRequireDefault(require("react"));
14
+ var _react = _interopRequireWildcard(require("react"));
15
15
 
16
16
  var _globalStateService = require("./global-state-service");
17
17
 
18
18
  var _offlineInterface = require("./offline-interface");
19
19
 
20
+ function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
21
+
22
+ function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
23
+
20
24
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
25
 
22
26
  /**
@@ -83,7 +87,7 @@ function CacheableSectionProvider(_ref2) {
83
87
  const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
84
88
  const store = useConst(createCacheableSectionStore); // On load, get sections and add to store
85
89
 
86
- _react.default.useEffect(() => {
90
+ (0, _react.useEffect)(() => {
87
91
  if (offlineInterface) {
88
92
  offlineInterface.getCachedSections().then(sections => {
89
93
  store.mutate(state => ({ ...state,
@@ -92,7 +96,6 @@ function CacheableSectionProvider(_ref2) {
92
96
  });
93
97
  }
94
98
  }, [store, offlineInterface]);
95
-
96
99
  return /*#__PURE__*/_react.default.createElement(_globalStateService.GlobalStateProvider, {
97
100
  store: store
98
101
  }, children);
@@ -110,25 +113,28 @@ CacheableSectionProvider.propTypes = {
110
113
  * @returns {Object} { recordingState: String, setRecordingState: Function, removeRecordingState: Function}
111
114
  */
112
115
  function useRecordingState(id) {
113
- const [recordingState] = (0, _globalStateService.useGlobalState)(state => state.recordingStates[id]);
114
- const setRecordingState = (0, _globalStateService.useGlobalStateMutation)(newState => state => ({ ...state,
116
+ const recordingStateSelector = (0, _react.useCallback)(state => state.recordingStates[id], [id]);
117
+ const [recordingState] = (0, _globalStateService.useGlobalState)(recordingStateSelector);
118
+ const setRecordingStateMutationCreator = (0, _react.useCallback)(newState => state => ({ ...state,
115
119
  recordingStates: { ...state.recordingStates,
116
120
  [id]: newState
117
121
  }
118
- }));
119
- const removeRecordingState = (0, _globalStateService.useGlobalStateMutation)(() => state => {
122
+ }), [id]);
123
+ const setRecordingState = (0, _globalStateService.useGlobalStateMutation)(setRecordingStateMutationCreator);
124
+ const removeRecordingStateMutationCreator = (0, _react.useCallback)(() => state => {
120
125
  const recordingStates = { ...state.recordingStates
121
126
  };
122
127
  delete recordingStates[id];
123
128
  return { ...state,
124
129
  recordingStates
125
130
  };
126
- });
127
- return {
131
+ }, [id]);
132
+ const removeRecordingState = (0, _globalStateService.useGlobalStateMutation)(removeRecordingStateMutationCreator);
133
+ return (0, _react.useMemo)(() => ({
128
134
  recordingState,
129
135
  setRecordingState,
130
136
  removeRecordingState
131
- };
137
+ }), [recordingState, setRecordingState, removeRecordingState]);
132
138
  }
133
139
  /**
134
140
  * Returns a function that syncs cached sections in the global state
@@ -140,13 +146,14 @@ function useRecordingState(id) {
140
146
 
141
147
  function useSyncCachedSections() {
142
148
  const offlineInterface = (0, _offlineInterface.useOfflineInterface)();
143
- const setCachedSections = (0, _globalStateService.useGlobalStateMutation)(cachedSections => state => ({ ...state,
149
+ const setCachedSectionsMutationCreator = (0, _react.useCallback)(cachedSections => state => ({ ...state,
144
150
  cachedSections
145
- }));
146
- return async function syncCachedSections() {
151
+ }), []);
152
+ const setCachedSections = (0, _globalStateService.useGlobalStateMutation)(setCachedSectionsMutationCreator);
153
+ return (0, _react.useCallback)(async () => {
147
154
  const sections = await offlineInterface.getCachedSections();
148
155
  setCachedSections(getSectionsById(sections));
149
- };
156
+ }, [offlineInterface, setCachedSections]);
150
157
  }
151
158
 
152
159
  /**
@@ -166,7 +173,7 @@ function useCachedSections() {
166
173
  * deleted, or `false` if asection with the specified ID does not exist.
167
174
  */
168
175
 
169
- async function removeById(id) {
176
+ const removeById = (0, _react.useCallback)(async id => {
170
177
  const success = await offlineInterface.removeSection(id);
171
178
 
172
179
  if (success) {
@@ -174,13 +181,12 @@ function useCachedSections() {
174
181
  }
175
182
 
176
183
  return success;
177
- }
178
-
179
- return {
184
+ }, [offlineInterface, syncCachedSections]);
185
+ return (0, _react.useMemo)(() => ({
180
186
  cachedSections,
181
187
  removeById,
182
188
  syncCachedSections
183
- };
189
+ }), [cachedSections, removeById, syncCachedSections]);
184
190
  }
185
191
 
186
192
  /**
@@ -203,7 +209,7 @@ function useCachedSection(id) {
203
209
  * section with the specified ID does not exist.
204
210
  */
205
211
 
206
- async function remove() {
212
+ const remove = (0, _react.useCallback)(async () => {
207
213
  const success = await offlineInterface.removeSection(id);
208
214
 
209
215
  if (success) {
@@ -211,12 +217,11 @@ function useCachedSection(id) {
211
217
  }
212
218
 
213
219
  return success;
214
- }
215
-
216
- return {
220
+ }, [offlineInterface, id, syncCachedSections]);
221
+ return (0, _react.useMemo)(() => ({
217
222
  lastUpdated,
218
223
  isCached: !!lastUpdated,
219
224
  remove,
220
225
  syncCachedSections
221
- };
226
+ }), [lastUpdated, remove, syncCachedSections]);
222
227
  }
@@ -63,7 +63,18 @@ function useCacheableSection(id) {
63
63
  };
64
64
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
65
65
 
66
- function startRecording() {
66
+ const onRecordingStarted = (0, _react.useCallback)(() => {
67
+ setRecordingState(recordingStates.recording);
68
+ }, [setRecordingState]);
69
+ const onRecordingCompleted = (0, _react.useCallback)(() => {
70
+ setRecordingState(recordingStates.default);
71
+ syncCachedSections();
72
+ }, [setRecordingState, syncCachedSections]);
73
+ const onRecordingError = (0, _react.useCallback)(error => {
74
+ console.error('Error during recording:', error);
75
+ setRecordingState(recordingStates.error);
76
+ }, [setRecordingState]);
77
+ const startRecording = (0, _react.useCallback)(function () {
67
78
  let {
68
79
  recordingTimeoutDelay = 1000,
69
80
  onStarted,
@@ -89,31 +100,16 @@ function useCacheableSection(id) {
89
100
  onError && onError(error);
90
101
  }
91
102
  }).then(() => setRecordingState(recordingStates.pending));
92
- }
93
-
94
- function onRecordingStarted() {
95
- setRecordingState(recordingStates.recording);
96
- }
97
-
98
- function onRecordingCompleted() {
99
- setRecordingState(recordingStates.default);
100
- syncCachedSections();
101
- }
102
-
103
- function onRecordingError(error) {
104
- console.error('Error during recording:', error);
105
- setRecordingState(recordingStates.error);
106
- } // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
103
+ }, [id, offlineInterface, onRecordingCompleted, onRecordingError, onRecordingStarted, setRecordingState]); // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
107
104
  // but provided through this hook for convenience
108
105
 
109
-
110
- return {
106
+ return (0, _react.useMemo)(() => ({
111
107
  recordingState,
112
108
  startRecording,
113
109
  lastUpdated,
114
110
  isCached,
115
111
  remove
116
- };
112
+ }), [recordingState, startRecording, lastUpdated, isCached, remove]);
117
113
  }
118
114
 
119
115
  /**
@@ -74,12 +74,17 @@ const useGlobalState = function () {
74
74
  (0, _react.useEffect)(() => {
75
75
  // NEW: deep equality check before updating
76
76
  const callback = state => {
77
- const newSelectedState = selector(state); // Second condition handles case where a selected object gets
78
- // deleted, but state does not update
77
+ const newSelectedState = selector(state); // Use this form to avoid having `selectedState` as a dep in here
79
78
 
80
- if (!(0, _isEqual.default)(selectedState, newSelectedState) || selectedState === undefined) {
81
- setSelectedState(newSelectedState);
82
- }
79
+ setSelectedState(currentSelectedState => {
80
+ // Second condition handles case where a selected object gets
81
+ // deleted, but state does not update
82
+ if (!(0, _isEqual.default)(currentSelectedState, newSelectedState) || currentSelectedState === undefined) {
83
+ return newSelectedState;
84
+ }
85
+
86
+ return currentSelectedState;
87
+ });
83
88
  };
84
89
 
85
90
  store.subscribe(callback); // Make sure to update state when selector changes
@@ -87,9 +92,7 @@ const useGlobalState = function () {
87
92
  callback(store.getState());
88
93
  return () => store.unsubscribe(callback);
89
94
  }, [store, selector]);
90
- /* eslint-disable-line react-hooks/exhaustive-deps */
91
-
92
- return [selectedState, store.mutate];
95
+ return (0, _react.useMemo)(() => [selectedState, store.mutate], [selectedState, store.mutate]);
93
96
  };
94
97
 
95
98
  exports.useGlobalState = useGlobalState;
@@ -0,0 +1,47 @@
1
+ import { renderHook } from '@testing-library/react-hooks';
2
+ import React from 'react';
3
+ import { mockOfflineInterface } from '../../utils/test-mocks';
4
+ import { useCachedSection, useRecordingState } from '../cacheable-section-state';
5
+ import { OfflineProvider } from '../offline-provider';
6
+
7
+ const wrapper = (_ref) => {
8
+ let {
9
+ children
10
+ } = _ref;
11
+ return /*#__PURE__*/React.createElement(OfflineProvider, {
12
+ offlineInterface: mockOfflineInterface
13
+ }, children);
14
+ };
15
+
16
+ test('useRecordingState has stable references', () => {
17
+ const {
18
+ result,
19
+ rerender
20
+ } = renderHook(() => useRecordingState('one'), {
21
+ wrapper
22
+ });
23
+ const origRecordingState = result.current.recordingState;
24
+ const origSetRecordingState = result.current.setRecordingState;
25
+ const origRemoveRecordingState = result.current.removeRecordingState;
26
+ rerender();
27
+ expect(result.current.recordingState).toBe(origRecordingState);
28
+ expect(result.current.setRecordingState).toBe(origSetRecordingState);
29
+ expect(result.current.removeRecordingState).toBe(origRemoveRecordingState);
30
+ });
31
+ test('useCachedSection has stable references', () => {
32
+ const {
33
+ result,
34
+ rerender
35
+ } = renderHook(() => useCachedSection('one'), {
36
+ wrapper
37
+ });
38
+ const origIsCached = result.current.isCached;
39
+ const origLastUpdated = result.current.lastUpdated;
40
+ const origRemove = result.current.remove;
41
+ const origSyncCachedSections = result.current.syncCachedSections;
42
+ rerender();
43
+ expect(result.current.isCached).toBe(origIsCached);
44
+ expect(result.current.lastUpdated).toBe(origLastUpdated);
45
+ expect(result.current.remove).toBe(origRemove);
46
+ expect(result.current.syncCachedSections).toBe(origSyncCachedSections);
47
+ });
@@ -44,6 +44,34 @@ it('renders in the default state initially', () => {
44
44
  expect(result.current.isCached).toBe(false);
45
45
  expect(result.current.lastUpdated).toBeUndefined();
46
46
  });
47
+ it('has stable references', () => {
48
+ const wrapper = (_ref2) => {
49
+ let {
50
+ children
51
+ } = _ref2;
52
+ return /*#__PURE__*/React.createElement(OfflineProvider, {
53
+ offlineInterface: mockOfflineInterface
54
+ }, children);
55
+ };
56
+
57
+ const {
58
+ result,
59
+ rerender
60
+ } = renderHook(() => useCacheableSection('one'), {
61
+ wrapper
62
+ });
63
+ const origRecordingState = result.current.recordingState;
64
+ const origStartRecording = result.current.startRecording;
65
+ const origLastUpdated = result.current.lastUpdated;
66
+ const origIsCached = result.current.isCached;
67
+ const origRemove = result.current.remove;
68
+ rerender();
69
+ expect(result.current.recordingState).toBe(origRecordingState);
70
+ expect(result.current.startRecording).toBe(origStartRecording);
71
+ expect(result.current.lastUpdated).toBe(origLastUpdated);
72
+ expect(result.current.isCached).toBe(origIsCached);
73
+ expect(result.current.remove).toBe(origRemove);
74
+ });
47
75
  it('handles a successful recording', async done => {
48
76
  const [sectionId, timeoutDelay] = ['one', 1234];
49
77
  const testOfflineInterface = { ...mockOfflineInterface,
@@ -53,10 +81,10 @@ it('handles a successful recording', async done => {
53
81
  }])
54
82
  };
55
83
 
56
- const wrapper = (_ref2) => {
84
+ const wrapper = (_ref3) => {
57
85
  let {
58
86
  children
59
- } = _ref2;
87
+ } = _ref3;
60
88
  return /*#__PURE__*/React.createElement(OfflineProvider, {
61
89
  offlineInterface: testOfflineInterface
62
90
  }, children);
@@ -125,10 +153,10 @@ it('handles a recording that encounters an error', async done => {
125
153
  startRecording: errorRecordingMock
126
154
  };
127
155
 
128
- const wrapper = (_ref3) => {
156
+ const wrapper = (_ref4) => {
129
157
  let {
130
158
  children
131
- } = _ref3;
159
+ } = _ref4;
132
160
  return /*#__PURE__*/React.createElement(OfflineProvider, {
133
161
  offlineInterface: testOfflineInterface
134
162
  }, children);
@@ -171,10 +199,10 @@ it('handles an error starting the recording', async () => {
171
199
  startRecording: failedMessageRecordingMock
172
200
  };
173
201
 
174
- const wrapper = (_ref4) => {
202
+ const wrapper = (_ref5) => {
175
203
  let {
176
204
  children
177
- } = _ref4;
205
+ } = _ref5;
178
206
  return /*#__PURE__*/React.createElement(OfflineProvider, {
179
207
  offlineInterface: testOfflineInterface
180
208
  }, children);
@@ -197,10 +225,10 @@ it('handles remove and updates sections', async () => {
197
225
  }]).mockResolvedValueOnce([])
198
226
  };
199
227
 
200
- const wrapper = (_ref5) => {
228
+ const wrapper = (_ref6) => {
201
229
  let {
202
230
  children
203
- } = _ref5;
231
+ } = _ref6;
204
232
  return /*#__PURE__*/React.createElement(OfflineProvider, {
205
233
  offlineInterface: testOfflineInterface
206
234
  }, children);
@@ -233,10 +261,10 @@ it('handles a change in ID', async () => {
233
261
  }])
234
262
  };
235
263
 
236
- const wrapper = (_ref6) => {
264
+ const wrapper = (_ref7) => {
237
265
  let {
238
266
  children
239
- } = _ref6;
267
+ } = _ref7;
240
268
  return /*#__PURE__*/React.createElement(OfflineProvider, {
241
269
  offlineInterface: testOfflineInterface
242
270
  }, children);
@@ -1,5 +1,5 @@
1
1
  import PropTypes from 'prop-types';
2
- import React from 'react';
2
+ import React, { useEffect, useCallback, useMemo } from 'react';
3
3
  import { createStore, useGlobalState, useGlobalStateMutation, GlobalStateProvider } from './global-state-service';
4
4
  import { useOfflineInterface } from './offline-interface'; // Functions in here use the global state service to manage cacheable section
5
5
  // state in a performant way
@@ -67,7 +67,7 @@ export function CacheableSectionProvider(_ref2) {
67
67
  const offlineInterface = useOfflineInterface();
68
68
  const store = useConst(createCacheableSectionStore); // On load, get sections and add to store
69
69
 
70
- React.useEffect(() => {
70
+ useEffect(() => {
71
71
  if (offlineInterface) {
72
72
  offlineInterface.getCachedSections().then(sections => {
73
73
  store.mutate(state => ({ ...state,
@@ -92,25 +92,28 @@ CacheableSectionProvider.propTypes = {
92
92
  * @returns {Object} { recordingState: String, setRecordingState: Function, removeRecordingState: Function}
93
93
  */
94
94
  export function useRecordingState(id) {
95
- const [recordingState] = useGlobalState(state => state.recordingStates[id]);
96
- const setRecordingState = useGlobalStateMutation(newState => state => ({ ...state,
95
+ const recordingStateSelector = useCallback(state => state.recordingStates[id], [id]);
96
+ const [recordingState] = useGlobalState(recordingStateSelector);
97
+ const setRecordingStateMutationCreator = useCallback(newState => state => ({ ...state,
97
98
  recordingStates: { ...state.recordingStates,
98
99
  [id]: newState
99
100
  }
100
- }));
101
- const removeRecordingState = useGlobalStateMutation(() => state => {
101
+ }), [id]);
102
+ const setRecordingState = useGlobalStateMutation(setRecordingStateMutationCreator);
103
+ const removeRecordingStateMutationCreator = useCallback(() => state => {
102
104
  const recordingStates = { ...state.recordingStates
103
105
  };
104
106
  delete recordingStates[id];
105
107
  return { ...state,
106
108
  recordingStates
107
109
  };
108
- });
109
- return {
110
+ }, [id]);
111
+ const removeRecordingState = useGlobalStateMutation(removeRecordingStateMutationCreator);
112
+ return useMemo(() => ({
110
113
  recordingState,
111
114
  setRecordingState,
112
115
  removeRecordingState
113
- };
116
+ }), [recordingState, setRecordingState, removeRecordingState]);
114
117
  }
115
118
  /**
116
119
  * Returns a function that syncs cached sections in the global state
@@ -121,13 +124,14 @@ export function useRecordingState(id) {
121
124
 
122
125
  function useSyncCachedSections() {
123
126
  const offlineInterface = useOfflineInterface();
124
- const setCachedSections = useGlobalStateMutation(cachedSections => state => ({ ...state,
127
+ const setCachedSectionsMutationCreator = useCallback(cachedSections => state => ({ ...state,
125
128
  cachedSections
126
- }));
127
- return async function syncCachedSections() {
129
+ }), []);
130
+ const setCachedSections = useGlobalStateMutation(setCachedSectionsMutationCreator);
131
+ return useCallback(async () => {
128
132
  const sections = await offlineInterface.getCachedSections();
129
133
  setCachedSections(getSectionsById(sections));
130
- };
134
+ }, [offlineInterface, setCachedSections]);
131
135
  }
132
136
 
133
137
  /**
@@ -147,7 +151,7 @@ export function useCachedSections() {
147
151
  * deleted, or `false` if asection with the specified ID does not exist.
148
152
  */
149
153
 
150
- async function removeById(id) {
154
+ const removeById = useCallback(async id => {
151
155
  const success = await offlineInterface.removeSection(id);
152
156
 
153
157
  if (success) {
@@ -155,13 +159,12 @@ export function useCachedSections() {
155
159
  }
156
160
 
157
161
  return success;
158
- }
159
-
160
- return {
162
+ }, [offlineInterface, syncCachedSections]);
163
+ return useMemo(() => ({
161
164
  cachedSections,
162
165
  removeById,
163
166
  syncCachedSections
164
- };
167
+ }), [cachedSections, removeById, syncCachedSections]);
165
168
  }
166
169
 
167
170
  /**
@@ -184,7 +187,7 @@ export function useCachedSection(id) {
184
187
  * section with the specified ID does not exist.
185
188
  */
186
189
 
187
- async function remove() {
190
+ const remove = useCallback(async () => {
188
191
  const success = await offlineInterface.removeSection(id);
189
192
 
190
193
  if (success) {
@@ -192,12 +195,11 @@ export function useCachedSection(id) {
192
195
  }
193
196
 
194
197
  return success;
195
- }
196
-
197
- return {
198
+ }, [offlineInterface, id, syncCachedSections]);
199
+ return useMemo(() => ({
198
200
  lastUpdated,
199
201
  isCached: !!lastUpdated,
200
202
  remove,
201
203
  syncCachedSections
202
- };
204
+ }), [lastUpdated, remove, syncCachedSections]);
203
205
  }
@@ -1,5 +1,5 @@
1
1
  import PropTypes from 'prop-types';
2
- import React, { useEffect } from 'react';
2
+ import React, { useCallback, useEffect, useMemo } from 'react';
3
3
  import { useRecordingState, useCachedSection } from './cacheable-section-state';
4
4
  import { useOfflineInterface } from './offline-interface';
5
5
  const recordingStates = {
@@ -45,7 +45,18 @@ export function useCacheableSection(id) {
45
45
  };
46
46
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
47
47
 
48
- function startRecording() {
48
+ const onRecordingStarted = useCallback(() => {
49
+ setRecordingState(recordingStates.recording);
50
+ }, [setRecordingState]);
51
+ const onRecordingCompleted = useCallback(() => {
52
+ setRecordingState(recordingStates.default);
53
+ syncCachedSections();
54
+ }, [setRecordingState, syncCachedSections]);
55
+ const onRecordingError = useCallback(error => {
56
+ console.error('Error during recording:', error);
57
+ setRecordingState(recordingStates.error);
58
+ }, [setRecordingState]);
59
+ const startRecording = useCallback(function () {
49
60
  let {
50
61
  recordingTimeoutDelay = 1000,
51
62
  onStarted,
@@ -71,31 +82,16 @@ export function useCacheableSection(id) {
71
82
  onError && onError(error);
72
83
  }
73
84
  }).then(() => setRecordingState(recordingStates.pending));
74
- }
75
-
76
- function onRecordingStarted() {
77
- setRecordingState(recordingStates.recording);
78
- }
79
-
80
- function onRecordingCompleted() {
81
- setRecordingState(recordingStates.default);
82
- syncCachedSections();
83
- }
84
-
85
- function onRecordingError(error) {
86
- console.error('Error during recording:', error);
87
- setRecordingState(recordingStates.error);
88
- } // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
85
+ }, [id, offlineInterface, onRecordingCompleted, onRecordingError, onRecordingStarted, setRecordingState]); // isCached, lastUpdated, remove: _could_ be accessed by useCachedSection,
89
86
  // but provided through this hook for convenience
90
87
 
91
-
92
- return {
88
+ return useMemo(() => ({
93
89
  recordingState,
94
90
  startRecording,
95
91
  lastUpdated,
96
92
  isCached,
97
93
  remove
98
- };
94
+ }), [recordingState, startRecording, lastUpdated, isCached, remove]);
99
95
  }
100
96
 
101
97
  /**
@@ -1,6 +1,6 @@
1
1
  import isEqual from 'lodash/isEqual';
2
2
  import PropTypes from 'prop-types';
3
- import React, { useEffect, useCallback, useContext, useState } from 'react';
3
+ import React, { useEffect, useCallback, useContext, useState, useMemo } from 'react';
4
4
 
5
5
  // This file creates a redux-like state management service using React context
6
6
  // that minimizes unnecessary rerenders that consume the context.
@@ -52,12 +52,17 @@ export const useGlobalState = function () {
52
52
  useEffect(() => {
53
53
  // NEW: deep equality check before updating
54
54
  const callback = state => {
55
- const newSelectedState = selector(state); // Second condition handles case where a selected object gets
56
- // deleted, but state does not update
55
+ const newSelectedState = selector(state); // Use this form to avoid having `selectedState` as a dep in here
57
56
 
58
- if (!isEqual(selectedState, newSelectedState) || selectedState === undefined) {
59
- setSelectedState(newSelectedState);
60
- }
57
+ setSelectedState(currentSelectedState => {
58
+ // Second condition handles case where a selected object gets
59
+ // deleted, but state does not update
60
+ if (!isEqual(currentSelectedState, newSelectedState) || currentSelectedState === undefined) {
61
+ return newSelectedState;
62
+ }
63
+
64
+ return currentSelectedState;
65
+ });
61
66
  };
62
67
 
63
68
  store.subscribe(callback); // Make sure to update state when selector changes
@@ -65,9 +70,7 @@ export const useGlobalState = function () {
65
70
  callback(store.getState());
66
71
  return () => store.unsubscribe(callback);
67
72
  }, [store, selector]);
68
- /* eslint-disable-line react-hooks/exhaustive-deps */
69
-
70
- return [selectedState, store.mutate];
73
+ return useMemo(() => [selectedState, store.mutate], [selectedState, store.mutate]);
71
74
  };
72
75
  export function useGlobalStateMutation(mutationCreator) {
73
76
  const store = useGlobalStateStore();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dhis2/app-service-offline",
3
3
  "description": "A runtime service for online/offline detection and offline caching",
4
- "version": "3.11.0-alpha.1",
4
+ "version": "3.11.0",
5
5
  "main": "./build/cjs/index.js",
6
6
  "module": "./build/es/index.js",
7
7
  "types": "build/types/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  "coverage": "yarn test --coverage"
34
34
  },
35
35
  "peerDependencies": {
36
- "@dhis2/app-service-config": "3.11.0-alpha.1",
36
+ "@dhis2/app-service-config": "3.11.0",
37
37
  "prop-types": "^15.7.2",
38
38
  "react": "^16.8.6",
39
39
  "react-dom": "^16.8.6"