From a534395f7f3fbcfcf3dc82ecc21d432ee64f46b3 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 2 Oct 2023 10:01:05 -0400 Subject: [PATCH 1/2] feat(useStorage): Add isEnabled option to useStorage/useLocalStorage/useSessionStorage via refactor to use options objects BREAKING CHANGE: useLocalStorage and useSessionStorage (via useStorage) require an object of named options instead of multiple function arguments Signed-off-by: Mike Turley --- .../useStorage/useLocalStorage.stories.mdx | 2 +- .../useStorage/useLocalStorage.stories.tsx | 34 +++++++++++---- .../useStorage/useSessionStorage.stories.mdx | 2 +- .../useStorage/useSessionStorage.stories.tsx | 6 +-- src/hooks/useStorage/useStorage.ts | 41 +++++++++++-------- 5 files changed, 55 insertions(+), 30 deletions(-) diff --git a/src/hooks/useStorage/useLocalStorage.stories.mdx b/src/hooks/useStorage/useLocalStorage.stories.mdx index 1557d4f..c2b4793 100644 --- a/src/hooks/useStorage/useLocalStorage.stories.mdx +++ b/src/hooks/useStorage/useLocalStorage.stories.mdx @@ -33,7 +33,7 @@ and it supports TypeScript [generics](https://www.typescriptlang.org/docs/handbo infer its return types based on the `defaultValue`. ```ts -const [value, setValue] = useLocalStorage(key: string, defaultValue: T); +const [value, setValue] = useLocalStorage({ key: string, defaultValue: T }); ``` ## Notes diff --git a/src/hooks/useStorage/useLocalStorage.stories.tsx b/src/hooks/useStorage/useLocalStorage.stories.tsx index f54d461..26caf75 100644 --- a/src/hooks/useStorage/useLocalStorage.stories.tsx +++ b/src/hooks/useStorage/useLocalStorage.stories.tsx @@ -20,7 +20,7 @@ import { ValidatedTextInput } from '../../components/ValidatedTextInput'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; export const PersistentCounterExample: React.FunctionComponent = () => { - const [count, setCount] = useLocalStorage('exampleCounter', 0); + const [count, setCount] = useLocalStorage({ key: 'exampleCounter', defaultValue: 0 }); return ( { }; export const PersistentTextFieldExample: React.FunctionComponent = () => { - const [value, setValue] = useLocalStorage('exampleTextField', ''); + const [value, setValue] = useLocalStorage({ key: 'exampleTextField', defaultValue: '' }); return ( { }; export const PersistentCheckboxExample: React.FunctionComponent = () => { - const [isChecked, setIsChecked] = useLocalStorage('exampleCheckboxChecked', false); + const [isChecked, setIsChecked] = useLocalStorage({ + key: 'exampleCheckboxChecked', + defaultValue: false, + }); return ( { export const WelcomeModalExample: React.FunctionComponent = () => { const ExamplePage: React.FunctionComponent = () => { - const [isModalDisabled, setIsModalDisabled] = useLocalStorage('welcomeModalDisabled', false); + const [isModalDisabled, setIsModalDisabled] = useLocalStorage({ + key: 'welcomeModalDisabled', + defaultValue: false, + }); const [isModalOpen, setIsModalOpen] = React.useState(!isModalDisabled); return ( <> @@ -121,7 +127,10 @@ export const WelcomeModalExample: React.FunctionComponent = () => { export const ReusedKeyExample: React.FunctionComponent = () => { // In a real app each of these components would be in separate files. const ComponentA: React.FunctionComponent = () => { - const [value, setValue] = useLocalStorage('exampleReusedKey', 'default value here'); + const [value, setValue] = useLocalStorage({ + key: 'exampleReusedKey', + defaultValue: 'default value here', + }); return (
@@ -136,7 +145,10 @@ export const ReusedKeyExample: React.FunctionComponent = () => { ); }; const ComponentB: React.FunctionComponent = () => { - const [value] = useLocalStorage('exampleReusedKey', 'default value here'); + const [value] = useLocalStorage({ + key: 'exampleReusedKey', + defaultValue: 'default value here', + }); return (
@@ -157,7 +169,8 @@ export const ReusedKeyExample: React.FunctionComponent = () => { export const CustomHookExample: React.FunctionComponent = () => { // This could be exported from its own file and imported in multiple component files. - const useMyStoredValue = () => useLocalStorage('myStoredValue', 'default defined once'); + const useMyStoredValue = () => + useLocalStorage({ key: 'myStoredValue', defaultValue: 'default defined once' }); // In a real app each of these components would be in separate files. const ComponentA: React.FunctionComponent = () => { @@ -176,7 +189,10 @@ export const CustomHookExample: React.FunctionComponent = () => { ); }; const ComponentB: React.FunctionComponent = () => { - const [value] = useLocalStorage('exampleReusedKey', 'default value here'); + const [value] = useLocalStorage({ + key: 'exampleReusedKey', + defaultValue: 'default value here', + }); return (
@@ -197,7 +213,7 @@ export const CustomHookExample: React.FunctionComponent = () => { export const ComplexValueExample: React.FunctionComponent = () => { type Item = { name: string; description?: string }; - const [items, setItems] = useLocalStorage('exampleArray', []); + const [items, setItems] = useLocalStorage({ key: 'exampleArray', defaultValue: [] }); const addForm = useFormState({ name: useFormField('', yup.string().required().label('Name')), diff --git a/src/hooks/useStorage/useSessionStorage.stories.mdx b/src/hooks/useStorage/useSessionStorage.stories.mdx index 37f0306..aba4e79 100644 --- a/src/hooks/useStorage/useSessionStorage.stories.mdx +++ b/src/hooks/useStorage/useSessionStorage.stories.mdx @@ -28,7 +28,7 @@ and it supports TypeScript [generics](https://www.typescriptlang.org/docs/handbo infer its return types based on the `defaultValue`. ```ts -const [value, setValue] = useSessionStorage(key: string, defaultValue: T); +const [value, setValue] = useSessionStorage({ key: string, defaultValue: T }); ``` ## Notes diff --git a/src/hooks/useStorage/useSessionStorage.stories.tsx b/src/hooks/useStorage/useSessionStorage.stories.tsx index 1809ff8..b33a815 100644 --- a/src/hooks/useStorage/useSessionStorage.stories.tsx +++ b/src/hooks/useStorage/useSessionStorage.stories.tsx @@ -18,7 +18,7 @@ import { ValidatedTextInput } from '../../components/ValidatedTextInput'; import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; export const PersistentCounterExample: React.FunctionComponent = () => { - const [count, setCount] = useSessionStorage('exampleCounter', 0); + const [count, setCount] = useSessionStorage({ key: 'exampleCounter', defaultValue: 0 }); return ( { }; export const PersistentTextFieldExample: React.FunctionComponent = () => { - const [value, setValue] = useSessionStorage('exampleTextField', ''); + const [value, setValue] = useSessionStorage({ key: 'exampleTextField', defaultValue: '' }); return ( { export const ComplexValueExample: React.FunctionComponent = () => { type Item = { name: string; description?: string }; - const [items, setItems] = useSessionStorage('exampleArray', []); + const [items, setItems] = useSessionStorage({ key: 'exampleArray', defaultValue: [] }); const addForm = useFormState({ name: useFormField('', yup.string().required().label('Name')), diff --git a/src/hooks/useStorage/useStorage.ts b/src/hooks/useStorage/useStorage.ts index 7e4805b..06e8152 100644 --- a/src/hooks/useStorage/useStorage.ts +++ b/src/hooks/useStorage/useStorage.ts @@ -35,30 +35,38 @@ const setValueInStorage = (storageType: StorageType, key: string, newValue: T } }; -const useStorage = ( - storageType: StorageType, - key: string, - defaultValue: T -): [T, React.Dispatch>] => { +interface IUseStorageOptions { + isEnabled?: boolean; + type: StorageType; + key: string; + defaultValue: T; +} + +const useStorage = ({ + isEnabled = true, + type, + key, + defaultValue, +}: IUseStorageOptions): [T, React.Dispatch>] => { const [cachedValue, setCachedValue] = React.useState( - getValueFromStorage(storageType, key, defaultValue) + getValueFromStorage(type, key, defaultValue) ); - const usingStorageEvents = storageType === 'localStorage' && typeof window !== 'undefined'; + const usingStorageEvents = type === 'localStorage' && typeof window !== 'undefined' && isEnabled; const setValue: React.Dispatch> = React.useCallback( (newValueOrFn: T | ((prevState: T) => T)) => { const newValue = newValueOrFn instanceof Function - ? newValueOrFn(getValueFromStorage(storageType, key, defaultValue)) + ? newValueOrFn(getValueFromStorage(type, key, defaultValue)) : newValueOrFn; - setValueInStorage(storageType, key, newValue); + setValueInStorage(type, key, newValue); if (!usingStorageEvents) { // The cache won't update automatically if there is no StorageEvent dispatched. setCachedValue(newValue); } }, - [storageType, key, defaultValue, usingStorageEvents] + [type, key, defaultValue, usingStorageEvents] ); React.useEffect(() => { @@ -77,12 +85,13 @@ const useStorage = ( return [cachedValue, setValue]; }; +export type UseStorageTypeOptions = Omit, 'type'>; + export const useLocalStorage = ( - key: string, - defaultValue: T -): [T, React.Dispatch>] => useStorage('localStorage', key, defaultValue); + options: UseStorageTypeOptions +): [T, React.Dispatch>] => useStorage({ ...options, type: 'localStorage' }); export const useSessionStorage = ( - key: string, - defaultValue: T -): [T, React.Dispatch>] => useStorage('sessionStorage', key, defaultValue); + options: UseStorageTypeOptions +): [T, React.Dispatch>] => + useStorage({ ...options, type: 'sessionStorage' }); From 4d777681393874b62f2a9d376f6d8b85f7fafdcf Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 2 Oct 2023 11:12:14 -0400 Subject: [PATCH 2/2] Fix broken storybook example Signed-off-by: Mike Turley --- src/hooks/useStorage/useLocalStorage.stories.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/hooks/useStorage/useLocalStorage.stories.tsx b/src/hooks/useStorage/useLocalStorage.stories.tsx index 26caf75..a42a68d 100644 --- a/src/hooks/useStorage/useLocalStorage.stories.tsx +++ b/src/hooks/useStorage/useLocalStorage.stories.tsx @@ -189,10 +189,7 @@ export const CustomHookExample: React.FunctionComponent = () => { ); }; const ComponentB: React.FunctionComponent = () => { - const [value] = useLocalStorage({ - key: 'exampleReusedKey', - defaultValue: 'default value here', - }); + const [value] = useMyStoredValue(); return (