Skip to content

Commit

Permalink
feat(hooks): Add useLocalStorage hook and documentation (#109)
Browse files Browse the repository at this point in the history
* Initial version of useLocalStorage from crane-ui-plugin

* Enhance implementation and add examples

* Handle cache update in test environment

* Rephrase

* Type fix

* Clarification

* Clearer variable name
  • Loading branch information
mturley authored Aug 10, 2022
1 parent 4f6ff1b commit ddbaef0
Show file tree
Hide file tree
Showing 7 changed files with 459 additions and 2 deletions.
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

0 comments on commit ddbaef0

Please sign in to comment.