Skip to content

Commit de83e25

Browse files
LiquidSeanrgommezz
authored andcommitted
Feat: Option to pass in custom comparison function (#208)
1 parent b4b1bcb commit de83e25

File tree

6 files changed

+127
-29
lines changed

6 files changed

+127
-29
lines changed

README.md

+30-2
Original file line numberDiff line numberDiff line change
@@ -440,11 +440,39 @@ if(action.type === offlineActionTypes.FETCH_OFFLINE_MODE) // do something in you
440440
SnackBars, Dialog, Popups, or simple informative text are good means of conveying to the user that the operation failed due to lack of internet connection.
441441

442442
### Offline Queue
443-
A queue system to store actions that failed due to lack of connectivity. It works for both plain object actions and thunks.
444-
It allows you to:
443+
A queue system to store actions that failed due to lack of connectivity. It works for both plain object actions and thunks. It allows you to:
444+
445445
- Re-dispatch the action/thunk as soon as the internet connection is back online again
446446
- Dismiss the action from the queue based on a different action dispatched (i.e. navigating to a different screen, the fetch action is no longer relevant)
447447

448+
#### Managing duplicate actions
449+
If a similar action already exists on the queue, we remove it and push it again to the end, so it has an overriding effect.
450+
The default criteria to detect duplicates is by using `lodash.isEqual` for plain actions and `thunk.toString()` for thunks/functions. However, you can customise the comparison function to acommodate it to your needs. For that, you need to use the factory version for your network reducer.
451+
452+
```js
453+
// configureStore.js
454+
import { createStore, combineReducers } from 'redux'
455+
import { createReducer as createNetworkReducer } from 'react-native-offline';
456+
import { comparisonFn } from './utils';
457+
458+
const rootReducer = combineReducers({
459+
// ... your other reducers here ...
460+
createNetworkReducer(comparisonFn),
461+
});
462+
463+
const store = createStore(rootReducer);
464+
export default store;
465+
```
466+
467+
The comparison function receives the action dispatched when offline and the current `actionQueue`. The result of the function will be either `undefined`, meaning no match found, or the action that matches the passed in action. So basically, you need to return the upcoming action if you wish to replace an existing one. An example of how to use it can be found [here](https://github.com/rgommezz/react-native-offline/blob/master/test/reducer.test.js#L121).
468+
469+
```js
470+
function comparisonFn(
471+
action: ReduxAction | ReduxThunk,
472+
actionQueue: Array<ReduxAction | ReduxThunk>,
473+
): ?(ReduxAction | ReduxThunk)
474+
```
475+
448476
#### Plain Objects
449477
In order to configure your PO actions to interact with the offline queue you need to use the `meta` property in your actions, following [flux standard actions convention](https://github.com/acdlite/flux-standard-action#meta). They need to adhere to the below API:
450478

src/index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ module.exports = {
99
return require('./components/NetworkConsumer').default;
1010
},
1111
get reducer() {
12-
return require('./redux/reducer').default;
12+
return require('./redux/createReducer').default();
13+
},
14+
get createReducer() {
15+
return require('./redux/createReducer').default;
1316
},
1417
get createNetworkMiddleware() {
1518
return require('./redux/createNetworkMiddleware').default;

src/redux/reducer.js renamed to src/redux/createReducer.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const initialState = {
1818
function handleOfflineAction(
1919
state: NetworkState,
2020
{ payload: { prevAction, prevThunk }, meta }: FluxActionWithPreviousIntent,
21+
comparisonFn: Function,
2122
): NetworkState {
2223
const isActionToRetry =
2324
typeof prevAction === 'object' && get(meta, 'retry') === true;
@@ -32,7 +33,7 @@ function handleOfflineAction(
3233
typeof actionToLookUp === 'object'
3334
? { ...actionToLookUp, meta }
3435
: actionToLookUp;
35-
const similarActionQueued = getSimilarActionInQueue(
36+
const similarActionQueued = comparisonFn(
3637
actionWithMetaData,
3738
state.actionQueue,
3839
);
@@ -80,26 +81,26 @@ function handleDismissActionsFromQueue(
8081
};
8182
}
8283

83-
export default function(
84+
export default (comparisonFn: Function = getSimilarActionInQueue) => (
8485
state: NetworkState = initialState,
8586
action: *,
86-
): NetworkState {
87+
): NetworkState => {
8788
switch (action.type) {
8889
case actionTypes.CONNECTION_CHANGE:
8990
return {
9091
...state,
9192
isConnected: action.payload,
9293
};
9394
case actionTypes.FETCH_OFFLINE_MODE:
94-
return handleOfflineAction(state, action);
95+
return handleOfflineAction(state, action, comparisonFn);
9596
case actionTypes.REMOVE_FROM_ACTION_QUEUE:
9697
return handleRemoveActionFromQueue(state, action.payload);
9798
case actionTypes.DISMISS_ACTIONS_FROM_QUEUE:
9899
return handleDismissActionsFromQueue(state, action.payload);
99100
default:
100101
return state;
101102
}
102-
}
103+
};
103104

104105
export function networkSelector(state: { network: NetworkState }) {
105106
return state.network;

src/redux/sagas.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { put, select, call, take, cancelled, fork } from 'redux-saga/effects';
44
import { eventChannel } from 'redux-saga';
55
import { AppState, Platform } from 'react-native';
66
import NetInfo from '@react-native-community/netinfo';
7-
import { networkSelector } from './reducer';
7+
import { networkSelector } from './createReducer';
88
import checkInternetAccess from '../utils/checkInternetAccess';
99
import { connectionChange } from './actionCreators';
1010
import type { HTTPMethod } from '../types';

test/reducer.test.js

+85-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
/* eslint flowtype/require-parameter-type: 0 */
2-
import reducer, { initialState, networkSelector } from '../src/redux/reducer';
2+
import { isEqual } from 'lodash';
3+
import createReducer, {
4+
initialState,
5+
networkSelector,
6+
} from '../src/redux/createReducer';
37
import * as actionCreators from '../src/redux/actionCreators';
8+
import getSimilarActionInQueue from '../src/utils/getSimilarActionInQueue';
9+
10+
const networkReducer = createReducer();
411

512
const getState = (isConnected = false, ...actionQueue) => ({
613
isConnected,
@@ -38,22 +45,22 @@ const prevActionToRetry1WithDifferentPayload = {
3845

3946
describe('unknown action type', () => {
4047
it('returns prevState on initialization', () => {
41-
expect(reducer(undefined, { type: 'ACTION_I_DONT_CARE' })).toEqual(
48+
expect(networkReducer(undefined, { type: 'ACTION_I_DONT_CARE' })).toEqual(
4249
initialState,
4350
);
4451
});
4552

4653
it('returns prevState if the action is not handled', () => {
4754
expect(
48-
reducer(initialState, { type: 'ANOTHER_ACTION_I_DONT_CARE' }),
55+
networkReducer(initialState, { type: 'ANOTHER_ACTION_I_DONT_CARE' }),
4956
).toEqual(initialState);
5057
});
5158
});
5259

5360
describe('CONNECTION_CHANGE action type', () => {
5461
it('changes isConnected state properly', () => {
5562
const mockAction = actionCreators.connectionChange(false);
56-
expect(reducer(initialState, mockAction)).toEqual({
63+
expect(networkReducer(initialState, mockAction)).toEqual({
5764
isConnected: false,
5865
actionQueue: [],
5966
});
@@ -82,8 +89,8 @@ describe('OFFLINE_ACTION action type', () => {
8289
const action = actionCreators.fetchOfflineMode(prevAction);
8390
const anotherAction = actionCreators.fetchOfflineMode(anotherPrevAction);
8491

85-
expect(reducer(initialState, action)).toEqual(initialState);
86-
expect(reducer(initialState, anotherAction)).toEqual(initialState);
92+
expect(networkReducer(initialState, action)).toEqual(initialState);
93+
expect(networkReducer(initialState, anotherAction)).toEqual(initialState);
8794
});
8895
});
8996

@@ -92,24 +99,81 @@ describe('OFFLINE_ACTION action type', () => {
9299
it('actions are pushed into the queue in order of arrival', () => {
93100
const preAction = actionCreators.connectionChange(false);
94101
const action1 = actionCreators.fetchOfflineMode(prevActionToRetry1);
95-
const prevState = reducer(initialState, preAction);
102+
const prevState = networkReducer(initialState, preAction);
96103

97-
let nextState = reducer(prevState, action1);
104+
let nextState = networkReducer(prevState, action1);
98105

99106
expect(nextState).toEqual({
100107
isConnected: false,
101108
actionQueue: [prevActionToRetry1],
102109
});
103110

104111
const action2 = actionCreators.fetchOfflineMode(prevActionToRetry2);
105-
nextState = reducer(nextState, action2);
112+
nextState = networkReducer(nextState, action2);
106113

107114
expect(nextState).toEqual(
108115
getState(false, prevActionToRetry1, prevActionToRetry2),
109116
);
110117
});
111118
});
112119

120+
describe('thunks that are the same with custom comparison function', () => {
121+
function comparisonFn(action, actionQueue) {
122+
if (typeof action === 'object') {
123+
return actionQueue.find(queued => isEqual(queued, action));
124+
}
125+
if (typeof action === 'function') {
126+
return actionQueue.find(
127+
queued =>
128+
action.meta.name === queued.meta.name &&
129+
action.meta.args.id === queued.meta.args.id,
130+
);
131+
}
132+
return undefined;
133+
}
134+
135+
const thunkFactory = (id, name, age) => {
136+
function thunk(dispatch) {
137+
dispatch({ type: 'UPDATE_DATA_REQUEST', payload: { id, name, age } });
138+
}
139+
thunk.meta = {
140+
args: { id, name, age },
141+
retry: true,
142+
};
143+
return thunk;
144+
};
145+
146+
it(`should add thunks if function is same but thunks are modifying different items`, () => {
147+
const prevState = getState(false, thunkFactory(1, 'Bilbo', 55));
148+
const thunk = actionCreators.fetchOfflineMode(
149+
thunkFactory(2, 'Link', 54),
150+
);
151+
152+
expect(getSimilarActionInQueue(thunk, prevState.actionQueue)).toEqual(
153+
prevState.actionQueue[0].action,
154+
);
155+
156+
const nextState = createReducer(comparisonFn)(prevState, thunk);
157+
158+
expect(nextState.actionQueue).toHaveLength(2);
159+
});
160+
161+
it(`should replace a thunk if thunk already exists to modify same item`, () => {
162+
const prevState = getState(false, thunkFactory(1, 'Bilbo', 55));
163+
const thunk = actionCreators.fetchOfflineMode(
164+
thunkFactory(1, 'Bilbo', 65),
165+
);
166+
167+
expect(getSimilarActionInQueue(thunk, prevState.actionQueue)).toEqual(
168+
prevState.actionQueue[0].action,
169+
);
170+
171+
const nextState = createReducer(comparisonFn)(prevState, thunk);
172+
173+
expect(nextState.actionQueue).toHaveLength(1);
174+
});
175+
});
176+
113177
describe('actions with the same type', () => {
114178
it(`should remove the action and add it back at the end of the queue
115179
if the action has the same payload`, () => {
@@ -120,7 +184,7 @@ describe('OFFLINE_ACTION action type', () => {
120184
);
121185
const action = actionCreators.fetchOfflineMode(prevActionToRetry1);
122186

123-
const nextState = reducer(prevState, action);
187+
const nextState = networkReducer(prevState, action);
124188
expect(nextState).toEqual(
125189
getState(false, prevActionToRetry2, prevActionToRetry1),
126190
);
@@ -136,7 +200,7 @@ describe('OFFLINE_ACTION action type', () => {
136200
prevActionToRetry1WithDifferentPayload,
137201
);
138202

139-
expect(reducer(prevState, action)).toEqual(
203+
expect(networkReducer(prevState, action)).toEqual(
140204
getState(
141205
false,
142206
prevActionToRetry2,
@@ -162,7 +226,7 @@ describe('REMOVE_ACTION_FROM_QUEUE action type', () => {
162226
...prevActionToRetry2,
163227
});
164228

165-
expect(reducer(prevState, action)).toEqual(
229+
expect(networkReducer(prevState, action)).toEqual(
166230
getState(
167231
false,
168232
prevActionToRetry1,
@@ -181,7 +245,7 @@ describe('thunks', () => {
181245
describe('action with meta.retry !== true', () => {
182246
it('should NOT add the action to the queue', () => {
183247
const action = actionCreators.fetchOfflineMode(fetchThunk);
184-
expect(reducer(initialState, action)).toEqual(initialState);
248+
expect(networkReducer(initialState, action)).toEqual(initialState);
185249
});
186250
});
187251

@@ -193,7 +257,9 @@ describe('thunks', () => {
193257
};
194258
const action = actionCreators.fetchOfflineMode(fetchThunk);
195259

196-
expect(reducer(prevState, action)).toEqual(getState(false, fetchThunk));
260+
expect(networkReducer(prevState, action)).toEqual(
261+
getState(false, fetchThunk),
262+
);
197263
});
198264

199265
it(`should remove the thunk and add it back at the end of the queue
@@ -212,7 +278,7 @@ describe('thunks', () => {
212278
retry: true,
213279
};
214280
const action = actionCreators.fetchOfflineMode(similarThunk);
215-
const nextState = reducer(prevState, action);
281+
const nextState = networkReducer(prevState, action);
216282

217283
expect(nextState).toEqual(getState(false, similarThunk));
218284
});
@@ -224,7 +290,7 @@ describe('thunks', () => {
224290
const prevState = getState(false, fetchThunk);
225291
const action = actionCreators.removeActionFromQueue(fetchThunk);
226292

227-
expect(reducer(prevState, action)).toEqual(getState(false));
293+
expect(networkReducer(prevState, action)).toEqual(getState(false));
228294
});
229295
});
230296
});
@@ -269,7 +335,7 @@ describe('dismiss feature', () => {
269335
);
270336
const action = actionCreators.dismissActionsFromQueue('NAVIGATE_BACK');
271337

272-
expect(reducer(prevState, action)).toEqual(
338+
expect(networkReducer(prevState, action)).toEqual(
273339
getState(false, actionEnqueued2, actionEnqueued3),
274340
);
275341
});
@@ -283,7 +349,7 @@ describe('dismiss feature', () => {
283349
);
284350
const action = actionCreators.dismissActionsFromQueue('NAVIGATE_TO_LOGIN');
285351

286-
expect(reducer(prevState, action)).toEqual(
352+
expect(networkReducer(prevState, action)).toEqual(
287353
getState(false, actionEnqueued3),
288354
);
289355
});
@@ -297,7 +363,7 @@ describe('dismiss feature', () => {
297363
);
298364
const action = actionCreators.dismissActionsFromQueue('NAVIGATE_AWAY');
299365

300-
expect(reducer(prevState, action)).toEqual(
366+
expect(networkReducer(prevState, action)).toEqual(
301367
getState(false, actionEnqueued1, actionEnqueued2, actionEnqueued3),
302368
);
303369
});

test/sagas.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
DEFAULT_PING_SERVER_URL,
2020
DEFAULT_TIMEOUT,
2121
} from '../src/utils/constants';
22-
import { networkSelector } from '../src/redux/reducer';
22+
import { networkSelector } from '../src/redux/createReducer';
2323
import checkInternetAccess from '../src/utils/checkInternetAccess';
2424

2525
const args = {

0 commit comments

Comments
 (0)