mirror of
https://github.com/streamich/react-use.git
synced 2026-01-25 14:17:16 +00:00
Merge pull request #896 from ayush987goyal/pr/useLocalStorage-update
feat: add serializer/deserializer option to useLocalStorage
This commit is contained in:
commit
84d29b2413
@ -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`)
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
1
tests/setupTests.ts
Normal file
@ -0,0 +1 @@
|
||||
import 'jest-localstorage-mock';
|
||||
95
tests/useLocalStorage.test.ts
Normal file
95
tests/useLocalStorage.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user