Skip to content

Commit 2771282

Browse files
authored
Improve loading states and fix authentication data fetching (#48)
* feat: replace redux-localstorage with redux-persist - Update store configuration to use redux-persist instead of redux-localstorage - Add PersistGate wrapper component for state rehydration - Configure whitelist-based persistence for better control over persisted state * feat: add loading state management for data fetching - Add loading state tracking to Redux store with initial states for unit/service - Implement GET_RESOURCE_START action to set loading states to true - Update dataReducer to handle loading state transitions and normalize state structure - Modify Dashboard component to use loading state instead of checking for undefined data - Add setResourceFetchStart action dispatch in Main component before fetch operations * fix: reset geolocation stage after login to prevent stuck state - Add reset parameter to getInitialLocation() to reset currentStage - Fix bug where geolocation wouldn't work after login due to persistent currentStage - Ensure getNearestUnits() is called properly on subsequent app sessions - Pass reset=true explicitly when calling getInitialLocation in Main component * fix: ensure user authentication before data fetching in AppComponent * fix: remove unnecessary import
1 parent 298be82 commit 2771282

9 files changed

Lines changed: 125 additions & 51 deletions

File tree

package-lock.json

Lines changed: 9 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"react-router-dom": "^6.23.1",
4343
"redux": "^5.0.1",
4444
"redux-actions": "^3.0.0",
45-
"redux-localstorage": "^0.4.1",
45+
"redux-persist": "^6.0.0",
4646
"redux-promise": "^0.6.0",
4747
"sass": "^1.77.4",
4848
"sass-loader": "^14.2.1",

src/actions/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import _ from 'lodash';
22
import { createAction } from 'redux-actions';
33
import * as ApiClient from '../lib/municipal-services-client.js';
44

5+
export const setResourceFetchStart = createAction(
6+
'GET_RESOURCE_START',
7+
(resourceType) => resourceType,
8+
(resourceType) => ({ resourceType })
9+
);
10+
511
export const fetchUnitsWithServices = createAction(
612
'GET_RESOURCE',
713
ApiClient.fetchUnitsWithServices,

src/components/DashBoard.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { UnitListElement } from './UnitList';
88
class DashBoard extends React.Component {
99

1010
render() {
11-
if (this.props.nearest === undefined || this.props.frequent === undefined) {
11+
if (this.props.loading) {
1212
return <div>Ladataan...</div>;
1313
}
1414
let nearest;
@@ -48,14 +48,18 @@ function topUnits(n, units, unitsByUpdateCount) {
4848
function mapStateToProps(state) {
4949
if (_.keys(state.data.unit).length > 0) {
5050
return {
51+
loading: state.data.loading.unit,
5152
nearest: _.map(state.data.unitsByDistance, (u) => {
5253
return state.data.unit[u.id];
5354
}),
5455
frequent: topUnits(20, state.data.unit, state.unitsByUpdateCount),
5556
userLocation: state.userLocation
5657
};
5758
}
58-
return {};
59+
return {
60+
loading: state.data.loading.unit,
61+
userLocation: state.userLocation
62+
};
5963
}
6064

6165
function mapDispatchToProps() {

src/components/Main.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
66
import { Link, Outlet } from 'react-router-dom';
77
import _ from 'lodash';
88

9-
import { fetchUnitsWithServices, fetchResource, setUserLocation, getNearestUnits } from '../actions/index';
9+
import { fetchUnitsWithServices, fetchResource, setUserLocation, getNearestUnits, setResourceFetchStart } from '../actions/index';
1010
import { getCurrentSeason } from './utils';
1111
import { getInitialLocation } from '../lib/geolocation';
1212
import { withRouter } from '../hooks/index';
@@ -28,12 +28,20 @@ function requireAuth(navigate, auth) {
2828
class AppComponent extends React.Component {
2929
componentDidMount() {
3030
let { navigate, auth } = this.props;
31-
requireAuth(navigate, auth);
31+
32+
// Only proceed with data fetching if user is authenticated
33+
if (!hasAuth(auth)) {
34+
requireAuth(navigate, auth);
35+
return;
36+
}
37+
3238
const services = constants.SERVICE_GROUPS[this.props.serviceGroup].services;
39+
this.props.setResourceFetchStart('service');
3340
this.props.fetchResource(
3441
'service', { id: services.join(',') },
3542
['id', 'name'], ['observable_properties']
3643
);
44+
this.props.setResourceFetchStart('unit');
3745
this.props.fetchUnitsWithServices(
3846
services, this.props.maintenanceOrganization, {
3947
selected: ['id', 'name', 'services', 'location', 'extensions'],
@@ -43,7 +51,7 @@ class AppComponent extends React.Component {
4351
if (services) {
4452
this.props.getNearestUnits(position, services, this.props.maintenanceOrganization);
4553
}
46-
});
54+
}, true);
4755
}
4856

4957
render() {
@@ -113,6 +121,9 @@ const mapDispatchToProps = (dispatch) => {
113121
},
114122
getNearestUnits: (...args) => {
115123
dispatch(getNearestUnits(...args));
124+
},
125+
setResourceFetchStart: (resourceType) => {
126+
dispatch(setResourceFetchStart(resourceType));
116127
}
117128
};
118129
};

src/index.js

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { createMemoryHistory } from 'history';
66
import { compose, applyMiddleware, legacy_createStore as createStore } from 'redux';
77
import { Provider } from 'react-redux';
88
import promiseMiddleware from 'redux-promise';
9-
import persistState from 'redux-localstorage';
9+
import { persistStore, persistReducer } from 'redux-persist';
10+
import storage from 'redux-persist/lib/storage'; // localStorage
11+
import { PersistGate } from 'redux-persist/integration/react';
1012
import 'bootstrap-sass';
1113

1214
import queueHandler from './actions/queueHandler';
@@ -36,15 +38,38 @@ import isomorphicFetch from 'isomorphic-fetch';
3638
isomorphicFetch;
3739
moment.locale('fi');
3840

39-
const stateToPersist = ['data', 'auth', 'updateQueue', 'unitsByUpdateTime', 'unitsByUpdateCount', 'serviceGroup'];
41+
const persistConfig = {
42+
key: 'root',
43+
storage,
44+
whitelist: ['auth', 'updateQueue', 'unitsByUpdateTime', 'unitsByUpdateCount', 'serviceGroup'], // Only persist these
45+
transforms: [
46+
// Transform to exclude loading from data if we include data
47+
{
48+
in: (state, key) => {
49+
if (key === 'data' && state) {
50+
// eslint-disable-next-line no-unused-vars
51+
const { loading, ...stateWithoutLoading } = state;
52+
return stateWithoutLoading;
53+
}
54+
return state;
55+
},
56+
// eslint-disable-next-line no-unused-vars
57+
out: (state, key) => state
58+
}
59+
]
60+
};
4061

41-
const finalCreateStore = compose(
42-
applyMiddleware(promiseMiddleware),
43-
persistState(stateToPersist))(createStore);
62+
const persistedReducer = persistReducer(persistConfig, rootReducer);
4463

45-
const enableReduxDevTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
64+
const store = createStore(
65+
persistedReducer,
66+
compose(
67+
applyMiddleware(promiseMiddleware),
68+
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
69+
)
70+
);
4671

47-
const store = finalCreateStore(rootReducer, enableReduxDevTools);
72+
const persistor = persistStore(store);
4873
window.store = store;
4974

5075
const rootElement = document.getElementById('app');
@@ -53,24 +78,26 @@ const root = createRoot(rootElement);
5378
// Render the main component into the dom
5479
root.render(
5580
<Provider store={store}>
56-
<BrowserRouter history={createMemoryHistory()}>
57-
<Routes>
58-
<Route path="/login" element={<LoginScreen />} />
59-
<Route path="/" element={<App />}>
60-
<Route exact path="/" element={<DashBoard />} />
61-
<Route path="/group" element={<GroupList />} />
62-
<Route path="/group/:groupId" element={<UnitList />} />
63-
<Route path="/group/:groupId/mass-edit" element={<UnitMassEditPropertySelect />} />
64-
<Route path="/group/:groupId/mass-edit/:propertyId" element={<UnitMassEdit />} />
65-
<Route path="/unit/:unitId" element={<UnitDetails />} />
66-
<Route path="/unit/:unitId/history" element={<UnitHistory />} />
67-
<Route path="/unit/:unitId/update/:propertyId/:valueId" element={<UpdateConfirmation />} />
68-
<Route path="/unit/:unitId/delete/:propertyId" element={<DeleteConfirmation />} />
69-
<Route path="/queue" element={<UpdateQueue />} />
70-
<Route path="*" element={<NotFound />} />
71-
</Route>
72-
</Routes>
73-
</BrowserRouter>
81+
<PersistGate loading={<div>Ladataan...</div>} persistor={persistor}>
82+
<BrowserRouter history={createMemoryHistory()}>
83+
<Routes>
84+
<Route path="/login" element={<LoginScreen />} />
85+
<Route path="/" element={<App />}>
86+
<Route exact path="/" element={<DashBoard />} />
87+
<Route path="/group" element={<GroupList />} />
88+
<Route path="/group/:groupId" element={<UnitList />} />
89+
<Route path="/group/:groupId/mass-edit" element={<UnitMassEditPropertySelect />} />
90+
<Route path="/group/:groupId/mass-edit/:propertyId" element={<UnitMassEdit />} />
91+
<Route path="/unit/:unitId" element={<UnitDetails />} />
92+
<Route path="/unit/:unitId/history" element={<UnitHistory />} />
93+
<Route path="/unit/:unitId/update/:propertyId/:valueId" element={<UpdateConfirmation />} />
94+
<Route path="/unit/:unitId/delete/:propertyId" element={<DeleteConfirmation />} />
95+
<Route path="/queue" element={<UpdateQueue />} />
96+
<Route path="*" element={<NotFound />} />
97+
</Route>
98+
</Routes>
99+
</BrowserRouter>
100+
</PersistGate>
74101
</Provider>
75102
);
76103

src/lib/geolocation.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function createSuccessCallback(callback) {
3737
return (position) => {
3838
currentStage++;
3939
if (currentStage < _.size(POSITION_OPTIONS) - 1) {
40-
getInitialLocation(callback);
40+
getInitialLocation(callback, false); // Don't reset on subsequent calls
4141
}
4242
callback(position);
4343
};
@@ -46,10 +46,15 @@ function createSuccessCallback(callback) {
4646
function error() {
4747
}
4848

49-
export function getInitialLocation(callback) {
49+
export function getInitialLocation(callback, reset = true) {
5050
if (DISABLED) {
5151
return;
5252
}
53+
// Reset stage for new geolocation session
54+
if (reset) {
55+
currentStage = 0;
56+
}
57+
5358
const options = POSITION_OPTIONS[STAGES[currentStage]];
5459
if (options) {
5560
navigator.geolocation.getCurrentPosition(createSuccessCallback(callback), error, options);

src/reducers/index.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const initialDataState = {
88
unitsByDistance: [],
99
observable_property: {},
1010
observation: {},
11-
service: {}
11+
service: {},
12+
loading: {}
1213
};
1314

1415
const initialAuthState = {
@@ -27,21 +28,37 @@ const serviceGroup = 'skiing';
2728

2829
function dataReducer(state = initialDataState, action) {
2930
switch (action.type) {
31+
case 'GET_RESOURCE_START':
32+
const startResourceType = action.meta.resourceType;
33+
return {
34+
...state,
35+
loading: {
36+
...state.loading,
37+
[startResourceType]: true
38+
}
39+
};
3040
case 'GET_RESOURCE':
3141
const resourceType = action.meta.resourceType;
3242
let existingResources = state[resourceType];
3343
if (action.meta.replaceAll === true) {
3444
existingResources = {};
3545
}
36-
return Object.assign(
37-
{}, state,
38-
{[resourceType]: Object.assign(
39-
existingResources,
40-
action.payload[resourceType])});
46+
return {
47+
...state,
48+
[resourceType]: {
49+
...existingResources,
50+
...action.payload[resourceType]
51+
},
52+
loading: {
53+
...state.loading,
54+
[resourceType]: false
55+
}
56+
};
4157
case 'GET_NEAREST_UNITS':
42-
return Object.assign(
43-
{}, state,
44-
{unitsByDistance: action.payload});
58+
return {
59+
...state,
60+
unitsByDistance: action.payload
61+
};
4562
}
4663
return state;
4764
}

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6300,10 +6300,10 @@ redux-actions@^3.0.0:
63006300
just-curry-it "5.3.0"
63016301
reduce-reducers "1.0.4"
63026302

6303-
redux-localstorage@^0.4.1:
6304-
version "0.4.1"
6305-
resolved "https://registry.yarnpkg.com/redux-localstorage/-/redux-localstorage-0.4.1.tgz#faf6d719c581397294d811473ffcedee065c933c"
6306-
integrity sha512-dUha0YoH+BSZ2q15pakB+JWeqiuXUf3Ir4rObOpNrZ96HEdciGAjkL10k3KGdLI7qvQw/c096asw/SQ6TPjU/A==
6303+
redux-persist@^6.0.0:
6304+
version "6.0.0"
6305+
resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8"
6306+
integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==
63076307

63086308
redux-promise@^0.6.0:
63096309
version "0.6.0"

0 commit comments

Comments
 (0)