Skip to content

Commit 49c1d9e

Browse files
committed
Resolves a merge conflict.
Rebase. This commit includes the following commits: feat(various): Created a hook out of the NetworkDetector component feat(various): Refactored the NetworkDetector component so that it shows a barrier if the user goes offline; Moved the use of the useNetworkDetection hook to the NetworkDetector component feat(notificationApi): Made the notificationApi file consistent with the other api files when it comes to refetching data feat(store): Data is now being refetched when the user's connection is restored feat(useReducerInfiniteLoading): In the middle of creating a version of useInfiniteLoading that uses the reducer from useLiveNotifications feat(various): Added the useNewLiveNotifications hook for testing purposes and resolves some issues feat(useReducerInfiniteLoading): The user's unread notifications are now cleared before refetching in the internet reconnection scenario. fix(useReducerInfiniteLoading): Resolved the issue that was causing the notifications to be cleared when more notifications are fetched fix(various): Removed the clearing that was happening when the notification dropdown was closed. This is already handled by the useReducerInfiniteLoading hook. refactor(various): Renamed notification to item in the useReducerInfiniteLoading hook refactor(useReducerInfiniteLoading): Implemented a better solution for the duplicate item issue when the user's internet connection is restored refactor(useReducerInfiniteLoading): Removed some unnecessary code and added some console logs in order to help identify why this hook keeps executing fix(useReducerInfiniteLoading): Resolves the infinite re-rendering of the notification listview fix(useReducerInfiniteLoading): Corrected the useReducerInfiniteLoading hook so that it returns the right type for the items and made it be compatible with the pages that use the useInfiniteLoading hook refactor(various): Now using the useReducerInfiniteLoading hook in all of the places where the useInfiniteLoading hook was being used fix(useReducerInfiniteLoading): Forget to add the error value to the useMemo dependency list fix(useReducerInfiniteLoading): The nextItemUrl wasn't being set correctly when the reducer state was reset. fix(various): I found that the resetApiState call would cause the infinite loading functionality to just refresh the whole page instead of working as you would aspect. Removing it fixed this issue. Based on my testing, I found it wasn't necessary for the notification functionality. fix(various): It was incorrect to remove the resetApiState function call for the notification functionality. refactor(various): Removed the old versions of useLiveNotifications and useInfiniteLoading and replaced them with the new ones refactored(useLiveNotifications): Removed some commented out code refactor(NetworkDetector): Removed the InteractionBarrier since the service worker PR will make this functionality unnecessary refactor(various): Renamed WithNumberIdentifier to WithIdentifier and made its id property accept string values as well refactor(various): In the middle of making the infinite loading functionality simpler refactor(various): Mostly everything is working. Just need to get the count on the NotificationListView to update when a notification is removed. feat(various): The count on the NotificationListView is now being updated correctly. refactor(various): Renamed the addOne and addMultiple cases refactor(useInfinteLoading): Need to implement a better solution for updating the count on NotificationListView refactor(various): The count is now being updated correctly again. refactor(various): Removed unnecessary properties and console logs; Resolved eslint issues
1 parent 4718d15 commit 49c1d9e

10 files changed

Lines changed: 225 additions & 196 deletions

File tree

src/app/redux/store.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { configureStore, ConfigureStoreOptions } from '@reduxjs/toolkit';
22
import { farmApi } from 'common/api/farmApi';
3+
import { setupListeners } from '@reduxjs/toolkit/query';
34
import { authApi } from 'common/api/authApi';
45
import { notificationApi } from 'common/api/notificationApi';
56
import { userApi } from 'common/api/userApi';
@@ -31,6 +32,8 @@ export const createAppStore = (options?: ConfigureStoreOptions['preloadedState']
3132

3233
export const store = createAppStore();
3334

35+
setupListeners(store.dispatch);
36+
3437
export type RootState = ReturnType<typeof store.getState>;
3538

3639
export type AppDispatch = typeof store.dispatch;

src/common/api/notificationApi.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import { customBaseQuery } from './customBaseQuery';
55

66
export const notificationApi = createApi({
77
reducerPath: 'notificationApi',
8+
89
baseQuery: customBaseQuery,
10+
11+
// Always refetch data, don't used cache.
12+
keepUnusedDataFor: 0,
13+
refetchOnMountOrArgChange: true,
14+
refetchOnReconnect: true,
15+
916
tagTypes: ['AppNotification'],
1017

1118
endpoints: builder => ({
Lines changed: 118 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,135 @@
1+
import { ActionCreatorWithoutPayload } from '@reduxjs/toolkit';
12
import { PaginatedResult } from 'common/models';
2-
import { useEffect, useMemo, useRef, useState } from 'react';
3+
import { useCallback, useEffect, useMemo, useReducer } from 'react';
4+
import { useDispatch } from 'react-redux';
35
import { UseQuery, UseQueryOptions } from 'rtk-query-config';
46

5-
export const useInfiniteLoading = <T, ResultType extends PaginatedResult<T>>(
6-
initialUrl: string,
7+
export interface WithIdentifier {
8+
id?: number | string;
9+
}
10+
11+
interface State<T> {
12+
items: T[];
13+
nextItemUrl: string | null;
14+
count: number;
15+
isGettingMore: boolean;
16+
}
17+
18+
const initialState = {
19+
items: [],
20+
nextItemUrl: null,
21+
count: 0,
22+
isGettingMore: false,
23+
};
24+
25+
type Action<T> =
26+
| { type: 'addOneToFront'; item: T }
27+
| { type: 'addMultipleToBack'; items: T[]; totalCount: number }
28+
| { type: 'set-next-item-url'; nextItemUrl: string | null }
29+
| { type: 'reset-get-more' }
30+
| { type: 'remove'; item: T }
31+
| { type: 'reset'; nextItemUrl: string | null };
32+
33+
const reducer = <T extends WithIdentifier>(state: State<T>, action: Action<T>) => {
34+
switch (action.type) {
35+
case 'addOneToFront':
36+
return { ...state, items: [action.item, ...state.items], count: state.count + 1 };
37+
case 'addMultipleToBack':
38+
return { ...state, items: [...state.items, ...action.items], count: action.totalCount };
39+
case 'remove':
40+
return {
41+
...state,
42+
items: state.items.filter(i => i.id !== action.item.id),
43+
count: state.count - 1,
44+
};
45+
case 'reset':
46+
return { ...initialState, nextItemUrl: action.nextItemUrl };
47+
case 'set-next-item-url':
48+
return { ...state, nextItemUrl: action.nextItemUrl, isGettingMore: true, allItemsRemoved: false };
49+
default:
50+
return { ...initialState };
51+
}
52+
};
53+
54+
export const useInfiniteLoading = <T extends WithIdentifier, ResultType extends PaginatedResult<T>>(
55+
initialUrl: string | null,
756
useQuery: UseQuery<ResultType>,
57+
resetApiStateFunction?: ActionCreatorWithoutPayload,
858
options?: UseQueryOptions,
959
) => {
10-
const [url, setUrl] = useState<string | null>(initialUrl);
11-
const [loadedData, setLoadedData] = useState<T[]>([]);
12-
const rerenderingType = useRef<string>('clear');
60+
const [{ items, nextItemUrl, count, isGettingMore }, itemDispatch] = useReducer(reducer, {
61+
...initialState,
62+
nextItemUrl: initialUrl,
63+
});
64+
const dispatch = useDispatch();
1365

14-
const { data, error, isLoading, isFetching } = useQuery(url, options);
66+
const { data: fetchedItems, isFetching, isLoading, refetch, error } = useQuery(nextItemUrl, options);
1567

16-
useEffect(() => {
17-
const clear = () => {
18-
rerenderingType.current = 'clear';
19-
setLoadedData([]);
20-
setUrl(initialUrl);
21-
};
68+
const addOneToFront = useCallback(
69+
(newItem: T) => {
70+
itemDispatch({ type: 'addOneToFront', item: newItem });
71+
},
72+
[itemDispatch],
73+
);
2274

23-
if (data && !isLoading) {
24-
setLoadedData(n => [...n, ...data.results]);
75+
const clear = useCallback(() => {
76+
itemDispatch({ type: 'reset', nextItemUrl: initialUrl });
77+
if (resetApiStateFunction) {
78+
dispatch(resetApiStateFunction());
2579
}
80+
}, [itemDispatch, initialUrl, dispatch, resetApiStateFunction]);
2681

27-
return () => {
28-
if (rerenderingType.current === 'clear') {
29-
clear();
30-
}
31-
if (rerenderingType.current === 'fetchMore') {
32-
rerenderingType.current = 'clear';
33-
}
34-
};
35-
}, [data, isLoading, initialUrl]);
82+
const remove = useCallback(
83+
(itemToRemove: T) => {
84+
itemDispatch({ type: 'remove', item: itemToRemove });
85+
},
86+
[itemDispatch],
87+
);
3688

3789
const hasMore = useMemo(() => {
3890
if (isLoading || isFetching) return false;
39-
return !!data?.links.next;
40-
}, [data, isLoading, isFetching]);
91+
return !!fetchedItems?.links.next;
92+
}, [fetchedItems, isLoading, isFetching]);
4193

42-
const fetchMore = () => {
43-
if (hasMore && data) {
44-
rerenderingType.current = 'fetchMore';
45-
setUrl(data.links.next);
94+
const getMore = useCallback(() => {
95+
if (fetchedItems?.links.next && !isFetching) {
96+
itemDispatch({ type: 'set-next-item-url', nextItemUrl: fetchedItems.links.next });
4697
}
47-
};
48-
49-
return {
50-
loadedData,
51-
error,
52-
isLoading,
53-
isFetching,
54-
totalCount: data?.meta.count,
55-
hasMore,
56-
fetchMore,
57-
};
98+
}, [itemDispatch, isFetching, fetchedItems]);
99+
100+
// Clear the items when the user's internet connection is restored
101+
useEffect(() => {
102+
if (!isLoading && isFetching && !isGettingMore) {
103+
clear();
104+
}
105+
}, [isLoading, isFetching, isGettingMore, clear]);
106+
107+
// Append new items that we got from the API to
108+
// the items list
109+
useEffect(() => {
110+
itemDispatch({
111+
type: 'addMultipleToBack',
112+
items: fetchedItems?.results || [],
113+
totalCount: fetchedItems?.meta.count || 0,
114+
});
115+
}, [fetchedItems]);
116+
117+
const itemProviderValue = useMemo(() => {
118+
const result = {
119+
items: items as T[],
120+
count,
121+
hasMore,
122+
isFetching,
123+
isLoading,
124+
remove,
125+
clear,
126+
getMore,
127+
refetch,
128+
addOneToFront,
129+
error,
130+
};
131+
return result;
132+
}, [clear, remove, getMore, hasMore, items, count, isFetching, isLoading, addOneToFront, refetch, error]);
133+
134+
return itemProviderValue;
58135
};

src/features/farm-dashboard/pages/UpdateFarmView.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import { Link, useNavigate, useParams } from 'react-router-dom';
1212
import { FarmDetailForm, FormData } from '../components/FarmDetailForm';
1313
import { useAuth } from 'features/auth/hooks';
1414
import { ChangeLog } from 'common/components/ChangeLog/ChangeLog';
15-
import { useInfiniteLoading } from 'common/hooks/useInfiniteLoading';
1615
import { QueryParamsBuilder } from 'common/api/queryParamsBuilder';
1716
import { HistoricalRecord } from 'common/models/historicalRecord';
1817
import { ChangeListGroup } from 'common/components/ChangeLog/ChangeListGroup';
1918
import { LoadingButton } from 'common/components/LoadingButton';
2019
import { useModal } from 'react-modal-hook';
2120
import { DimmableContent } from 'common/styles/utilities';
21+
import { WithIdentifier, useInfiniteLoading } from 'common/hooks/useInfiniteLoading';
2222

2323
export type RouteParams = {
2424
id: string;
@@ -35,15 +35,18 @@ export const UpdateFarmView: FC = () => {
3535
const queryParams = new QueryParamsBuilder().setPaginationParams(1, pageSize).build();
3636
const url = `/farms/${id}/history/?${queryParams}`;
3737
const {
38-
loadedData: farmHistory,
38+
items: farmHistory,
3939
error: farmHistoryError,
4040
isFetching: isFetchingHistory,
41-
totalCount,
41+
count: totalCount,
4242
hasMore,
43-
fetchMore,
44-
} = useInfiniteLoading<HistoricalRecord<User>, PaginatedResult<HistoricalRecord<User>>>(url, useGetFarmHistoryQuery, {
45-
skip: user?.role !== 'ADMIN',
46-
});
43+
getMore,
44+
} = useInfiniteLoading<HistoricalRecord<User> & WithIdentifier, PaginatedResult<HistoricalRecord<User>>>(
45+
url,
46+
useGetFarmHistoryQuery,
47+
undefined,
48+
{ skip: user?.role !== 'ADMIN' },
49+
);
4750

4851
const [formValidationErrors, setFormValidationErrors] = useState<ServerValidationErrors<FormData> | null>(null);
4952

@@ -65,7 +68,7 @@ export const UpdateFarmView: FC = () => {
6568
className='action-shadow'
6669
loading={isFetchingHistory}
6770
variant='primary'
68-
onClick={() => fetchMore()}
71+
onClick={() => getMore()}
6972
>
7073
Load More
7174
</LoadingButton>
Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,8 @@
1-
import { FC, PropsWithChildren, useEffect, useRef, useState } from 'react';
2-
import * as notificationService from 'common/services/notification';
1+
import { FC, PropsWithChildren } from 'react';
2+
import { useNetworkDetection } from '../hooks/useNetworkConnection';
33

44
export const NetworkDetector: FC<PropsWithChildren<unknown>> = ({ children }) => {
5-
const [isDisconnected, setDisconnectedStatus] = useState(false);
6-
const prevDisconnectionStatus = useRef(false);
7-
8-
const handleConnectionChange = () => {
9-
setDisconnectedStatus(!navigator.onLine);
10-
};
11-
12-
const getRandomNumber = () => {
13-
return new Date().valueOf().toString();
14-
};
15-
16-
useEffect(() => {
17-
window.addEventListener('online', handleConnectionChange);
18-
window.addEventListener('offline', handleConnectionChange);
19-
20-
if (isDisconnected) {
21-
notificationService.showErrorMessage('Internet Connection Lost', getRandomNumber());
22-
} else if (prevDisconnectionStatus.current) {
23-
notificationService.showSuccessMessage('Internet Connection Restored', getRandomNumber());
24-
}
25-
26-
prevDisconnectionStatus.current = isDisconnected;
27-
28-
return () => {
29-
window.removeEventListener('online', handleConnectionChange);
30-
window.removeEventListener('offline', handleConnectionChange);
31-
};
32-
}, [isDisconnected]);
5+
useNetworkDetection();
336

347
return <>{children}</>;
358
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useState, useRef, useEffect } from 'react';
2+
import * as notificationService from 'common/services/notification';
3+
4+
export const getRandomNumber = () => {
5+
return new Date().valueOf().toString();
6+
};
7+
8+
export const useNetworkDetection = () => {
9+
const [isDisconnected, setDisconnectedStatus] = useState(false);
10+
const prevDisconnectionStatus = useRef(false);
11+
12+
const handleConnectionChange = () => {
13+
setDisconnectedStatus(!navigator.onLine);
14+
};
15+
16+
useEffect(() => {
17+
window.addEventListener('online', handleConnectionChange);
18+
window.addEventListener('offline', handleConnectionChange);
19+
20+
if (isDisconnected) {
21+
notificationService.showErrorMessage('Internet Connection Lost', getRandomNumber());
22+
} else if (prevDisconnectionStatus.current) {
23+
notificationService.showSuccessMessage('Internet Connection Restored', getRandomNumber());
24+
}
25+
26+
prevDisconnectionStatus.current = isDisconnected;
27+
28+
return () => {
29+
window.removeEventListener('online', handleConnectionChange);
30+
window.removeEventListener('offline', handleConnectionChange);
31+
};
32+
}, [isDisconnected]);
33+
34+
return { isDisconnected };
35+
};

src/features/notifications/components/NotificationDropdown.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { faBell, faEnvelope, faEnvelopeOpen } from '@fortawesome/free-solid-svg-
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import { useGetReadNotificationsQuery, useMarkAllReadMutation } from 'common/api/notificationApi';
44
import { LoadingButton } from 'common/components/LoadingButton';
5-
import { useInfiniteLoading } from 'common/hooks/useInfiniteLoading';
5+
import { WithIdentifier, useInfiniteLoading } from 'common/hooks/useInfiniteLoading';
66
import { PaginatedResult } from 'common/models';
77
import { AppNotification } from 'common/models/notifications';
88
import { NoContent } from 'common/styles/utilities';
@@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom';
1212
import styled from 'styled-components';
1313
import { NotificationContext } from '../context';
1414
import { renderNotification } from './renderNotification';
15+
import { notificationApi } from 'common/api/notificationApi';
1516

1617
const StyledContainer = styled.div`
1718
min-width: 420px;
@@ -72,10 +73,15 @@ export const NotificationDropdown: FC = () => {
7273
count: unreadNotificationsCount,
7374
clear: clearUnreadNotifications,
7475
} = useContext(NotificationContext);
75-
const { loadedData: readNotifications, isLoading: isLoadingReadNotifications } = useInfiniteLoading<
76-
AppNotification,
77-
PaginatedResult<AppNotification>
78-
>('', useGetReadNotificationsQuery);
76+
const {
77+
items: readNotifications,
78+
isLoading: isLoadingReadNotifications,
79+
hasMore: hasMoreReadNotifications,
80+
} = useInfiniteLoading<AppNotification & WithIdentifier, PaginatedResult<AppNotification>>(
81+
'',
82+
useGetReadNotificationsQuery,
83+
notificationApi.util.resetApiState,
84+
);
7985
const [markAllRead, { isLoading: isLoadingMarkAllRead }] = useMarkAllReadMutation();
8086

8187
const handleMarkAllRead = async () => {

0 commit comments

Comments
 (0)