Skip to content

Commit

Permalink
Add useReadSessionStorage hook
Browse files Browse the repository at this point in the history
  • Loading branch information
jonlinkens committed Jun 26, 2023
1 parent 5bd5770 commit 4cf78c4
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/usehooks-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <p>SessionId: {sessionId ?? 'not set'}</p>
}
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { renderHook } from '@testing-library/react-hooks/dom'

import { useReadSessionStorage } from './useReadSessionStorage'

class SessionStorageMock {
store: Record<string, unknown> = {}

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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useCallback, useEffect, useState } from 'react'

import { useEventListener } from '..'

type Options<T> = {
parseAsJson: boolean
parser: (value: string | null) => T
}

type Value<T> = T | null

export function useReadSessionStorage<T>(
key: string,
{
parseAsJson = true,
parser = parseAsJson ? parseJSON : castValue,
}: Partial<Options<T>> = {},
): Value<T> {
// Get from session storage then
// parse stored json or return initialValue

const readValue = useCallback((): Value<T> => {
// 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<Value<T>>(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<T>(value: string | null): T {
return JSON.parse(value ?? '')
}

// This is used when parseAsJSON === false
function castValue<T>(value: string | null): T {
return value as T
}

0 comments on commit 4cf78c4

Please sign in to comment.