Skip to content

Commit

Permalink
add storage class to handle gracefull degration (#61)
Browse files Browse the repository at this point in the history
* add storage class to handle gracefull degration

* refactor storage to use IProxyStorage

* mock storage implementation for integration tests

* remove unused useRef from src/use-localstorage.ts

Co-authored-by: Pier-Luc Gendreau <[email protected]>

Co-authored-by: Pier-Luc Gendreau <[email protected]>
  • Loading branch information
gerbyzation and Zertz authored Nov 16, 2020
1 parent 2b85b87 commit 7b83c40
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 100 deletions.
5 changes: 3 additions & 2 deletions src/local-storage-events.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { storage } from './storage'
interface KVP<K, V> {
key: K,
value: V
Expand Down Expand Up @@ -49,7 +50,7 @@ export function isTypeOfLocalStorageChanged<TValue>(evt: any): evt is LocalStora
*/
export function writeStorage<TValue>(key: string, value: TValue) {
try {
localStorage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : `${value}`);
storage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : `${value}`);
window.dispatchEvent(new LocalStorageChanged({ key, value }));
} catch (err) {
if (err instanceof TypeError && err.message.includes('circular structure')) {
Expand Down Expand Up @@ -86,6 +87,6 @@ export function writeStorage<TValue>(key: string, value: TValue) {
* @param {string} key The key of the item you wish to delete from localStorage.
*/
export function deleteFromStorage(key: string) {
localStorage.removeItem(key);
storage.removeItem(key);
window.dispatchEvent(new LocalStorageChanged({ key, value: null }))
}
70 changes: 70 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Test if localStorage API is available
* From https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#Feature-detecting_localStorage
* @returns {boolean}
*/
export function localStorageAvailable(): boolean {
try {
var x = '@rehooks/local-storage:' + new Date().toISOString();
localStorage.setItem(x, x);
localStorage.removeItem(x);
return true;
}
catch(e) {
return e instanceof DOMException && (
// everything except Firefox
e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
(localStorage && localStorage.length !== 0);
}
}


interface IProxyStorage {
getItem(key: string): string | null
setItem(Key: string, value: string): void
removeItem(key: string): void
}

export class LocalStorageProxy implements IProxyStorage {
getItem(key: string): string | null {
return localStorage.getItem(key)
}

setItem(key: string, value: string): void {
localStorage.setItem(key, value)
}

removeItem(key: string): void {
localStorage.removeItem(key)
}
}

export class MemoryStorageProxy implements IProxyStorage {
private _memoryStorage = new Map<string, string>()

getItem(key: string): string | null {
return this._memoryStorage.get(key) ?? null
}

setItem(key: string, value: string): void {
this._memoryStorage.set(key, value)
}

removeItem(key: string): void {
this._memoryStorage.delete(key)
}
}

const proxyStorageFrom = (isAvailable: boolean) => isAvailable
? new LocalStorageProxy()
: new MemoryStorageProxy()

export const storage = proxyStorageFrom(localStorageAvailable())
7 changes: 4 additions & 3 deletions src/use-localstorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
LocalStorageChanged,
isTypeOfLocalStorageChanged,
} from './local-storage-events';
import { storage } from './storage'
import { useEffect, useState, useCallback } from 'react';

/**
Expand Down Expand Up @@ -54,9 +55,9 @@ export function useLocalStorage<TValue = string>(
defaultValue: TValue | null = null,
) {
const [localState, updateLocalState] = useState<TValue | null>(
localStorage.getItem(key) === null
storage.getItem(key) === null
? defaultValue
: tryParse(localStorage.getItem(key)!)
: tryParse(storage.getItem(key)!)
);

const onLocalStorageChange = (event: LocalStorageChanged<TValue> | StorageEvent) => {
Expand Down Expand Up @@ -84,7 +85,7 @@ export function useLocalStorage<TValue = string>(

// Write default value to the local storage if there currently isn't any value there.
// Don't however write a defaultValue that is null otherwise it'll trigger infinite updates.
if (localStorage.getItem(key) === null && defaultValue !== null) {
if (storage.getItem(key) === null && defaultValue !== null) {
writeStorage(key, defaultValue);
}

Expand Down
2 changes: 2 additions & 0 deletions test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useLocalStorage, writeStorage, deleteFromStorage } from '../src';
import { renderHook } from '@testing-library/react-hooks';
import { render, fireEvent, act, cleanup } from '@testing-library/react';

import { storage } from '../src/storage'


afterEach(() => {
cleanup();
Expand Down
51 changes: 51 additions & 0 deletions test/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { MemoryStorageProxy, LocalStorageProxy } from '../src/storage'

describe('ProxyStorage', () => {
describe('when localStorage is available', () => {
// check assumption localStorage is available in tests
window.localStorage.setItem('hi', 'hi')
expect(window.localStorage.getItem('hi')).toEqual('hi')

const storage = new LocalStorageProxy()

it('calls localStorage.setItem', () => {
storage.setItem('key1', 'value2')
expect(localStorage.getItem('key1')).toEqual('value2')
})

it('calls localStorage.getItem', () => {
localStorage.setItem('key2', 'value1')
expect(storage.getItem('key2')).toEqual('value1')
})

it('calls localStorage.removeItem', () => {
localStorage.setItem('key3', 'value1')
expect(storage.removeItem('key3')).toEqual(undefined)
expect(localStorage.getItem('key3')).toEqual(null)
})
})

describe('when localStorage is not available', () => {
// check assumption localStorage is available in tests
window.localStorage.setItem('hi', 'hi')
expect(window.localStorage.getItem('hi')).toEqual('hi')

const storage = new MemoryStorageProxy()

it('returns default instead of calling localStorage.setItem', () => {
expect(storage.setItem('key4', 'value2')).toEqual(undefined)
expect(localStorage.getItem('key4')).toEqual(null)
})

it('returns default instead of calling localStorage.getItem', () => {
localStorage.setItem('key5', 'value1')
expect(storage.getItem('key5')).toEqual(null)
})

it('returns default instead of calling localStorage.getItem', () => {
localStorage.setItem('key6', 'value1')
expect(storage.removeItem('key6')).toEqual(undefined)
expect(localStorage.getItem('key6')).toEqual('value1')
})
})
})
Loading

0 comments on commit 7b83c40

Please sign in to comment.