Skip to content

Commit

Permalink
Update usage of useLocalStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
tnagorra committed Sep 6, 2024
1 parent 89a4223 commit 751c7a9
Show file tree
Hide file tree
Showing 20 changed files with 146 additions and 387 deletions.
14 changes: 6 additions & 8 deletions src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
} from '#generated/types/graphql';
import useThrottledValue from '#hooks/useThrottledValue';
import { getWindowSize } from '#utils/common';
import { setToStorage } from '#utils/localStorage';
import { defaultConfigValue } from '#utils/constants';

import wrappedRoutes, { unwrappedRoutes } from './routes';

Expand Down Expand Up @@ -111,13 +111,11 @@ function App() {
const [userAuth, setUserAuth] = useState<UserAuth>();
const [size, setSize] = useState<SizeContextProps>(getWindowSize);
const [ready, setReady] = useState(false);
const [storageState, setStorageState] = useState<LocalStorageContextProps['storageState']>({});

useEffect(() => {
Object.keys(storageState).forEach((key) => {
setToStorage(key, storageState[key].value);
});
}, [storageState]);
const [storageState, setStorageState] = useState<LocalStorageContextProps['storageState']>({
'timur-config': {
defaultValue: defaultConfigValue,
},
});

const debouncedSize = useThrottledValue(size);

Expand Down
2 changes: 1 addition & 1 deletion src/App/routes/PageError/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
.footer {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--spacing-md);

.actions {
Expand Down
14 changes: 3 additions & 11 deletions src/components/Page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ import SizeContext from '#contexts/size';
import useDebouncedValue from '#hooks/useDebouncedValue';
import useLocalStorage from '#hooks/useLocalStorage';
import useSetFieldValue from '#hooks/useSetFieldValue';
import {
defaultConfigValue,
KEY_CONFIG_STORAGE,
} from '#utils/constants';
import { ConfigStorage } from '#utils/types';

import styles from './styles.module.css';

Expand Down Expand Up @@ -62,17 +57,14 @@ function Page(props: Props) {

const { width } = useContext(SizeContext);

const [storedState, setStoredState] = useLocalStorage<ConfigStorage>(
KEY_CONFIG_STORAGE,
defaultConfigValue,
);
const [storedConfig, setStoredConfig] = useLocalStorage('timur-config');

const {
startSidebarShown,
endSidebarShown,
} = storedState;
} = storedConfig;

const setFieldValue = useSetFieldValue(setStoredState);
const setFieldValue = useSetFieldValue(setStoredConfig);

const setSidebarShown = useCallback(
(newValue: boolean) => setFieldValue(newValue, 'startSidebarShown'),
Expand Down
21 changes: 15 additions & 6 deletions src/contexts/localStorage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { createContext } from 'react';

export type StoredValue<VALUE = unknown> = {
timestamp: number;
value: VALUE;
import { PutNull } from '#utils/common';
import { defaultConfigValue } from '#utils/constants';
import { ConfigStorage } from '#utils/types';

export type StoredValue<VALUE extends object> = {
value?: PutNull<VALUE>;
defaultValue: VALUE;
};
export type StorageState = {
'timur-config': StoredValue<ConfigStorage>,
};
type StorageState<VALUE = unknown> = Record<string, StoredValue<VALUE>>;

export interface LocalStorageContextProps {
storageState: StorageState;
setStorageState: React.Dispatch<React.SetStateAction<StorageState>>;
}

// FIXME: replace this with simpler alternative
const LocalStorageContext = createContext<LocalStorageContextProps>({
storageState: {},
storageState: {
'timur-config': {
defaultValue: defaultConfigValue,
},
},
setStorageState: () => {
// eslint-disable-next-line no-console
console.error('LocalStorageContext::setStorage() called without a provider');
Expand Down
112 changes: 67 additions & 45 deletions src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,87 @@ import {
useCallback,
useContext,
useEffect,
useState,
useMemo,
} from 'react';
import { isDefined } from '@togglecorp/fujs';

import LocalStorageContext, { StoredValue } from '#contexts/localStorage';
import { isCallable } from '#utils/common';
import { getFromStorage } from '#utils/localStorage';
import LocalStorageContext, { StorageState } from '#contexts/localStorage';
import {
isCallable,
putNull,
putUndefined,
} from '#utils/common';
import {
getFromStorage,
setToStorage,
} from '#utils/localStorage';

import useDebouncedValue from './useDebouncedValue';
function useLocalStorage<K extends keyof StorageState>(key: K) {
const {
storageState,
setStorageState,
} = useContext(LocalStorageContext);

function useLocalStorage<T>(
key: string,
defaultValue: T,
debounce = 200,
) {
const [value, setValue] = useState<StoredValue<T>>(
() => {
const fromStorage = getFromStorage<T>(key);
type T = StorageState[K];

const hasReadValue = isDefined(storageState[key].value);

return {
timestamp: new Date().getTime(),
value: fromStorage ?? defaultValue,
};
useEffect(
() => {
if (hasReadValue) {
return;
}
const val = getFromStorage<T['value']>(key);
setStorageState((oldValue) => ({
...oldValue,
[key]: {
...oldValue[key],
value: val,
},
}));
},
[key, hasReadValue, setStorageState],
);

const { storageState, setStorageState } = useContext(LocalStorageContext);
const debouncedValue = useDebouncedValue(value, debounce);
const setValue: React.Dispatch<React.SetStateAction<NonNullable<T['value']>>> = useCallback(
(newValue) => {
setStorageState((oldValue) => {
const oldValueValue = oldValue[key].value;
const oldValueDefaultValue = oldValue[key].defaultValue;

useEffect(() => {
if (!storageState[key]) {
return;
}
const resolvedValue = isCallable(newValue)
? newValue(oldValueValue ?? oldValueDefaultValue)
: newValue;

if (storageState[key].timestamp > value.timestamp) {
setValue(storageState[key] as StoredValue<T>);
}
}, [storageState, key, value]);
setToStorage(key, putNull(resolvedValue));

useEffect(() => {
setStorageState((oldStorageValue) => ({
...oldStorageValue,
[key]: debouncedValue,
}));
}, [debouncedValue, key, setStorageState]);
return {
...oldValue,
[key]: {
...oldValue[key],
value: resolvedValue,
},
} satisfies StorageState;
});
},
[key, setStorageState],
);

const setValueSafe = useCallback((newValue: T | ((v: T) => T)) => {
setValue((oldValue) => {
const resolvedValue = isCallable(newValue)
? newValue(oldValue.value)
: newValue;
const { value } = storageState[key];
const { defaultValue } = storageState[key];

return {
timestamp: new Date().getTime(),
value: resolvedValue,
} satisfies StoredValue<T>;
});
}, []);
const finalValue = useMemo(
() => putUndefined({
...defaultValue,
...value,
}),
[defaultValue, value],
);

return [value.value, setValueSafe] as const;
return [
finalValue,
setValue,
] as const;
}

export default useLocalStorage;
2 changes: 1 addition & 1 deletion src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,10 @@ body {
overflow-x: hidden;
color: var(--color-text);
font-family: var(--font-family-sans-serif);
font-size: var(--font-size-md);
font-weight: var(--font-weight-normal);
font-style: normal;
font-optical-sizing: auto;
font-size: var(--font-size-md);

@media screen {
margin: 0;
Expand Down
34 changes: 33 additions & 1 deletion src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export function getDurationNumber(value: string | undefined) {
}
// decimal
if (value.match(/^\d{0,2}\.\d{1,2}$/)) {
return Number(value);
return Math.round(Number(value) * 60);
}
// hh:mm
// hh:m
Expand Down Expand Up @@ -332,3 +332,35 @@ export function groupListByAttributes<LIST_ITEM, ATTRIBUTE>(

return groupedItems;
}

export type PutNull<T extends object> = {
[key in keyof T]: T[key] extends undefined ? null : T[key];
}
export function putNull<T extends object>(value: T) {
type PartialWithNull<Q> = {
[P in keyof Q]: Q[P] | null;
};
const copy: PartialWithNull<T> = { ...value };
Object.keys(copy).forEach((key) => {
const safeKey = key as keyof T;
if (copy[safeKey] === undefined) {
copy[safeKey] = null;
}
});

return copy as PutNull<T>;
}
export type PutUndefined<T extends object> = {
[key in keyof T]: T[key] extends null ? undefined : T[key];
}
export function putUndefined<T extends object>(value: T) {
const copy: Partial<T> = { ...value };
Object.keys(copy).forEach((key) => {
const safeKey = key as keyof T;
if (copy[safeKey] === null) {
copy[safeKey] = undefined;
}
});

return copy as PutUndefined<T>;
}
2 changes: 0 additions & 2 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import {
NumericOption,
} from './types';

export const KEY_CONFIG_STORAGE = 'timur-config';

export const defaultConfigValue: ConfigStorage = {
defaultTaskType: undefined,
defaultTaskStatus: 'DONE',
Expand Down
7 changes: 6 additions & 1 deletion src/utils/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isNotDefined } from '@togglecorp/fujs';

export function getFromStorage<T>(key: string) {
const val = localStorage.getItem(key);
return val === null || val === undefined ? undefined : JSON.parse(val) as T;
Expand All @@ -7,6 +9,9 @@ export function removeFromStorage(key: string) {
localStorage.removeItem(key);
}

export function setToStorage<T>(key: string, value: T) {
export function setToStorage(key: string, value: unknown) {
if (isNotDefined(value)) {
localStorage.clearItem(key);
}
localStorage.setItem(key, JSON.stringify(value));
}
Loading

0 comments on commit 751c7a9

Please sign in to comment.