From c983ea8c5899e12c5ed6d89427bc17337dc85816 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Mon, 8 Aug 2022 17:35:50 -0400 Subject: [PATCH 1/7] Initial version of useLocalStorage from crane-ui-plugin --- src/hooks/useLocalStorage/index.ts | 1 + .../useLocalStorage.stories.mdx | 48 +++++++++++ .../useLocalStorage.stories.tsx | 80 +++++++++++++++++++ src/hooks/useLocalStorage/useLocalStorage.ts | 26 ++++++ src/index.ts | 1 + 5 files changed, 156 insertions(+) create mode 100644 src/hooks/useLocalStorage/index.ts create mode 100644 src/hooks/useLocalStorage/useLocalStorage.stories.mdx create mode 100644 src/hooks/useLocalStorage/useLocalStorage.stories.tsx create mode 100644 src/hooks/useLocalStorage/useLocalStorage.ts diff --git a/src/hooks/useLocalStorage/index.ts b/src/hooks/useLocalStorage/index.ts new file mode 100644 index 0000000..01cda12 --- /dev/null +++ b/src/hooks/useLocalStorage/index.ts @@ -0,0 +1 @@ +export * from './useLocalStorage'; diff --git a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx new file mode 100644 index 0000000..06ee574 --- /dev/null +++ b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx @@ -0,0 +1,48 @@ +import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks'; +import { useLocalStorage } from './useLocalStorage'; +import { + PersistentTextFieldExample, + PersistentCheckboxExample, + WelcomeModalExample, +} from './useLocalStorage.stories.tsx'; +import GithubLink from '../../../.storybook/helpers/GithubLink'; + + + +# useLocalStorage + +A custom hook resembling `React.useState` which persists the value using the browser's localStorage API. + +Can be used as a drop-in replacement for `useState`, but the stored value must be a string. + +``` +TODO: put type signature here +``` + +## Using default values + +TODO + +## Using boolean values + +TODO + +## Examples + +### Persistent text field + + + + + +### Persistent checkbox + + + + + +### Modal with "Don't show this again" checkbox + + + + diff --git a/src/hooks/useLocalStorage/useLocalStorage.stories.tsx b/src/hooks/useLocalStorage/useLocalStorage.stories.tsx new file mode 100644 index 0000000..85b810f --- /dev/null +++ b/src/hooks/useLocalStorage/useLocalStorage.stories.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { Button, Checkbox, TextContent, Text, TextInput, Modal } from '@patternfly/react-core'; +import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; +import { useLocalStorage } from './useLocalStorage'; + +export const PersistentTextFieldExample: React.FunctionComponent = () => { + const [value, setValue] = useLocalStorage('exampleTextFieldValue'); + return ; +}; + +export const PersistentCheckboxExample: React.FunctionComponent = () => { + const [isCheckedStr, setIsCheckedStr] = useLocalStorage('exampleCheckboxChecked'); + return ( + setIsCheckedStr(checked ? 'true' : 'false')} + /> + ); +}; + +export const WelcomeModalExample: React.FunctionComponent = () => { + const ExamplePage: React.FunctionComponent = () => { + const [isModalDisabledStr, setIsModalDisabledStr] = useLocalStorage('welcomeModalDisabled'); + const [isWelcomeModalOpen, setIsModalOpen] = React.useState(isModalDisabledStr !== 'true'); + + return ( + <> + + Some Page Title + You reached the example page! + + If you checked the "don't show this again" box, try reloading the page + and you'll see the welcome modal won't come back. If you want to see it again, + clear your browsing data or try an incognito tab. + + + + setIsModalOpen(false)} + > + + + This is an introductory message that you will see each time you visit the example + page, unless you check the box below! + + + { + setIsModalDisabledStr(checked ? 'true' : 'false'); + }} + /> + + + ); + }; + + // The following is just for the embedded example to work. In a real app you'd just follow the above ExamplePage. + const [isExamplePageOpen, setIsExamplePageOpen] = React.useState(false); + if (!isExamplePageOpen) { + return ( + <> + + This button will take you to a page that has a welcome modal! + + + + ); + } + return ; +}; diff --git a/src/hooks/useLocalStorage/useLocalStorage.ts b/src/hooks/useLocalStorage/useLocalStorage.ts new file mode 100644 index 0000000..b69f75e --- /dev/null +++ b/src/hooks/useLocalStorage/useLocalStorage.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; + +export const useLocalStorage = (key: string): [string | null, (value: string | null) => void] => { + const [value, setCachedValue] = React.useState(window.localStorage.getItem(key)); + + const setValue = (newValue: string | null) => { + if (newValue === null) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, newValue); + } + window.dispatchEvent(new StorageEvent('storage', { key, newValue })); + }; + + React.useEffect(() => { + const onStorageUpdated = (event: StorageEvent) => { + if (event.key === key) setCachedValue(event.newValue); + }; + window.addEventListener('storage', onStorageUpdated); + return () => { + window.removeEventListener('storage', onStorageUpdated); + }; + }, [key]); + + return [value, setValue]; +}; diff --git a/src/index.ts b/src/index.ts index 00b4d7d..0c9766b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,5 +5,6 @@ export * from './components/LoadingEmptyState'; export * from './hooks/useSelectionState'; export * from './hooks/useFormState'; +export * from './hooks/useLocalStorage'; export * from './modules/kube-client'; From 2b8c7649b06d675189e11bc0085dcbeb45363ad9 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 9 Aug 2022 16:42:16 -0400 Subject: [PATCH 2/7] Enhance implementation and add examples --- package.json | 2 +- .../useLocalStorage.stories.mdx | 88 +++++- .../useLocalStorage.stories.tsx | 253 +++++++++++++++--- src/hooks/useLocalStorage/useLocalStorage.ts | 59 +++- yarn.lock | 2 +- 5 files changed, 350 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 9105465..1ea089f 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "sass-loader": "^11.0.1", "semantic-release": "^19.0.3", "ts-jest": "^26.5.5", - "tslib": "^2.2.0", + "tslib": "^2.4.0", "typescript": "^4.2.4" }, "config": { diff --git a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx index 06ee574..c47c7a7 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx +++ b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx @@ -1,9 +1,14 @@ import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs/blocks'; import { useLocalStorage } from './useLocalStorage'; import { + ClearAllExamplesButton, + PersistentCounterExample, PersistentTextFieldExample, PersistentCheckboxExample, WelcomeModalExample, + ReusedKeyExample, + CustomHookExample, + ComplexValueExample, } from './useLocalStorage.stories.tsx'; import GithubLink from '../../../.storybook/helpers/GithubLink'; @@ -11,31 +16,60 @@ import GithubLink from '../../../.storybook/helpers/GithubLink'; # useLocalStorage -A custom hook resembling `React.useState` which persists the value using the browser's localStorage API. +A custom hook resembling `React.useState` which persists and synchronizes any JSON-serializable value using the +browser's [localStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). -Can be used as a drop-in replacement for `useState`, but the stored value must be a string. +The value will persist across page reloads, and updates to the value will cause a re-render anywhere `useLocalStorage` +is called with the same unique `key` string. The value identified by a `key` will stay in sync between multiple calls +within the same page, across multiple pages and across multiple tabs/windows via a `StorageEvent` listener. The value +will only be lost if the user switches to a different browser or a fresh incognito session, or if they clear their +browsing data. -``` -TODO: put type signature here +Just like `useState`, `useLocalStorage` returns the current value and setter function in an array tuple so they can be +given arbitrary names using [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), +and it supports TypeScript [generics](https://www.typescriptlang.org/docs/handbook/generics.html) and can optionally +infer its return types based on the `defaultValue`. + +```ts +const [value, setValue] = useLocalStorage(key: string, defaultValue: T); ``` -## Using default values +## Notes -TODO +On first render, `value` will either be synchronously loaded from storage or will fall back to `defaultValue` if there +is no value in storage. If necessary, the storage value can be fully removed by calling `setValue(undefined)`, which +will re-render with `value` set back to `defaultValue`. Note that this is distinct from calling `setValue(null)`, which +will persist and synchronize the `null` value. -## Using boolean values +If `setValue` is called with a value which cannot be serialized to JSON +([see Exceptions here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions)), +or if an error is thrown by the `localStorage` API, the error is caught and logged to the console and the `value` falls +back to the `defaultValue`. -TODO +`useLocalStorage` is safe to use in a unit testing environment where `window.localStorage` is not available. +If `window` is undefined, the hook will simply always use the `defaultValue` and the setter function will do nothing. ## Examples -### Persistent text field +For each of these examples, try opening this page in multiple windows side-by-side to see the synchronization in action, +and try reloading the page to see the persistence in action. + +### Persistent counter (`number` value) + +The classic `useState` example, but persistent. The `count` variable's type here is inferred as a primitive `number`, +not a `string`. + + + + + +### Persistent text field (`string` value) -### Persistent checkbox +### Persistent checkbox (`boolean` value) @@ -43,6 +77,40 @@ TODO ### Modal with "Don't show this again" checkbox +This button will simulate navigating to a page that has a welcome modal which can be disabled for future visits by the user. + + +### Sharing a persistent value by reusing the same `key` + +The same value can be rendered and updated in multiple different components and in the same component rendered in multiple places +by reusing the same unique `key` string. In this implementation, you will need to use the same `defaultValue` for each instance +or you'll end up with the values out of sync until one of them is changed by the user. See the next example for an improvement. + + + + + +### Sharing a persistent value by factoring out into a custom hook + +Sharing the same value can be done more easily (and without the repeated `defaultValue` problem) by creating a custom hook +wrapping your `useLocalStorage` call and using the custom hook in multiple places. + + + + + +### Persistent array and object values + +Any JSON-serializable value can be used with `useLocalStorage` such as an array or an object. Note that this may not be +desirable; if possible it is simpler to use multiple instances of `useLocalStorage` with separate keys and individual primitive values. + + + + + + + + diff --git a/src/hooks/useLocalStorage/useLocalStorage.stories.tsx b/src/hooks/useLocalStorage/useLocalStorage.stories.tsx index 85b810f..04ff2ae 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.stories.tsx +++ b/src/hooks/useLocalStorage/useLocalStorage.stories.tsx @@ -1,61 +1,101 @@ import * as React from 'react'; -import { Button, Checkbox, TextContent, Text, TextInput, Modal } from '@patternfly/react-core'; +import * as yup from 'yup'; +import { + Button, + Checkbox, + TextContent, + Text, + TextInput, + Modal, + NumberInput, + List, + ListItem, + Form, + FormGroup, +} from '@patternfly/react-core'; import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; import { useLocalStorage } from './useLocalStorage'; +import { useFormState, useFormField } from '../useFormState'; +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); + return ( + setCount(count - 1)} + onPlus={() => setCount(count + 1)} + onChange={(event) => setCount(Number(event.currentTarget.value))} + inputName="input" + inputAriaLabel="number input" + minusBtnAriaLabel="minus" + plusBtnAriaLabel="plus" + /> + ); +}; export const PersistentTextFieldExample: React.FunctionComponent = () => { - const [value, setValue] = useLocalStorage('exampleTextFieldValue'); - return ; + const [value, setValue] = useLocalStorage('exampleTextField', ''); + return ( + + ); }; export const PersistentCheckboxExample: React.FunctionComponent = () => { - const [isCheckedStr, setIsCheckedStr] = useLocalStorage('exampleCheckboxChecked'); + const [isChecked, setIsChecked] = useLocalStorage('exampleCheckboxChecked', false); return ( setIsCheckedStr(checked ? 'true' : 'false')} + label="I'm a persistent checkbox!" + isChecked={isChecked} + onChange={setIsChecked} /> ); }; export const WelcomeModalExample: React.FunctionComponent = () => { const ExamplePage: React.FunctionComponent = () => { - const [isModalDisabledStr, setIsModalDisabledStr] = useLocalStorage('welcomeModalDisabled'); - const [isWelcomeModalOpen, setIsModalOpen] = React.useState(isModalDisabledStr !== 'true'); - + const [isModalDisabled, setIsModalDisabled] = useLocalStorage('welcomeModalDisabled', false); + const [isModalOpen, setIsModalOpen] = React.useState(!isModalDisabled); return ( <> - Some Page Title + Example Page Title You reached the example page! If you checked the "don't show this again" box, try reloading the page - and you'll see the welcome modal won't come back. If you want to see it again, - clear your browsing data or try an incognito tab. + and returning here and you'll see the welcome modal won't come back. If you + want to see it again, clear your browsing data or try an incognito tab, or use the + "Clear localStorage for all examples" button above. - + setIsModalOpen(false)} + actions={[ + , + ]} > - + This is an introductory message that you will see each time you visit the example - page, unless you check the box below! + page, unless you check the box below! Tell your users what this page is all about, but + let them choose not to be annoyed with it every time. { - setIsModalDisabledStr(checked ? 'true' : 'false'); - }} + isChecked={isModalDisabled} + onChange={setIsModalDisabled} /> @@ -66,15 +106,166 @@ export const WelcomeModalExample: React.FunctionComponent = () => { const [isExamplePageOpen, setIsExamplePageOpen] = React.useState(false); if (!isExamplePageOpen) { return ( - <> - - This button will take you to a page that has a welcome modal! - - - + ); } return ; }; + +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'); + return ( +
+ + Component A + + +
+ ); + }; + const ComponentB: React.FunctionComponent = () => { + const [value] = useLocalStorage('exampleReusedKey', 'default value here'); + return ( +
+ + Component B + {value} + +
+ ); + }; + return ( + <> + + + + + ); +}; + +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'); + + // In a real app each of these components would be in separate files. + const ComponentA: React.FunctionComponent = () => { + const [value, setValue] = useMyStoredValue(); + return ( +
+ + Component A + + +
+ ); + }; + const ComponentB: React.FunctionComponent = () => { + const [value] = useLocalStorage('exampleReusedKey', 'default value here'); + return ( +
+ + Component B + {value} + +
+ ); + }; + return ( + <> + + + + + ); +}; + +export const ComplexValueExample: React.FunctionComponent = () => { + type Item = { name: string; description?: string }; + const [items, setItems] = useLocalStorage('exampleArray', []); + + const addForm = useFormState({ + name: useFormField('', yup.string().required().label('Name')), + description: useFormField('', yup.string().label('Description')), + }); + + return ( + <> + + Saved items + {items.length > 0 ? ( + + {items.map((item, index) => ( + + Name: {item.name} + {item.description ? <>, Description: {item.description} : null} + + + ))} + + ) : ( + No items yet + )} + + + Add item: + +
+ + + + + + + + ); +}; + +export const ClearAllExamplesButton: React.FunctionComponent = () => ( +
+ +
+); diff --git a/src/hooks/useLocalStorage/useLocalStorage.ts b/src/hooks/useLocalStorage/useLocalStorage.ts index b69f75e..5527700 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.ts +++ b/src/hooks/useLocalStorage/useLocalStorage.ts @@ -1,26 +1,63 @@ import * as React from 'react'; -export const useLocalStorage = (key: string): [string | null, (value: string | null) => void] => { - const [value, setCachedValue] = React.useState(window.localStorage.getItem(key)); +const getValueFromStorage = (key: string, defaultValue: T): T => { + if (typeof window === 'undefined') return defaultValue; + try { + const item = window.localStorage.getItem(key); + return item ? (JSON.parse(item) as T) : defaultValue; + } catch (error) { + console.error(error); + return defaultValue; + } +}; - const setValue = (newValue: string | null) => { - if (newValue === null) { - window.localStorage.removeItem(key); +const setValueInStorage = (key: string, newValue: T) => { + if (typeof window === 'undefined') return; + try { + if (newValue !== undefined) { + const newValueJSON = JSON.stringify(newValue); + window.localStorage.setItem(key, newValueJSON); + // setItem only causes the StorageEvent to be dispatched in other windows. We dispatch it here + // manually so that all instances of useLocalStorage on this window also react to this change. + window.dispatchEvent(new StorageEvent('storage', { key, newValue: newValueJSON })); } else { - window.localStorage.setItem(key, newValue); + window.localStorage.removeItem(key); + window.dispatchEvent(new StorageEvent('storage', { key, newValue: null })); } - window.dispatchEvent(new StorageEvent('storage', { key, newValue })); - }; + } catch (error) { + console.error(error); + } +}; + +export const useLocalStorage = ( + key: string, + defaultValue: T +): [T, React.Dispatch>] => { + const [cachedValue, setCachedValue] = React.useState(getValueFromStorage(key, defaultValue)); + + const setValue: React.Dispatch> = React.useCallback( + (newValueOrFn: T | ((prevState: T) => T)) => { + const newValue = + newValueOrFn instanceof Function + ? newValueOrFn(getValueFromStorage(key, defaultValue)) + : newValueOrFn; + setValueInStorage(key, newValue); + }, + [key, defaultValue] + ); React.useEffect(() => { + if (typeof window === 'undefined') return; const onStorageUpdated = (event: StorageEvent) => { - if (event.key === key) setCachedValue(event.newValue); + if (event.key === key) { + setCachedValue(event.newValue ? JSON.parse(event.newValue) : defaultValue); + } }; window.addEventListener('storage', onStorageUpdated); return () => { window.removeEventListener('storage', onStorageUpdated); }; - }, [key]); + }, [key, defaultValue]); - return [value, setValue]; + return [cachedValue, setValue]; }; diff --git a/yarn.lock b/yarn.lock index 00b2b1c..de2e21c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14913,7 +14913,7 @@ tslib@^2.0.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== -tslib@^2.0.1, tslib@^2.2.0: +tslib@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== From f1785357f28b0aa54d8d920b05ef362f6f61c449 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 9 Aug 2022 17:18:38 -0400 Subject: [PATCH 3/7] Handle cache update in test environment --- src/hooks/useLocalStorage/useLocalStorage.stories.mdx | 5 +++-- src/hooks/useLocalStorage/useLocalStorage.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx index c47c7a7..a8c7fde 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx +++ b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx @@ -46,8 +46,9 @@ If `setValue` is called with a value which cannot be serialized to JSON or if an error is thrown by the `localStorage` API, the error is caught and logged to the console and the `value` falls back to the `defaultValue`. -`useLocalStorage` is safe to use in a unit testing environment where `window.localStorage` is not available. -If `window` is undefined, the hook will simply always use the `defaultValue` and the setter function will do nothing. +`useLocalStorage` is safe to use in a server-side rendering or unit testing environment where `window.localStorage` is +not available. If `window` is undefined, the hook will simply always use the `defaultValue` and the setter function +will do nothing. ## Examples diff --git a/src/hooks/useLocalStorage/useLocalStorage.ts b/src/hooks/useLocalStorage/useLocalStorage.ts index 5527700..90dbac9 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.ts +++ b/src/hooks/useLocalStorage/useLocalStorage.ts @@ -42,6 +42,10 @@ export const useLocalStorage = ( ? newValueOrFn(getValueFromStorage(key, defaultValue)) : newValueOrFn; setValueInStorage(key, newValue); + if (typeof window === 'undefined') { + // If we're in a server or test environment, the cache won't update automatically since there's no StorageEvent. + setCachedValue(newValue); + } }, [key, defaultValue] ); From 807cce1d0aff571adaa501d89a5311a79805fbcf Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 9 Aug 2022 17:21:04 -0400 Subject: [PATCH 4/7] Rephrase --- src/hooks/useLocalStorage/useLocalStorage.stories.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx index a8c7fde..d05ec2b 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx +++ b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx @@ -37,9 +37,9 @@ const [value, setValue] = useLocalStorage(key: string, defaultValue: T); ## Notes On first render, `value` will either be synchronously loaded from storage or will fall back to `defaultValue` if there -is no value in storage. If necessary, the storage value can be fully removed by calling `setValue(undefined)`, which -will re-render with `value` set back to `defaultValue`. Note that this is distinct from calling `setValue(null)`, which -will persist and synchronize the `null` value. +is no value in storage. If necessary, the data can be fully removed from `localStorage` by calling +`setValue(undefined)`, which will re-render with `value` set back to `defaultValue`. Note that this is distinct from +calling `setValue(null)`, which will persist and synchronize the `null` value. If `setValue` is called with a value which cannot be serialized to JSON ([see Exceptions here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#exceptions)), From 594bddfa366c58611f0f9b7e72ee928cc46795a9 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 9 Aug 2022 17:21:59 -0400 Subject: [PATCH 5/7] Type fix --- src/hooks/useLocalStorage/useLocalStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useLocalStorage/useLocalStorage.ts b/src/hooks/useLocalStorage/useLocalStorage.ts index 90dbac9..48b22b7 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.ts +++ b/src/hooks/useLocalStorage/useLocalStorage.ts @@ -11,7 +11,7 @@ const getValueFromStorage = (key: string, defaultValue: T): T => { } }; -const setValueInStorage = (key: string, newValue: T) => { +const setValueInStorage = (key: string, newValue: T | undefined) => { if (typeof window === 'undefined') return; try { if (newValue !== undefined) { From 2edf31d8c2194add5f3ed93f0b672e69a39310d6 Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Tue, 9 Aug 2022 17:25:05 -0400 Subject: [PATCH 6/7] Clarification --- src/hooks/useLocalStorage/useLocalStorage.stories.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx index d05ec2b..e2a0a09 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.stories.mdx +++ b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx @@ -48,7 +48,7 @@ back to the `defaultValue`. `useLocalStorage` is safe to use in a server-side rendering or unit testing environment where `window.localStorage` is not available. If `window` is undefined, the hook will simply always use the `defaultValue` and the setter function -will do nothing. +will only update the cached value in React state. ## Examples From 0fc47868005235708c8e4ffac78c9b8e3f9351df Mon Sep 17 00:00:00 2001 From: Mike Turley Date: Wed, 10 Aug 2022 11:08:19 -0400 Subject: [PATCH 7/7] Clearer variable name --- src/hooks/useLocalStorage/useLocalStorage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useLocalStorage/useLocalStorage.ts b/src/hooks/useLocalStorage/useLocalStorage.ts index 48b22b7..9ba0a39 100644 --- a/src/hooks/useLocalStorage/useLocalStorage.ts +++ b/src/hooks/useLocalStorage/useLocalStorage.ts @@ -3,8 +3,8 @@ import * as React from 'react'; const getValueFromStorage = (key: string, defaultValue: T): T => { if (typeof window === 'undefined') return defaultValue; try { - const item = window.localStorage.getItem(key); - return item ? (JSON.parse(item) as T) : defaultValue; + const itemJSON = window.localStorage.getItem(key); + return itemJSON ? (JSON.parse(itemJSON) as T) : defaultValue; } catch (error) { console.error(error); return defaultValue;