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

Update useSessionStorage to support strings + add new options + add useReadSessionStorage #340

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
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
}
11 changes: 10 additions & 1 deletion packages/usehooks-ts/src/useSessionStorage/useSessionStorage.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
Persist the state with session storage so that it remains after a page refresh. This can be useful to record session information. This hook is used in the same way as useState except that you must pass the storage key in the 1st parameter. If the window object is not present (as in SSR), `useSessionStorage()` will return the default value.
Persist the state with session storage so that it remains after a page refresh. This can be useful to record session information. This hook is used in the same way as useState except that you must pass the storage key in the 1st parameter. If the window object is not present (as in SSR) or the storage key does not exist, `useSessionStorage()` will return the default value provided.

**Options:**

There is some additional config you can pass to this hook with a third, `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
- `serializer: (value: T) => string` - a custom serializer to store values in a format different from the default behavior of `JSON.stringify`

Related hooks:

- [`useReadSessionStorage()`](/react-hook/use-read-session-storage)
- [`useLocalStorage()`](/react-hook/use-local-storage)
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,61 @@ describe('useSessionStorage()', () => {
jest.clearAllMocks()
})

test('initial state is in the returned state', () => {
const { result } = renderHook(() => useSessionStorage('key', 'value'))
test('Existing string state is in the returned state when parseAsJson is false', () => {
window.sessionStorage.setItem('key', 'value')
const { result } = renderHook(() =>
useSessionStorage('key', 'value', { parseAsJson: false }),
)

expect(result.current[0]).toBe('value')
})

test('Custom parser returns expected value', () => {
window.sessionStorage.setItem('key', 'value')

const { result } = renderHook(() =>
useSessionStorage('key', 'value', { parser: doubleLetters }),
)

expect(result.current[0]).toBe('vvaalluuee')
})

test('Custom serializer stores expected value', () => {
const { result } = renderHook(() =>
useSessionStorage('key', 'value', {
parseAsJson: false,
serializer: doubleLetters,
}),
)

act(() => {
const setState = result.current[1]
setState('value')
})

expect(result.current[0]).toBe('vvaalluuee')
})

test('Custom parser returns expected value after being serialized', () => {
const { result } = renderHook(() =>
useSessionStorage('key', 'value', {
parseAsJson: false,
serializer: doubleLetters,
parser: doubleLetters,
}),
)

expect(result.current[0]).toBe('value')
})

test('Existing object state is in the returned state', () => {
const obj = { value: 'foo' }
window.sessionStorage.setItem('key', JSON.stringify(obj))
const { result } = renderHook(() => useSessionStorage('key', obj))

expect(result.current[0]).toStrictEqual(obj)
})

test('Initial state is a callback function', () => {
const { result } = renderHook(() => useSessionStorage('key', () => 'value'))

Expand Down Expand Up @@ -75,19 +124,6 @@ describe('useSessionStorage()', () => {
expect(window.sessionStorage.getItem('key')).toBe(JSON.stringify('edited'))
})

test('Update the state with undefined', () => {
const { result } = renderHook(() =>
useSessionStorage<string | undefined>('keytest', 'value'),
)

act(() => {
const setState = result.current[1]
setState(undefined)
})

expect(result.current[0]).toBeUndefined()
})

test('Update the state with a callback function', () => {
const { result } = renderHook(() => useSessionStorage('count', 2))

Expand Down Expand Up @@ -129,3 +165,14 @@ describe('useSessionStorage()', () => {
expect(result.current[1] === originalCallback).toBe(true)
})
})

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
}
Loading