Skip to content

Commit

Permalink
feat: add serializer/deserializer option to useLocalStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
ayush987goyal committed Jan 15, 2020
1 parent 2b1bd55 commit 5316510
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 13 deletions.
13 changes: 9 additions & 4 deletions docs/useLocalStorage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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`)
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -159,6 +160,9 @@
"coverageDirectory": "coverage",
"testMatch": [
"<rootDir>/tests/**/*.test.(ts|tsx)"
],
"setupFiles": [
"./tests/setupTests.ts"
]
}
}
32 changes: 23 additions & 9 deletions src/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -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) {
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
Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions tests/setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import 'jest-localstorage-mock';
95 changes: 95 additions & 0 deletions tests/useLocalStorage.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 5316510

Please sign in to comment.