Skip to content

Commit

Permalink
✨ Add remove function to useLocalStorage and useSessionStorage (#573
Browse files Browse the repository at this point in the history
 by @k-melnychuk & @RubyHuntsman)

* Add remove function to useLocalStorage and useSessionStorage

* Update demos
  • Loading branch information
k-melnychuk committed Apr 4, 2024
1 parent b240a4d commit 06dfd5e
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-mayflies-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"usehooks-ts": minor
---

Add remove function to useLocalStorage and useSessionStorage
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLocalStorage } from './useLocalStorage'

export default function Component() {
const [value, setValue] = useLocalStorage('test-key', 0)
const [value, setValue, removeValue] = useLocalStorage('test-key', 0)

return (
<div>
Expand All @@ -20,6 +20,13 @@ export default function Component() {
>
Decrement
</button>
<button
onClick={() => {
removeValue()
}}
>
Reset
</button>
</div>
)
}
22 changes: 22 additions & 0 deletions packages/usehooks-ts/src/useLocalStorage/useLocalStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ describe('useLocalStorage()', () => {
expect(window.localStorage.getItem('key')).toBe(JSON.stringify('edited'))
})

it('Remove the state removes localStorage key', () => {
const { result } = renderHook(() => useLocalStorage('key', 'value'))

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

expect(result.current[0]).toBe('updated')
expect(window.localStorage.getItem('key')).toBe(JSON.stringify('updated'))

act(() => {
const removeValue = result.current[2]
removeValue()
})

// Expect null as it's a default return if storage key doesn't exist
expect(window.localStorage.getItem('key')).toBeNull()
// Expect the state to match the default value
expect(result.current[0]).toBe('value')
})

it('Update the state with undefined', () => {
const { result } = renderHook(() =>
useLocalStorage<string | undefined>('key', 'value'),
Expand Down
34 changes: 28 additions & 6 deletions packages/usehooks-ts/src/useLocalStorage/useLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,20 @@ const IS_SERVER = typeof window === 'undefined'
* @param {string} key - The key under which the value will be stored in local storage.
* @param {T | (() => T)} initialValue - The initial value of the state or a function that returns the initial value.
* @param {UseLocalStorageOptions<T>} [options] - Options for customizing the behavior of serialization and deserialization (optional).
* @returns {[T, Dispatch<SetStateAction<T>>]} A tuple containing the stored value and a function to set the value.
* @returns {[T, Dispatch<SetStateAction<T>>, () => void]} A tuple containing the stored value, a function to set the value and a function to remove the key from storage.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-local-storage)
* @example
* ```tsx
* const [count, setCount] = useLocalStorage('count', 0);
* // Access the `count` value and the `setCount` function to update it.
* const [count, setCount, removeCount] = useLocalStorage('count', 0);
* // Access the `count` value, the `setCount` function to update it and `removeCount` function to remove the key from storage.
* ```
*/
export function useLocalStorage<T>(
key: string,
initialValue: T | (() => T),
options: UseLocalStorageOptions<T> = {},
): [T, Dispatch<SetStateAction<T>>] {
): [T, Dispatch<SetStateAction<T>>, () => void] {
const { initializeWithValue = true } = options

const serializer = useCallback<(value: T) => string>(
Expand Down Expand Up @@ -95,7 +95,7 @@ export function useLocalStorage<T>(
const initialValueToUse =
initialValue instanceof Function ? initialValue() : initialValue

// Prevent build error "window is undefined" but keeps working
// Prevent build error "window is undefined" but keep working
if (IS_SERVER) {
return initialValueToUse
}
Expand All @@ -113,6 +113,7 @@ export function useLocalStorage<T>(
if (initializeWithValue) {
return readValue()
}

return initialValue instanceof Function ? initialValue() : initialValue
})

Expand Down Expand Up @@ -143,6 +144,27 @@ export function useLocalStorage<T>(
}
})

const removeValue = useEventCallback(() => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(
`Tried removing localStorage key “${key}” even though environment is not a client`,
)
}

const defaultValue =
initialValue instanceof Function ? initialValue() : initialValue

// Remove the key from local storage
window.localStorage.removeItem(key)

// Save state with default value
setStoredValue(defaultValue)

// We dispatch a custom event so every similar useLocalStorage hook is notified
window.dispatchEvent(new StorageEvent('local-storage', { key }))
})

useEffect(() => {
setStoredValue(readValue())
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -165,5 +187,5 @@ export function useLocalStorage<T>(
// See: useLocalStorage()
useEventListener('local-storage', handleStorageChange)

return [storedValue, setValue]
return [storedValue, setValue, removeValue]
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useSessionStorage } from './useSessionStorage'

export default function Component() {
const [value, setValue] = useSessionStorage('test-key', 0)
const [value, setValue, removeValue] = useSessionStorage('test-key', 0)

return (
<div>
Expand All @@ -20,6 +20,13 @@ export default function Component() {
>
Decrement
</button>
<button
onClick={() => {
removeValue()
}}
>
Reset
</button>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ describe('useSessionStorage()', () => {
expect(window.sessionStorage.getItem('key')).toBe(JSON.stringify('edited'))
})

it('Remove the state removes sessionStorage key', () => {
const { result } = renderHook(() => useSessionStorage('key', 'value'))

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

expect(result.current[0]).toBe('updated')
expect(window.sessionStorage.getItem('key')).toBe(JSON.stringify('updated'))

act(() => {
const removeValue = result.current[2]
removeValue()
})

// Expect null as it's a default return if storage key doesn't exist
expect(window.sessionStorage.getItem('key')).toBeNull()
// Expect the state to match the default value
expect(result.current[0]).toBe('value')
})

it('Update the state with undefined', () => {
const { result } = renderHook(() =>
useSessionStorage<string | undefined>('keytest', 'value'),
Expand Down
33 changes: 27 additions & 6 deletions packages/usehooks-ts/src/useSessionStorage/useSessionStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,20 @@ const IS_SERVER = typeof window === 'undefined'
* @param {string} key - The key under which the value will be stored in session storage.
* @param {T | (() => T)} initialValue - The initial value of the state or a function that returns the initial value.
* @param {?UseSessionStorageOptions<T>} [options] - Options for customizing the behavior of serialization and deserialization (optional).
* @returns {[T, Dispatch<SetStateAction<T>>]} A tuple containing the stored value and a function to set the value.
* @returns {[T, Dispatch<SetStateAction<T>>, () => void]} A tuple containing the stored value, a function to set the value and a function to remove the key from storage.
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-session-storage)
* @example
* ```tsx
* const [count, setCount] = useSessionStorage('count', 0);
* // Access the `count` value and the `setCount` function to update it.
* const [count, setCount, removeCount] = useSessionStorage('count', 0);
* // Access the `count` value, the `setCount` function to update it and `removeCount` function to remove the key from storage.
* ```
*/
export function useSessionStorage<T>(
key: string,
initialValue: T | (() => T),
options: UseSessionStorageOptions<T> = {},
): [T, Dispatch<SetStateAction<T>>] {
): [T, Dispatch<SetStateAction<T>>, () => void] {
const { initializeWithValue = true } = options

const serializer = useCallback<(value: T) => string>(
Expand Down Expand Up @@ -95,7 +95,7 @@ export function useSessionStorage<T>(
const initialValueToUse =
initialValue instanceof Function ? initialValue() : initialValue

// Prevent build error "window is undefined" but keep keep working
// Prevent build error "window is undefined" but keep working
if (IS_SERVER) {
return initialValueToUse
}
Expand Down Expand Up @@ -144,6 +144,27 @@ export function useSessionStorage<T>(
}
})

const removeValue = useEventCallback(() => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(
`Tried removing sessionStorage key “${key}” even though environment is not a client`,
)
}

const defaultValue =
initialValue instanceof Function ? initialValue() : initialValue

// Remove the key from session storage
window.sessionStorage.removeItem(key)

// Save state with default value
setStoredValue(defaultValue)

// We dispatch a custom event so every similar useSessionStorage hook is notified
window.dispatchEvent(new StorageEvent('session-storage', { key }))
})

useEffect(() => {
setStoredValue(readValue())
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand All @@ -166,5 +187,5 @@ export function useSessionStorage<T>(
// See: useSessionStorage()
useEventListener('session-storage', handleStorageChange)

return [storedValue, setValue]
return [storedValue, setValue, removeValue]
}

0 comments on commit 06dfd5e

Please sign in to comment.