Skip to content

Commit

Permalink
Allow setter to be called in render phase
Browse files Browse the repository at this point in the history
  • Loading branch information
jraoult committed Apr 7, 2024
1 parent 2066727 commit 56d9d12
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 30 deletions.
16 changes: 16 additions & 0 deletions packages/usehooks-ts/src/useLocalStorage/useLocalStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,22 @@ describe('useLocalStorage()', () => {
expect(result.current[1] === originalCallback).toBe(true)
})

it('setValue can be called in render phase', () => {
const { result } = renderHook(() => {
const [value, setValue] = useLocalStorage('count', 1)

// Adjust state with the recommended approach
// cf. https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
if (value !== 2) {
setValue(2)
}

return value
})

expect(result.current).toBe(2)
})

it('should use default JSON.stringify and JSON.parse when serializer/deserializer not provided', () => {
const { result } = renderHook(() => useLocalStorage('key', 'initialValue'))

Expand Down
68 changes: 38 additions & 30 deletions packages/usehooks-ts/src/useLocalStorage/useLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from 'react'

import type { Dispatch, SetStateAction } from 'react'

import { useEventCallback } from '../useEventCallback'
import { useEventListener } from '../useEventListener'

declare global {
Expand Down Expand Up @@ -50,23 +49,29 @@ export function useLocalStorage<T>(
initialValue: T | (() => T),
options: UseLocalStorageOptions<T> = {},
): [T, Dispatch<SetStateAction<T>>, () => void] {
const { initializeWithValue = true } = options
const {
initializeWithValue = true,
// Destructure these options to help eslint react-hooks plugin analyse the
// `useCallback` dependencies
serializer: oSerializer,
deserializer: oDeserializer,
} = options

const serializer = useCallback<(value: T) => string>(
value => {
if (options.serializer) {
return options.serializer(value)
if (oSerializer) {
return oSerializer(value)
}

return JSON.stringify(value)
},
[options],
[oSerializer],
)

const deserializer = useCallback<(value: string) => T>(
value => {
if (options.deserializer) {
return options.deserializer(value)
if (oDeserializer) {
return oDeserializer(value)
}
// Support 'undefined' as a value
if (value === 'undefined') {
Expand All @@ -86,7 +91,7 @@ export function useLocalStorage<T>(

return parsed as T
},
[options, initialValue],
[oDeserializer, initialValue],
)

// Get from local storage then
Expand Down Expand Up @@ -119,32 +124,35 @@ export function useLocalStorage<T>(

// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue: Dispatch<SetStateAction<T>> = useEventCallback(value => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`,
)
}
const setValue: Dispatch<SetStateAction<T>> = useCallback(
value => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`,
)
}

try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(readValue()) : value
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(readValue()) : value

// Save to local storage
window.localStorage.setItem(key, serializer(newValue))
// Save to local storage
window.localStorage.setItem(key, serializer(newValue))

// Save state
setStoredValue(newValue)
// Save state
setStoredValue(newValue)

// We dispatch a custom event so every similar useLocalStorage hook is notified
window.dispatchEvent(new StorageEvent('local-storage', { key }))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
})
// We dispatch a custom event so every similar useLocalStorage hook is notified
window.dispatchEvent(new StorageEvent('local-storage', { key }))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
},
[key, readValue, serializer],
)

const removeValue = useEventCallback(() => {
const removeValue = useCallback(() => {
// Prevent build error "window is undefined" but keeps working
if (IS_SERVER) {
console.warn(
Expand All @@ -163,7 +171,7 @@ export function useLocalStorage<T>(

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

useEffect(() => {
setStoredValue(readValue())
Expand Down

0 comments on commit 56d9d12

Please sign in to comment.