Implement Redux Persist for state persistence across page reloads

- Install redux-persist package
- Configure persistReducer with whitelist (offline, activities, children)
- Exclude network slice from persistence (should be fresh on reload)
- Add PersistGate to ReduxProvider with loading indicator
- Configure serializableCheck to ignore persist actions
- Store state now persists to localStorage automatically

This fixes the issue where app state was lost on page reload, improving UX.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-02 14:52:12 +00:00
parent 537e5d7823
commit 8c0981fa90
4 changed files with 2082 additions and 23 deletions

View File

@@ -2,8 +2,10 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from '@/store/store'; import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from '@/store/store';
import { setupNetworkDetection } from '@/store/middleware/offlineMiddleware'; import { setupNetworkDetection } from '@/store/middleware/offlineMiddleware';
import { CircularProgress, Box } from '@mui/material';
export function ReduxProvider({ children }: { children: React.ReactNode }) { export function ReduxProvider({ children }: { children: React.ReactNode }) {
const cleanupRef = useRef<(() => void) | null>(null); const cleanupRef = useRef<(() => void) | null>(null);
@@ -20,5 +22,25 @@ export function ReduxProvider({ children }: { children: React.ReactNode }) {
}; };
}, []); }, []);
return <Provider store={store}>{children}</Provider>; return (
<Provider store={store}>
<PersistGate
loading={
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh'
}}
>
<CircularProgress />
</Box>
}
persistor={persistor}
>
{children}
</PersistGate>
</Provider>
);
} }

File diff suppressed because it is too large Load Diff

View File

@@ -32,8 +32,11 @@
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.63.0", "react-hook-form": "^7.63.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"redux-persist": "^6.0.0",
"remark-gfm": "^4.0.1",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"web-vitals": "^5.1.0", "web-vitals": "^5.1.0",
"workbox-webpack-plugin": "^7.3.0", "workbox-webpack-plugin": "^7.3.0",

View File

@@ -1,4 +1,7 @@
import { configureStore, Middleware } from '@reduxjs/toolkit'; import { configureStore, Middleware } from '@reduxjs/toolkit';
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
import { combineReducers } from '@reduxjs/toolkit';
// Slices // Slices
import offlineReducer from './slices/offlineSlice'; import offlineReducer from './slices/offlineSlice';
@@ -10,26 +13,41 @@ import networkReducer from './slices/networkSlice';
import { offlineMiddleware } from './middleware/offlineMiddleware'; import { offlineMiddleware } from './middleware/offlineMiddleware';
import { syncMiddleware } from './middleware/syncMiddleware'; import { syncMiddleware } from './middleware/syncMiddleware';
// Persist configuration
const persistConfig = {
key: 'root',
version: 1,
storage,
// Only persist these slices (exclude network status as it should be fresh on reload)
whitelist: ['offline', 'activities', 'children'],
};
const rootReducer = combineReducers({
offline: offlineReducer,
activities: activitiesReducer,
children: childrenReducer,
network: networkReducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: persistedReducer,
offline: offlineReducer,
activities: activitiesReducer,
children: childrenReducer,
network: networkReducer,
},
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware({
serializableCheck: { serializableCheck: {
// Ignore these action types for serialization check // Ignore redux-persist action types
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'], ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER, 'persist/PERSIST', 'persist/REHYDRATE'],
// Ignore these field paths in all actions // Ignore these field paths in all actions
ignoredActionPaths: ['meta.arg', 'payload.timestamp'], ignoredActionPaths: ['meta.arg', 'payload.timestamp', 'register'],
// Ignore these paths in the state // Ignore these paths in the state
ignoredPaths: ['items.dates'], ignoredPaths: ['items.dates', 'register'],
}, },
}).concat(offlineMiddleware, syncMiddleware), }).concat(offlineMiddleware, syncMiddleware),
}); });
export const persistor = persistStore(store);
// Infer the `RootState` and `AppDispatch` types from the store itself // Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>; export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;