Skip to content

Commit

Permalink
Rework useStorageValue to more simple and robust variant (#960)
Browse files Browse the repository at this point in the history
feat: rework useStorageValue to more simple and robust variant

BREAKING CHANGE: new implementation brings different API.
It is not backward compatible!

Co-authored-by: Arttu Olli <[email protected]>
  • Loading branch information
xobotyi and ArttuOll authored Nov 3, 2022
1 parent 84d029f commit 7bcc385
Show file tree
Hide file tree
Showing 10 changed files with 449 additions and 789 deletions.
24 changes: 7 additions & 17 deletions src/useLocalStorageValue/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,15 @@ interface ExampleProps {
* LocalStorage key to manage.
*/
key: string;
/**
* Subscribe to window's `storage` event.
*/
handleStorageEvent: boolean;
/**
* Isolate hook from others on page - it will not receive updates from other hooks with the same key.
*/
isolated: boolean;
}

export const Example: React.FC<ExampleProps> = ({
key = 'react-hookz-ls-test',
defaultValue = '@react-hookz is awesome',
handleStorageEvent = true,
isolated = false,
}) => {
const [value, setValue, removeValue] = useLocalStorageValue(key, defaultValue, {
handleStorageEvent,
isolated,
const lsVal = useLocalStorageValue(key, {
defaultValue,
initializeWithValue: true,
});

return (
Expand All @@ -40,12 +30,12 @@ export const Example: React.FC<ExampleProps> = ({
<br />
<input
type="text"
value={value}
value={lsVal.value}
onChange={(ev) => {
setValue(ev.currentTarget.value);
lsVal.set(ev.currentTarget.value);
}}
/>{' '}
<button onClick={removeValue}>clear storage value</button>
/>
<button onClick={lsVal.remove}>remove storage value</button>
</div>
);
};
40 changes: 18 additions & 22 deletions src/useLocalStorageValue/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Example } from './example.stories';
import { ImportPath } from '../../__docs__/ImportPath';
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
import { Example } from './example.stories'
import { ImportPath } from '../../__docs__/ImportPath'
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'

<Meta title="Side-effect/useLocalStorageValue" component={Example} />

Expand All @@ -13,16 +13,13 @@ Manages a single LocalStorage key.
- Synchronized between all hooks on the page with the same key.
- SSR compatible.

> **_This hook provides stable API, meaning returned methods does not change between renders_**
> This hook uses `useSafeState` underneath, so it is safe to use its `setState` in async hooks.
> **_This hook provides stable API, meaning the returned methods do not change between renders._**
> Does not allow usage of `null` value, since JSON allows serializing `null` values - it would be
> impossible to separate null value fom 'no such value' API result which is also `null`.
> While using SSR, to avoid hydration mismatch, consider setting `initializeWithStorageValue` option
> to `false`, this will yield `undefined` state on first render and defer value fetch till effects
> execution stage.
> Due to support of SSR this hook returns undefined on first render even if value is there, to avoid
> this behavior set the `initializeWithValue` option to true.
#### Example

Expand Down Expand Up @@ -50,23 +47,22 @@ function useLocalStorageValue<T>(

<ImportPath />


#### Arguments

- **key** _`string`_ - LocalStorage key to manage.
- **defaultValue** _`T | null`_ _(default: null)_ - Default value to return in case key not
presented in LocalStorage.
- **options** _`object`_ - Hook options:
- **isolated** _`boolean`_ _(default: false)_ - Disable synchronisation with other hook instances
with the same key on the same page.
- **handleStorageEvent** _`boolean`_ _(default: true)_ - Subscribe to window's `storage` event.
- **storeDefaultValue** _`boolean`_ _(default: false)_ - store default value.
- **initializeWithStorageValue** _`boolean`_ _(default: true)_ - fetch storage value on first
render. If set to `false` will make hook to yield `undefined` state on first render and defer
value fetch till effects execution stage.
- **defaultValue** _`T | null`_ - Value to return if `key` is not present in LocalStorage.
- **initializeWithValue** _`boolean`_ _(default: true)_ - Fetch storage value on first render. If
set to `false` will make the hook yield `undefined` on first render and defer fetching of the
value until effects are executed.

#### Return

0. **state** - LocalStorage item value or default value in case of item absence.
1. **setValue** - Method to set new item value.
2. **removeValue** - Method to remove item from storage.
3. **fetchValue** - Method to pull value from localStorage.
Object with following properties. Note that this object changes with value while its methods are
stable between renders, thus it is safe to pass them as props.
- **value** - LocalStorage value of the given `key` argument or `defaultValue`, if the key was not
present.
- **set** - Method to set a new value for the managed `key`.
- **remove** - Method to remove the current value of `key`.
- **fetch** - Method to manually retrieve the value of `key`.
76 changes: 24 additions & 52 deletions src/useLocalStorageValue/useLocalStorageValue.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
HookReturn,
UseStorageValueOptions,
useStorageValue,
UseStorageValueOptions,
UseStorageValueResult,
} from '../useStorageValue/useStorageValue';
import { isBrowser, noop } from '../util/const';

let IS_LOCAL_STORAGE_AVAILABLE = false;
let IS_LOCAL_STORAGE_AVAILABLE: boolean;

try {
IS_LOCAL_STORAGE_AVAILABLE = isBrowser && !!window.localStorage;
Expand All @@ -15,62 +15,34 @@ try {
IS_LOCAL_STORAGE_AVAILABLE = false;
}

interface UseLocalStorageValue {
<T = unknown>(key: string, defaultValue?: null, options?: UseStorageValueOptions): HookReturn<
T,
typeof defaultValue,
UseStorageValueOptions<true | undefined>
>;

<T = unknown>(
key: string,
defaultValue: null,
options: UseStorageValueOptions<false>
): HookReturn<T, typeof defaultValue, typeof options>;

<T>(key: string, defaultValue: T, options?: UseStorageValueOptions): HookReturn<
T,
typeof defaultValue,
UseStorageValueOptions<true | undefined>
>;

<T>(key: string, defaultValue: T, options: UseStorageValueOptions<false>): HookReturn<
T,
typeof defaultValue,
typeof options
>;

<T>(key: string, defaultValue?: T | null, options?: UseStorageValueOptions): HookReturn<
T,
typeof defaultValue,
typeof options
>;
}
type UseLocalStorageValue = <
Type,
Default extends Type = Type,
Initialize extends boolean | undefined = boolean | undefined
>(
key: string,
options?: UseStorageValueOptions<Type, Initialize>
) => UseStorageValueResult<Type, Default, Initialize>;

/**
* Manages a single localStorage key.
*
* @param key Storage key to manage
* @param defaultValue Default value to yield in case the key is not in storage
* @param options
*/
export const useLocalStorageValue: UseLocalStorageValue = IS_LOCAL_STORAGE_AVAILABLE
? <T>(
key: string,
defaultValue: T | null = null,
options: UseStorageValueOptions = {}
): HookReturn<T, typeof defaultValue, typeof options> =>
useStorageValue(localStorage, key, defaultValue, options)
: <T>(
key: string,
defaultValue: T | null = null,
options: UseStorageValueOptions = {}
): HookReturn<T, typeof defaultValue, typeof options> => {
/* istanbul ignore next */
export const useLocalStorageValue: UseLocalStorageValue = !IS_LOCAL_STORAGE_AVAILABLE
? <
Type,
Default extends Type = Type,
Initialize extends boolean | undefined = boolean | undefined
>(
_key: string,
_options?: UseStorageValueOptions<Type, Initialize>
): UseStorageValueResult<Type, Default, Initialize> => {
if (isBrowser && process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console
console.warn('LocalStorage is not available in this environment');
}

return [undefined, noop, noop, noop];
return { value: undefined as Type, set: noop, remove: noop, fetch: noop };
}
: (key, options) => {
return useStorageValue(localStorage, key, options);
};
23 changes: 5 additions & 18 deletions src/useSessionStorageValue/__docs__/example.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,13 @@ interface ExampleProps {
* SessionStorage key to manage.
*/
key: string;
/**
* Subscribe to window's `storage` event.
*/
handleStorageEvent: boolean;
/**
* Isolate hook from others on page - it will not receive updates from other hooks with the same key.
*/
isolated: boolean;
}

export const Example: React.FC<ExampleProps> = ({
key = 'react-hookz-ss-test',
defaultValue = '@react-hookz is awesome',
handleStorageEvent = true,
isolated = false,
}) => {
const [value, setValue, removeValue] = useSessionStorageValue(key, defaultValue, {
handleStorageEvent,
isolated,
});
const ssVal = useSessionStorageValue(key, { defaultValue, initializeWithValue: true });

return (
<div>
Expand All @@ -40,12 +27,12 @@ export const Example: React.FC<ExampleProps> = ({
<br />
<input
type="text"
value={value}
value={ssVal.value}
onChange={(ev) => {
setValue(ev.currentTarget.value);
ssVal.set(ev.currentTarget.value);
}}
/>{' '}
<button onClick={removeValue}>clear storage value</button>
/>
<button onClick={ssVal.remove}>remove storage value</button>
</div>
);
};
39 changes: 17 additions & 22 deletions src/useSessionStorageValue/__docs__/story.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Example } from './example.stories';
import { ImportPath } from '../../__docs__/ImportPath';
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
import { Example } from './example.stories'
import { ImportPath } from '../../__docs__/ImportPath'
import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs'

<Meta title="Side-effect/useSessionStorageValue" component={Example} />

Expand All @@ -13,16 +13,13 @@ Manages a single SessionStorage key.
- Synchronized between all hooks on the page with the same key.
- SSR compatible.

> **_This hook provides stable API, meaning returned methods does not change between renders_**
> This hook uses `useSafeState` underneath, so it is safe to use its `setState` in async hooks.
> **_This hook provides stable API, meaning the returned methods do not change between renders._**
> Does not allow usage of `null` value, since JSON allows serializing `null` values - it would be
> impossible to separate null value fom 'no such value' API result which is also `null`.
> While using SSR, to avoid hydration mismatch, consider setting `initializeWithStorageValue` option
> to `false`, this will yield `undefined` state on first render and defer value fetch till effects
> execution stage.
> Due to support of SSR this hook returns undefined on first render even if value is there, to avoid
> this behavior set the `initializeWithValue` option to true.
#### Example

Expand Down Expand Up @@ -53,20 +50,18 @@ function useSessionStorageValue<T>(
#### Arguments

- **key** _`string`_ - SessionStorage key to manage.
- **defaultValue** _`T | null`_ _(default: null)_ - Default value to return in case key not
presented in SessionStorage.
- **options** _`object`_ - Hook options:
- **isolated** _`boolean`_ _(default: false)_ - Disable synchronisation with other hook instances
with the same key on same page.
- **handleStorageEvent** _`boolean`_ _(default: true)_ - Subscribe to window's `storage` event.
- **storeDefaultValue** _`boolean`_ _(default: false)_ - store default value.
- **initializeWithStorageValue** _`boolean`_ _(default: true)_ - fetch storage value on first
render. If set to `false` will make hook to yield `undefined` state on first render and defer
value fetch till effects execution stage.
- **defaultValue** _`T | null`_ - Value to return if `key` is not present in SessionStorage.
- **initializeWithValue** _`boolean`_ _(default: true)_ - Fetch storage value on first render. If
set to `false` will make the hook yield `undefined` on first render and defer fetching of the
value until effects are executed.

#### Return

0. **state** - SessionStorage item value or default value in case of item absence.
1. **setValue** - Method to set new item value.
2. **removeValue** - Method to remove item from storage.
3. **fetchValue** - Method to pull value from sessionStorage.
Object with following properties. Note that this object changes with value while its methods are
stable between renders, thus it is safe to pass them as props.
- **value** - SessionStorage value of the given `key` argument or `defaultValue`, if the key was not
present.
- **set** - Method to set a new value for the managed `key`.
- **remove** - Method to remove the current value of `key`.
- **fetch** - Method to manually retrieve the value of `key`.
Loading

0 comments on commit 7bcc385

Please sign in to comment.