Merge pull request #896 from ayush987goyal/pr/useLocalStorage-update

feat: add serializer/deserializer option to useLocalStorage
This commit is contained in:
Vadim Dalecky 2020-01-16 00:38:13 -08:00 committed by GitHub
commit 84d29b2413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 13 deletions

View File

@ -2,11 +2,10 @@
React side-effect hook that manages a single `localStorage` key.
## Usage
```jsx
import {useLocalStorage} from 'react-use';
import { useLocalStorage } from 'react-use';
const Demo = () => {
const [value, setValue] = useLocalStorage('my-key', 'foo');
@ -21,15 +20,21 @@ const Demo = () => {
};
```
## Reference
```js
useLocalStorage(key);
useLocalStorage(key, initialValue);
useLocalStorage(key, initialValue, raw);
useLocalStorage(key, initialValue, { raw: true });
useLocalStorage(key, initialValue, {
raw: false,
serializer: (value: T) => string,
deserializer: (value: string) => T,
});
```
- `key` — `localStorage` key to manage.
- `initialValue` — initial value to set, if value in `localStorage` is empty.
- `raw` — boolean, if set to `true`, hook will not attempt to JSON serialize stored values.
- `serializer` — custom serializer (defaults to `JSON.stringify`)
- `deserializer` — custom deserializer (defaults to `JSON.parse`)

View File

@ -89,6 +89,7 @@
"gh-pages": "2.2.0",
"husky": "3.1.0",
"jest": "24.9.0",
"jest-localstorage-mock": "^2.4.0",
"keyboardjs": "2.5.1",
"lint-staged": "9.5.0",
"markdown-loader": "5.1.0",
@ -159,6 +160,9 @@
"coverageDirectory": "coverage",
"testMatch": [
"<rootDir>/tests/**/*.test.(ts|tsx)"
],
"setupFiles": [
"./tests/setupTests.ts"
]
}
}

View File

@ -1,22 +1,37 @@
import { useEffect, useState } from 'react';
import { isClient } from './util';
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
type parserOptions<T> =
| {
raw: true;
}
| {
raw: false;
serializer: (value: T) => string;
deserializer: (value: string) => T;
};
const useLocalStorage = <T>(key: string, initialValue?: T, raw?: boolean): [T, Dispatch<SetStateAction<T>>] => {
const useLocalStorage = <T>(
key: string,
initialValue?: T,
options?: parserOptions<T>
): [T, React.Dispatch<React.SetStateAction<T>>] => {
if (!isClient) {
return [initialValue as T, () => {}];
}
// Use provided serializer/deserializer or the default ones
const serializer = options ? (options.raw ? String : options.serializer) : JSON.stringify;
const deserializer = options ? (options.raw ? String : options.deserializer) : JSON.parse;
const [state, setState] = useState<T>(() => {
try {
const localStorageValue = localStorage.getItem(key);
if (typeof localStorageValue !== 'string') {
localStorage.setItem(key, raw ? String(initialValue) : JSON.stringify(initialValue));
return initialValue;
if (localStorageValue !== null) {
return deserializer(localStorageValue);
} else {
return raw ? localStorageValue : JSON.parse(localStorageValue || 'null');
initialValue && localStorage.setItem(key, serializer(initialValue));
return initialValue;
}
} catch {
// If user is in private mode or has storage restriction
@ -28,8 +43,7 @@ const useLocalStorage = <T>(key: string, initialValue?: T, raw?: boolean): [T, D
useEffect(() => {
try {
const serializedState = raw ? String(state) : JSON.stringify(state);
localStorage.setItem(key, serializedState);
localStorage.setItem(key, serializer(state));
} catch {
// If user is in private mode or has storage restriction
// localStorage can throw. Also JSON.stringify can throw.

1
tests/setupTests.ts Normal file
View File

@ -0,0 +1 @@
import 'jest-localstorage-mock';

View File

@ -0,0 +1,95 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useLocalStorage } from '../src';
const STRINGIFIED_VALUE = '{"a":"b"}';
const JSONIFIED_VALUE = { a: 'b' };
afterEach(() => {
localStorage.clear();
jest.clearAllMocks();
});
it('should return undefined if no initialValue provided and localStorage empty', () => {
const { result } = renderHook(() => useLocalStorage('some_key'));
expect(result.current[0]).toBeUndefined();
});
it('should set the value from existing localStorage key', () => {
const key = 'some_key';
localStorage.setItem(key, STRINGIFIED_VALUE);
const { result } = renderHook(() => useLocalStorage(key));
expect(result.current[0]).toEqual(JSONIFIED_VALUE);
});
it('should return initialValue if localStorage empty and set that to localStorage', () => {
const key = 'some_key';
const value = 'some_value';
const { result } = renderHook(() => useLocalStorage(key, value));
expect(result.current[0]).toBe(value);
expect(localStorage.__STORE__[key]).toBe(`"${value}"`);
});
it('should return the value from localStorage if exists even if initialValue provied', () => {
const key = 'some_key';
localStorage.setItem(key, STRINGIFIED_VALUE);
const { result } = renderHook(() => useLocalStorage(key, 'random_value'));
expect(result.current[0]).toEqual(JSONIFIED_VALUE);
});
it('should properly update the localStorage on change', () => {
const key = 'some_key';
const updatedValue = { b: 'a' };
const expectedValue = '{"b":"a"}';
const { result } = renderHook(() => useLocalStorage(key));
act(() => {
result.current[1](updatedValue);
});
expect(result.current[0]).toBe(updatedValue);
expect(localStorage.__STORE__[key]).toBe(expectedValue);
});
describe('Options with raw true', () => {
it('should set the value from existing localStorage key', () => {
const key = 'some_key';
localStorage.setItem(key, STRINGIFIED_VALUE);
const { result } = renderHook(() => useLocalStorage(key, '', { raw: true }));
expect(result.current[0]).toEqual(STRINGIFIED_VALUE);
});
it('should return initialValue if localStorage empty and set that to localStorage', () => {
const key = 'some_key';
const { result } = renderHook(() => useLocalStorage(key, STRINGIFIED_VALUE, { raw: true }));
expect(result.current[0]).toBe(STRINGIFIED_VALUE);
expect(localStorage.__STORE__[key]).toBe(STRINGIFIED_VALUE);
});
});
describe('Options with raw false and provided serializer/deserializer', () => {
const serializer = (_: string) => '321';
const deserializer = (_: string) => '123';
it('should return valid serialized value from existing localStorage key', () => {
const key = 'some_key';
localStorage.setItem(key, STRINGIFIED_VALUE);
const { result } = renderHook(() =>
useLocalStorage(key, STRINGIFIED_VALUE, { raw: false, serializer, deserializer })
);
expect(result.current[0]).toBe('123');
});
});

View File

@ -8615,6 +8615,11 @@ jest-leak-detector@^24.9.0:
jest-get-type "^24.9.0"
pretty-format "^24.9.0"
jest-localstorage-mock@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/jest-localstorage-mock/-/jest-localstorage-mock-2.4.0.tgz#c6073810735dd3af74020ea6c3885ec1cc6d0d13"
integrity sha512-/mC1JxnMeuIlAaQBsDMilskC/x/BicsQ/BXQxEOw+5b1aGZkkOAqAF3nu8yq449CpzGtp5jJ5wCmDNxLgA2m6A==
jest-matcher-utils@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"