Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add storage class to handle gracefull degration #61

Merged
merged 4 commits into from
Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 }))
}
64 changes: 64 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* 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);
}
}


/**
* Provides a proxy to localStorage, returning default return values
* if `localStorage` is not available
*/
export class ProxyStorage {
gerbyzation marked this conversation as resolved.
Show resolved Hide resolved
available: boolean

constructor() {
this.available = localStorageAvailable()
}

getItem(key: string): string | null {
if (this.available === true) {
return localStorage.getItem(key)
}
return null
}

setItem(key: string, value: string): void {
if (this.available === true) {
return localStorage.setItem(key, value)
}
return undefined
}

removeItem(key: string): void {
if (this.available === true) {
return localStorage.removeItem(key)
}
return undefined
}
}


export const storage = new ProxyStorage()
9 changes: 5 additions & 4 deletions src/use-localstorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
LocalStorageChanged,
isTypeOfLocalStorageChanged,
} from './local-storage-events';
import { useEffect, useState, useCallback } from 'react';
import { storage } from './storage'
import { useEffect, useState, useCallback, useRef } from 'react';
gerbyzation marked this conversation as resolved.
Show resolved Hide resolved

/**
* This exists for trying to serialize the value back to JSON.
Expand Down Expand Up @@ -53,9 +54,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 @@ -83,7 +84,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
56 changes: 56 additions & 0 deletions test/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ProxyStorage } 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 ProxyStorage()

it('initiates ProxyStorage as available', () => {
expect(storage.available).toEqual(true)
})

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 ProxyStorage()
storage.available = false

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')
})
})
})
45 changes: 45 additions & 0 deletions test/use-localstorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useLocalStorage, deleteFromStorage } from '../src';
import { renderHook, act } from '@testing-library/react-hooks';

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

describe('Module: use-localstorage', () => {
describe('useLocalStorage', () => {
it('is callable', () => {
Expand Down Expand Up @@ -130,5 +132,48 @@ describe('Module: use-localstorage', () => {
});
});
});

describe("when localStorage api is disabled", () => {
beforeAll(() => storage.available = false)

afterAll(() => storage.available = true)

it('should return default value', () => {

const key = 'car';
const defaultValue = 'beamer'

const { result } = renderHook(() => useLocalStorage(key, defaultValue))

expect(result.current[0]).toBe(defaultValue)
})

it('still tracks state in localStorage', () => {
const key = 'car';
const defaultValue = 'beamer'

const { result } = renderHook(() => useLocalStorage(key, defaultValue))

expect(result.current[0]).toBe(defaultValue)

act(() => result.current[1]('merc'))
expect(result.current[0]).toEqual('merc')
expect(localStorage.getItem('car')).toEqual(null)
})

it('defaults back to defaultValue in localState when deleted', () => {
const key = 'unavailableAPI';
const defaultValue = 'localStorage'

const { result } = renderHook(() => useLocalStorage(key, defaultValue))

expect(result.current[0]).toBe(defaultValue)

act(() => result.current[1]('webrtc'))
expect(result.current[0]).toEqual('webrtc')
act(() => deleteFromStorage('unavailableAPI'))
expect(result.current[0]).toEqual(defaultValue)
})
})
});
});