Skip to content

Commit

Permalink
✨ implement a memoryStorage fallback, resolve #2
Browse files Browse the repository at this point in the history
  • Loading branch information
astoilkov committed Sep 3, 2024
1 parent e61e7b9 commit 4059262
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 15 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@
{
"name": "import *",
"path": "index.js",
"limit": "1.5 kB",
"limit": "1.7 kB",
"brotli": false
},
{
"name": "import *",
"path": "index.js",
"limit": "700 B"
"limit": "750 B"
}
]
}
23 changes: 23 additions & 0 deletions src/memoryStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class MemoryStorage {
#storage = new Map<string, string>();

getItem(key: string): string | null {
if (this.#storage.has(key)) {
const value = this.#storage.get(key);
return value === undefined ? "undefined" : value;
}
return null;
}

setItem(key: string, value: string): void {
this.#storage.set(key, value);
}

removeItem(key: string): void {
this.#storage.delete(key);
}
}

const memoryStorage = new MemoryStorage();

export default memoryStorage;
16 changes: 13 additions & 3 deletions src/useStorageState.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import memoryStorage from "./memoryStorage";
import type { Dispatch, SetStateAction } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'

export type StorageStateOptions<T> = {
defaultValue?: T | (() => T)
storage?: Storage
storage?: StorageLike
sync?: boolean
storeDefault?: boolean
serializer?: {
Expand All @@ -20,6 +21,12 @@ export type StorageState<T> = [
removeItem: () => void,
]

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

export default function useStorageState(
key: string,
options?: StorageStateOptions<undefined>,
Expand All @@ -41,7 +48,10 @@ export default function useStorageState<T = undefined>(
return useStorage(
key,
defaultValue,
options?.storage,
options?.storage
?? goodTry(() => localStorage)
?? goodTry(() => sessionStorage)
?? memoryStorage,
options?.sync,
options?.storeDefault,
serializer?.parse,
Expand All @@ -52,7 +62,7 @@ export default function useStorageState<T = undefined>(
function useStorage<T>(
key: string,
defaultValue: T | undefined,
storage: Storage = goodTry(() => localStorage) ?? sessionStorage,
storage: StorageLike,
sync: boolean = true,
storeDefault: boolean = false,
parse: (value: string) => unknown = parseJSON,
Expand Down
70 changes: 60 additions & 10 deletions test/browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
* @vitest-environment jsdom
*/

import util from 'node:util'
import superjson from 'superjson'
import { act, render, renderHook } from '@testing-library/react'
import React, { useEffect, useLayoutEffect, useMemo } from 'react'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import useStorageState from '../src/useStorageState.js'
import util from "node:util";
import superjson from "superjson";
import { act, render, renderHook } from "@testing-library/react";
import React, { useEffect, useLayoutEffect, useMemo } from "react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import useStorageState from "../src/useStorageState.js";
import memoryStorage from "../src/memoryStorage";

beforeEach(() => {
// Throw an error when `console.error()` is called. This is especially useful in a React tests
Expand All @@ -24,10 +25,12 @@ beforeEach(() => {
// - "Warning: Cannot update a component (`Component`) while rendering a different component
// (`Component`). To locate the bad setState() call inside `Component`, follow the stack trace
// as described in https://reactjs.org/link/setstate-in-render"
vi.spyOn(console, 'error').mockImplementation((format: string, ...args: any[]) => {
throw new Error(util.format(format, ...args))
})
})
vi.spyOn(console, "error").mockImplementation(
(format: string, ...args: any[]) => {
throw new Error(util.format(format, ...args));
},
);
});

afterEach(() => {
try {
Expand Down Expand Up @@ -173,6 +176,29 @@ describe('useLocalStorageState()', () => {
}).not.toThrow()
})

// https://github.com/astoilkov/use-storage-state/issues/2
test('Chrome option to not allow sites to save data on device', () => {
// in Safari, even just accessing `localStorage` throws "SecurityError: The operation is
// insecure."
vi.spyOn(window, 'localStorage', 'get').mockImplementation(() => {
throw new Error()
})
vi.spyOn(window, 'sessionStorage', 'get').mockImplementation(() => {
throw new Error()
})

const { result } = renderHook(() =>
useStorageState<string | undefined>('set-item-will-throw', { defaultValue: '' }),
)

expect(() => {
act(() => {
const setValue = result.current[1]
setValue('will-throw')
})
}).not.toThrow()
})

test('can set value to `undefined`', () => {
const { result: resultA, unmount } = renderHook(() =>
useStorageState<string[] | undefined>('todos', {
Expand Down Expand Up @@ -679,5 +705,29 @@ describe('useLocalStorageState()', () => {
expect(localStorage.getItem('count')).toBe('2')

})

test('memoryStorage', () => {
const { result } = renderHook(() =>
useStorageState<number>('count', {
storage: memoryStorage,
})
)
const [,setCount,removeItem] = result.current

act(() => {
removeItem()
})
expect(result.current[0]).toBe(undefined)

act(() => {
setCount(1)
})
expect(result.current[0]).toBe(1)

act(() => {
setCount(undefined)
})
expect(result.current[0]).toBe(undefined)
})
})
})

0 comments on commit 4059262

Please sign in to comment.