Skip to content

Commit

Permalink
Replaced redux with zustand
Browse files Browse the repository at this point in the history
  • Loading branch information
RoelVB committed Nov 19, 2024
1 parent ecaf0e4 commit 046e82a
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 313 deletions.
206 changes: 47 additions & 159 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.11.0",
"@mui/material": "^5.11.2",
"@reduxjs/toolkit": "^1.9.1",
"async-mutex": "^0.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"sjcl-all": "^1.0.1",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"zustand": "^5.0.1"
},
"devDependencies": {
"@types/chai": "^4.3.4",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const DropdownContainer = styled(Popper)(({ theme })=>({

const Dropdown: React.FC<IProps> = (props)=>
{
const settings = useSettings();
const settings = useSettings(state=>state.settings);

// Keep track of the anchor element's width
const [minWidth, setMinWidth] = React.useState<number|undefined>(props.anchorEl.clientWidth);
Expand Down
2 changes: 1 addition & 1 deletion src/components/EmbeddedPicker/CredentialPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const CredentialPicker: React.FC<IProps> = (props)=>
const dropdown = React.useRef<ReturnType<typeof openDropdown>>();
const [credentails, setCredentials] = React.useState(props.credentails);
const [errorMsg, setErrorMsg] = React.useState<string>();
const settings = useSettings();
const settings = useSettings(state=>state.settings);

// Close the dropdown when this element unmounts
React.useEffect(()=>{
Expand Down
81 changes: 46 additions & 35 deletions src/components/Hooks/Settings.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,52 @@
import React from 'react';
import { Mutex } from 'async-mutex';
import { create } from 'zustand';
import { log } from '../../classes/Constants';
import { ISettings, loadSettings } from '../../Settings';
import { ISettings, loadSettings, saveSettings } from '../../Settings';

const fetchingSettings = new Mutex();
let fetchedSettings: ISettings|undefined;

/**
* This hooks fetches settings only once, even if it's used in multiple components
* @returns settings
*/
export const useSettings = () =>
interface SettingsState
{
const [settings, setSettings] = React.useState<ISettings>();
settings?: ISettings;
isLoading: boolean;
isSaving: boolean;
errorMsg?: string;
loadSettings: ()=>void;
saveSettings: (settings: Partial<ISettings>)=>void;
}

React.useEffect(()=>{
(async ()=>{
if(fetchingSettings.isLocked()) // Some other component is already fetching the settings?
{
await fetchingSettings.waitForUnlock();
setSettings(fetchedSettings);
}
else if(!fetchedSettings) // We need to fetch the settings
{
await fetchingSettings.runExclusive(async ()=>{
try {
fetchedSettings = await loadSettings();
setSettings(fetchedSettings);
} catch(error) {
log('error', 'Failed to load settings.', error);
}
});
export const useSettings = create<SettingsState>()((set, get, store) => ({
isLoading: false,
isSaving: false,
loadSettings: async ()=>{
if(!get().isLoading)
{
log('debug', 'Load settings');
try {
set({isLoading: true, errorMsg: undefined});
set({settings: await loadSettings()});
} catch(error) {
console.error('Failed to load settings.', error);
set({errorMsg: `Failed to load settings. ${String(error)}`});
} finally {
set({isLoading: false});
}
else // We already have loaded the settings
setSettings(fetchedSettings);
})();
}, []);
}
},
saveSettings: async (settings)=>{
log('debug', 'Saving settings', settings);
try {
set({isSaving: true, errorMsg: undefined});
await saveSettings(settings);

if(get().settings) // We already have settings loaded?
set(state => ({settings: {...state.settings!, ...settings}})); // Merge new settings with the original
else
get().loadSettings(); // Load all settings
} catch(error) {
console.error('Failed to save settings.', error);
set({errorMsg: `Failed to save settings. ${String(error)}`});
} finally {
set({isSaving: false});
}
},
}));

return settings;
};
export default useSettings;
11 changes: 5 additions & 6 deletions src/components/Options/Changelog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import Link from '@mui/material/Link';
import AlertTitle from '@mui/material/AlertTitle';
import ScienceIcon from '@mui/icons-material/Science';
import changelog from '../../changelog/changelog';
import { ExtensionName, isBeta } from '../../classes/Constants';
import useSettings from '../Hooks/Settings';
import { PaperGrid } from './Options';
import SwitchOption from './SwitchOption';
import { ExtensionName, isBeta } from '../../classes/Constants';
import { useAppDispatch, useAppSelector } from '../redux/Store';
import { saveSettings } from '../redux/Settings';

export interface IProps
{
Expand All @@ -21,8 +20,8 @@ export interface IProps

const Changelog: React.FC<IProps> = (props)=>
{
const dispatch = useAppDispatch();
const hideTryBetaMsg = useAppSelector(state=>state.settings.settings?.hideTryBetaMsg);
const hideTryBetaMsg = useSettings(state=>state.settings?.hideTryBetaMsg);
const saveSettings = useSettings(state=>state.saveSettings);

// Order by version number
const versions = React.useMemo(()=>{
Expand All @@ -37,7 +36,7 @@ const Changelog: React.FC<IProps> = (props)=>
}, []);

const disabledBetaMsg = React.useCallback(()=>{
dispatch(saveSettings({hideTryBetaMsg: true}));
saveSettings({hideTryBetaMsg: true});
}, []);

return (<Box sx={{display: 'flex', flexDirection: 'column', gap: 1}}>
Expand Down
14 changes: 7 additions & 7 deletions src/components/Options/Connection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import { useAssociation } from '../Hooks/Association';
import { PaperGrid } from './Options';
import { useAppDispatch, useAppSelector } from '../redux/Store';
import { defaultSettings } from '../../Settings';
import Button from '@mui/material/Button';
import { saveSettings } from '../redux/Settings';
import { log } from '../../classes/Constants';
import CircularProgress from '@mui/material/CircularProgress';
import useSettings from '../Hooks/Settings';

const AssociationStatus: React.FC = ()=>
{
const settings = useAppSelector(state=>state.settings.settings);
const settings = useSettings(state=>state.settings);
const [status, associationId, associationError, associate] = useAssociation([settings?.keePassHost, settings?.keePassPort]);

if(status === 'checking')
Expand Down Expand Up @@ -57,8 +56,9 @@ const Connection: React.FC = ()=>
{
const [ inputHost, setInputHost ] = React.useState<string>();
const [ inputPort, setInputPort ] = React.useState<number>();
const [settings, isSaving] = useAppSelector(state=>[state.settings.settings, state.settings.isSaving]);
const dispatch = useAppDispatch();
const settings = useSettings(state=>state.settings);
const isSaving = useSettings(state=>state.isSaving);
const saveSettings = useSettings(state=>state.saveSettings);

/** Host or port changed? */
const canApply = React.useMemo(()=>{
Expand All @@ -71,10 +71,10 @@ const Connection: React.FC = ()=>
/** Save host and port */
const onApply = React.useCallback(()=>{
log('debug', `Apply KeePassHttp settings (${inputHost || defaultSettings.keePassHost}:${inputPort || defaultSettings.keePassPort})`);
dispatch(saveSettings({
saveSettings({
keePassHost: inputHost || defaultSettings.keePassHost,
keePassPort: inputPort || defaultSettings.keePassPort,
}));
});
}, [inputHost, inputPort]);

return (<Box sx={{display: 'flex', flexDirection: 'column', gap: 1}}>
Expand Down
16 changes: 6 additions & 10 deletions src/components/Options/Options.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store, useAppDispatch, useAppSelector } from '../redux/Store';
import { loadSettings } from '../redux/Settings';
import Box from '@mui/material/Box';
import PageHeader from './PageHeader';
import Container from '@mui/material/Container';
Expand All @@ -18,6 +15,7 @@ import FindInPageIcon from '@mui/icons-material/FindInPage';
import FiberNewIcon from '@mui/icons-material/FiberNew';
import Connection from './Connection';
import Wrapper from '../Wrapper';
import useSettings from '../Hooks/Settings';
import Behaviour from './Behaviour';
import Appearance from './Appearance';
import Changelog from './Changelog';
Expand All @@ -29,7 +27,7 @@ export interface IProps

export const PaperGrid: React.FC<React.PropsWithChildren<{gridProps?: GridProps}>> = (props)=>
{
const isLoading = useAppSelector(state=>state.settings.isLoading);
const isLoading = useSettings(state=>state.isLoading);

if(isLoading)
return (<Skeleton sx={{minHeight: 48}} />)
Expand All @@ -47,15 +45,15 @@ const Options: React.FC<IProps> = (props)=>
{
const showUpdate = React.useMemo(()=>new URLSearchParams(location.search).get('update'), []);
const [selectedTab, setSelectedTab] = React.useState<'connection'|'behaviour'|'appearance'|'changelog'>(showUpdate?'changelog':'connection');
const errorMessage = useAppSelector(state=>state.settings.errorMessage);
const dispatch = useAppDispatch();
const errorMessage = useSettings(state=>state.errorMsg);
const loadSettings = useSettings(state=>state.loadSettings);

React.useEffect(()=>{
// Update the page title
document.title = EXTENSIONNAME;

// Load settings
dispatch(loadSettings());
loadSettings();
}, []);

return (<Wrapper sx={{minHeight: '100vh'}}>
Expand Down Expand Up @@ -105,9 +103,7 @@ const Options: React.FC<IProps> = (props)=>
export const mount = (container: HTMLElement, props: IProps): ()=>void =>
{
const reactContainer = createRoot(container);
reactContainer.render(<Provider store={store}>
<Options {...props} />
</Provider>);
reactContainer.render(<Options {...props} />);

return reactContainer.unmount;
};
Expand Down
23 changes: 11 additions & 12 deletions src/components/Options/SwitchOption.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React from 'react';
import Grid from '@mui/material/Grid';
import Switch from '@mui/material/Switch';
import { ISettings } from '../../Settings';
import { useAppDispatch, useAppSelector } from '../redux/Store';
import { saveSettings } from '../redux/Settings';
import { SxProps, Theme } from '@mui/material/styles';
import { ISettings } from '../../Settings';
import useSettings from '../Hooks/Settings';

export interface IProps
{
Expand All @@ -15,30 +14,30 @@ export interface IProps

const SwitchOption: React.FC<IProps> = (props)=>
{
const isSaving = useAppSelector(state=>state.settings.isSaving);
const optionEnabled = useAppSelector(state=>{
const isSaving = useSettings(state=>state.isSaving);
const optionEnabled = useSettings(state=>{
const [ option, subOption ] = props.option.split('.') as [keyof ISettings, keyof ISettings['theme']];
if(subOption)
return (state.settings.settings as any)?.[option]?.[subOption] as boolean;
return (state.settings as any)?.[option]?.[subOption] as boolean;
else
return state.settings.settings?.[option] as boolean;
return state.settings?.[option] as boolean;
});
const dispatch = useAppDispatch();
const saveSettings = useSettings(state=>state.saveSettings);

const setOption = React.useCallback((checked: boolean)=>{
const [ option, subOption ] = props.option.split('.') as [keyof ISettings, keyof ISettings['theme']];

if(subOption)
{
dispatch(saveSettings({
saveSettings({
[option]: {[subOption]: checked},
}));
});
}
else
{
dispatch(saveSettings({
saveSettings({
[option]: checked,
}));
});
}
}, [props.option])

Expand Down
64 changes: 0 additions & 64 deletions src/components/redux/Settings.ts

This file was deleted.

15 changes: 0 additions & 15 deletions src/components/redux/Store.ts

This file was deleted.

0 comments on commit 046e82a

Please sign in to comment.