From 1df500420348ca049a7aba9ccec7462a505bd2bb Mon Sep 17 00:00:00 2001 From: Peizhi Zheng Date: Thu, 14 Jan 2021 20:46:41 -0800 Subject: [PATCH] Persist slice of redux state in local storage --- ui/src/App.tsx | 4 ++-- ui/src/actions/settingsActions.ts | 1 - ui/src/index.tsx | 5 ++++ ui/src/localStorage.ts | 24 +++++++++++++++++++ ui/src/store.ts | 8 ++++++- ui/src/theme.tsx | 3 +-- ui/src/views/SettingsView.tsx | 40 +++++++++++++++++-------------- 7 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 ui/src/localStorage.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 8d30cc0..b945993 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -26,7 +26,7 @@ import DoubleArrowIcon from "@material-ui/icons/DoubleArrow"; import CloseIcon from "@material-ui/icons/Close"; import { AppState } from "./store"; import { paths } from "./paths"; -import { makeTheme } from "./theme"; +import { useTheme } from "./theme"; import { closeSnackbar } from "./actions/snackbarActions"; import ListItemLink from "./components/ListItemLink"; import SchedulersView from "./views/SchedulersView"; @@ -146,7 +146,7 @@ function SlideUpTransition(props: TransitionProps) { } function App(props: ConnectedProps) { - const theme = makeTheme(props.themePreference); + const theme = useTheme(props.themePreference); const classes = useStyles(theme)(); const [open, setOpen] = useState(true); const toggleDrawer = () => setOpen(!open); diff --git a/ui/src/actions/settingsActions.ts b/ui/src/actions/settingsActions.ts index dc9f1e4..c1f21ea 100644 --- a/ui/src/actions/settingsActions.ts +++ b/ui/src/actions/settingsActions.ts @@ -1,5 +1,4 @@ import { ThemePreference } from "../reducers/settingsReducer"; - // List of settings related action types. export const POLL_INTERVAL_CHANGE = "POLL_INTERVAL_CHANGE"; export const THEME_PREFERENCE_CHANGE = "THEME_PREFERENCE_CHANGE"; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 163bcea..d4df6be 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -5,6 +5,11 @@ import { Provider } from "react-redux"; import App from "./App"; import store from "./store"; import * as serviceWorker from "./serviceWorker"; +import { saveState } from "./localStorage"; + +store.subscribe(() => { + saveState(store.getState()); +}); ReactDOM.render( diff --git a/ui/src/localStorage.ts b/ui/src/localStorage.ts new file mode 100644 index 0000000..013d71d --- /dev/null +++ b/ui/src/localStorage.ts @@ -0,0 +1,24 @@ +import { AppState } from "./store"; + +const LOCAL_STORAGE_KEY = "asynqmon:state"; + +export function loadState(): AppState | undefined { + try { + const serializedState = localStorage.getItem(LOCAL_STORAGE_KEY); + if (serializedState === null) { + return undefined; + } + return JSON.parse(serializedState); + } catch (err) { + return undefined; + } +} + +export function saveState(state: AppState) { + try { + const serializedState = JSON.stringify({ settings: state.settings }); + localStorage.setItem(LOCAL_STORAGE_KEY, serializedState); + } catch (err) { + console.error("saveState: could not save state: ", err); + } +} diff --git a/ui/src/store.ts b/ui/src/store.ts index f8017d1..a9abce2 100644 --- a/ui/src/store.ts +++ b/ui/src/store.ts @@ -7,6 +7,7 @@ import schedulerEntriesReducer from "./reducers/schedulerEntriesReducer"; import snackbarReducer from "./reducers/snackbarReducer"; import queueStatsReducer from "./reducers/queueStatsReducer"; import redisInfoReducer from "./reducers/redisInfoReducer"; +import { loadState } from "./localStorage"; const rootReducer = combineReducers({ settings: settingsReducer, @@ -19,9 +20,14 @@ const rootReducer = combineReducers({ redis: redisInfoReducer, }); +const preloadedState = loadState(); + // AppState is the top-level application state maintained by redux store. export type AppState = ReturnType; -export default configureStore({ +const store = configureStore({ reducer: rootReducer, + preloadedState, }); + +export default store; diff --git a/ui/src/theme.tsx b/ui/src/theme.tsx index 7992700..c57469b 100644 --- a/ui/src/theme.tsx +++ b/ui/src/theme.tsx @@ -2,8 +2,7 @@ import { createMuiTheme, Theme } from "@material-ui/core/styles"; import { ThemePreference } from "./reducers/settingsReducer"; import useMediaQuery from "@material-ui/core/useMediaQuery"; -export function makeTheme(themePreference: ThemePreference): Theme { - // eslint-disable-next-line react-hooks/rules-of-hooks +export function useTheme(themePreference: ThemePreference): Theme { let prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); if (themePreference === ThemePreference.Always) { prefersDarkMode = true; diff --git a/ui/src/views/SettingsView.tsx b/ui/src/views/SettingsView.tsx index 70b29c4..19e17e2 100644 --- a/ui/src/views/SettingsView.tsx +++ b/ui/src/views/SettingsView.tsx @@ -63,14 +63,14 @@ function SettingsView(props: PropsFromRedux) { props.selectTheme(event.target.value as ThemePreference); }; return ( - + Settings - + Polling Interval @@ -93,22 +93,26 @@ function SettingsView(props: PropsFromRedux) { /> - - Dark theme - - + + + + Dark theme + + + + );