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/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..e2a0a09
--- /dev/null
+++ b/src/hooks/useLocalStorage/useLocalStorage.stories.mdx
@@ -0,0 +1,117 @@
+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';
+
+
+
+# useLocalStorage
+
+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).
+
+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.
+
+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);
+```
+
+## 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 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)),
+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 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 only update the cached value in React state.
+
+## Examples
+
+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 (`boolean` value)
+
+
+
+### 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
new file mode 100644
index 0000000..04ff2ae
--- /dev/null
+++ b/src/hooks/useLocalStorage/useLocalStorage.stories.tsx
@@ -0,0 +1,271 @@
+import * as React from 'react';
+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('exampleTextField', '');
+ return (
+
+ );
+};
+
+export const PersistentCheckboxExample: React.FunctionComponent = () => {
+ const [isChecked, setIsChecked] = useLocalStorage('exampleCheckboxChecked', false);
+ return (
+
+ );
+};
+
+export const WelcomeModalExample: React.FunctionComponent = () => {
+ const ExamplePage: React.FunctionComponent = () => {
+ const [isModalDisabled, setIsModalDisabled] = useLocalStorage('welcomeModalDisabled', false);
+ const [isModalOpen, setIsModalOpen] = React.useState(!isModalDisabled);
+ return (
+ <>
+
+ Example Page Title
+ You reached the example page!
+
+ If you checked the "don't show this again" box, try reloading the page
+ 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! Tell your users what this page is all about, but
+ let them choose not to be annoyed with it every time.
+
+
+
+
+ >
+ );
+ };
+
+ // 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 (
+
+ );
+ }
+ 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 (
+
+ );
+ };
+ 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 (
+