From 4cf78c4043212df91e33ecfe0aa9866bf1ae9092 Mon Sep 17 00:00:00 2001 From: Jon Linkens Date: Mon, 26 Jun 2023 11:50:59 +0100 Subject: [PATCH] Add useReadSessionStorage hook --- packages/usehooks-ts/src/index.ts | 1 + .../useReadSessionStorage.demo.tsx | 8 ++ .../useReadSessionStorage.md | 13 +++ .../useReadSessionStorage.test.ts | 83 +++++++++++++++++++ .../useReadSessionStorage.ts | 74 +++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.demo.tsx create mode 100644 packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.md create mode 100644 packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.test.ts create mode 100644 packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.ts diff --git a/packages/usehooks-ts/src/index.ts b/packages/usehooks-ts/src/index.ts index 62808bd5..bcb0e7b7 100644 --- a/packages/usehooks-ts/src/index.ts +++ b/packages/usehooks-ts/src/index.ts @@ -25,6 +25,7 @@ export * from './useMap/useMap' export * from './useMediaQuery/useMediaQuery' export * from './useOnClickOutside/useOnClickOutside' export * from './useReadLocalStorage/useReadLocalStorage' +export * from './useReadSessionStorage/useReadSessionStorage' export * from './useScreen/useScreen' export * from './useScript/useScript' export * from './useSessionStorage/useSessionStorage' diff --git a/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.demo.tsx b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.demo.tsx new file mode 100644 index 00000000..fc0ba867 --- /dev/null +++ b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.demo.tsx @@ -0,0 +1,8 @@ +import { useReadSessionStorage } from '..' + +export default function Component() { + // Assuming a value was set in session storage with this key + const sessionId = useReadSessionStorage('sessionId') + + return

SessionId: {sessionId ?? 'not set'}

+} diff --git a/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.md b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.md new file mode 100644 index 00000000..468d2138 --- /dev/null +++ b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.md @@ -0,0 +1,13 @@ +This React Hook allows you to read a value from session storage by its key. It can be useful if you just want to read without passing a default value. +If the window object is not present (as in SSR) or if the value doesn't exist, `useReadSessionStorage()` will return `null`. + +**Options:** + +As with `useSessionStorage()`, there is some additional config you can pass to this hook with a second, `options` argument: + +- `parseAsJson: boolean` - defaults to `true`. If you have a previously set session storage value that you don't want to parse using `JSON.parse`, you can set this to `false` +- `parser: (value: string | null) => T` - a custom parser if you have stored a session storage value with a custom serializer function + +Related hooks: + +If you want to be able to change the value, use [`useSessionStorage()`](/react-hook/use-session-storage). diff --git a/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.test.ts b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.test.ts new file mode 100644 index 00000000..523b20c9 --- /dev/null +++ b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.test.ts @@ -0,0 +1,83 @@ +import { renderHook } from '@testing-library/react-hooks/dom' + +import { useReadSessionStorage } from './useReadSessionStorage' + +class SessionStorageMock { + store: Record = {} + + clear() { + this.store = {} + } + + getItem(key: string) { + return this.store[key] || null + } + + setItem(key: string, value: unknown) { + this.store[key] = value + '' + } + + removeItem(key: string) { + delete this.store[key] + } +} + +Object.defineProperty(window, 'sessionStorage', { + value: new SessionStorageMock(), +}) + +describe('useReadSessionStorage()', () => { + beforeEach(() => { + window.sessionStorage.clear() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + test('returns null if no item in session storage', () => { + const { result } = renderHook(() => useReadSessionStorage('key')) + + expect(result.current).toBe(null) + }) + + test('returns object if no options passed', () => { + const obj = { value: 'test' } + window.sessionStorage.setItem('key', JSON.stringify(obj)) + + const { result } = renderHook(() => useReadSessionStorage('key')) + + expect(result.current).toStrictEqual(obj) + }) + + test('returns string if parseAsJson is false', () => { + window.sessionStorage.setItem('key', 'value') + + const { result } = renderHook(() => + useReadSessionStorage('key', { parseAsJson: false }), + ) + + expect(result.current).toBe('value') + }) + + test('returns expected value with custom parser', () => { + window.sessionStorage.setItem('key', 'value') + + const { result } = renderHook(() => + useReadSessionStorage('key', { parser: doubleLetters }), + ) + + expect(result.current).toBe('vvaalluuee') + }) +}) + +function doubleLetters(value: string | null) { + if (value === null) { + return '' + } + let result = '' + for (let i = 0; i < value.length; i++) { + result += value[i] + value[i] + } + return result +} diff --git a/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.ts b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.ts new file mode 100644 index 00000000..77919357 --- /dev/null +++ b/packages/usehooks-ts/src/useReadSessionStorage/useReadSessionStorage.ts @@ -0,0 +1,74 @@ +import { useCallback, useEffect, useState } from 'react' + +import { useEventListener } from '..' + +type Options = { + parseAsJson: boolean + parser: (value: string | null) => T +} + +type Value = T | null + +export function useReadSessionStorage( + key: string, + { + parseAsJson = true, + parser = parseAsJson ? parseJSON : castValue, + }: Partial> = {}, +): Value { + // Get from session storage then + // parse stored json or return initialValue + + const readValue = useCallback((): Value => { + // Prevent build error "window is undefined" but keep keep working + if (typeof window === 'undefined') { + return null + } + + try { + const item = window.sessionStorage.getItem(key) + + if (item) return parser(item) as T + return null + } catch (error) { + console.warn(`Error reading sessionStorage key “${key}”:`, error) + return null + } + }, [key, parser]) + + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState>(readValue) + + useEffect(() => { + setStoredValue(readValue()) + }, [readValue]) + + const handleStorageChange = useCallback( + (event: StorageEvent | CustomEvent) => { + if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) { + return + } + setStoredValue(readValue()) + }, + [key, readValue], + ) + + // this only works for other documents, not the current one + useEventListener('storage', handleStorageChange) + + // this is a custom event, triggered in writeValueTosessionStorage + // See: useSessionStorage() + useEventListener('session-storage', handleStorageChange) + + return storedValue +} + +function parseJSON(value: string | null): T { + return JSON.parse(value ?? '') +} + +// This is used when parseAsJSON === false +function castValue(value: string | null): T { + return value as T +}