Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hooks): Add useLocalStorage hook and documentation #109

Merged
merged 7 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useLocalStorage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useLocalStorage';
117 changes: 117 additions & 0 deletions src/hooks/useLocalStorage/useLocalStorage.stories.mdx
Original file line number Diff line number Diff line change
@@ -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';

<Meta title="Hooks/useLocalStorage" component={useLocalStorage} />

# 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<T>(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`.

<Canvas>
<Story story={PersistentCounterExample} />
</Canvas>

### Persistent text field (`string` value)

<Canvas>
<Story story={PersistentTextFieldExample} />
</Canvas>

### Persistent checkbox (`boolean` value)

<Canvas>
<Story story={PersistentCheckboxExample} />
</Canvas>

### 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.

<Canvas>
<Story story={WelcomeModalExample} />
</Canvas>

### 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.

<Canvas>
<Story story={ReusedKeyExample} />
</Canvas>

### 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.

<Canvas>
<Story story={CustomHookExample} />
</Canvas>

### 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.

<Canvas>
<Story story={ComplexValueExample} />
</Canvas>

<ClearAllExamplesButton />

<GithubLink path="src/hooks/useLocalStorage/useLocalStorage.ts" />
Loading