Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/spotty-jeans-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'classic-react-hooks': minor
---

- Fix missing export for `useMultipleIntersectionObserver` hook
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export { default as useOnMountEffect } from './lib/use-on-mount-effect'
export { default as useCounter } from './lib/use-counter'

export { default as useInterSectionObserver } from './lib/use-intersection-observer'
export { default as useMultipleIntersectionObserver } from './lib/use-multi-intersection-observer'
108 changes: 108 additions & 0 deletions use-local.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { useCallback, useRef, useSyncExternalStore } from 'react'

export default function useLocal<State>({ key, initialValue }: { key: string, initialValue?: State | (() => State) }) {
// Cache the last parse value to avoid unnecessary re-renders
const lastValueRef = useRef<State | null>(null)
const lastStringRef = useRef<string | null>(null)
const lastKeyRef = useRef<string>(key)

const getStoredState = useCallback(() => {
try {
// Key has changed, reset cached values
if (lastKeyRef.current !== key) {
localStorage.removeItem(lastKeyRef.current)
lastKeyRef.current = key
lastValueRef.current = null
lastStringRef.current = null
}

const item = localStorage.getItem(key)

if (item != null) {
// Only parse if the string value has changed
if (lastStringRef.current !== item) {
lastStringRef.current = item
lastValueRef.current = JSON.parse(item)
}
return lastValueRef.current!
}
} catch (err) {
console.log(err)
}

// Return initial value
let returnVal: State
if (typeof initialValue == "function") {
returnVal = (initialValue as () => State)()
} else {
returnVal = initialValue as State
}

// Cache the initial value
lastValueRef.current = returnVal
return returnVal
}, [key, initialValue])

// Server-side snapshot - return initial value to avoid hydration mismatch
const getServerSnapshot = useCallback(() => {
if (typeof initialValue == 'function') {
return (initialValue as () => State)()
}
return initialValue as State
}, [initialValue])

const storedState = useSyncExternalStore(
useCallback((callback) => {
const handleStorageChange = (event: Event) => {
callback()
}

// Listen for our custom events
window.addEventListener(key, handleStorageChange)

// Also listen for storage event from other tabs
const handleStorageEvent = (event: StorageEvent) => {
if (event.key === key) {
callback()
}
}

window.addEventListener('storage', handleStorageEvent)

return () => {
window.removeEventListener(key, handleStorageChange)
window.removeEventListener('storage', handleStorageEvent)
}
}, []),
getStoredState,
getServerSnapshot
)

const updateState = useCallback(() => {
(value: State | (() => State)) => {
try {
let newValue: State

if (typeof value === 'function') {
newValue = (value as (state: State) => State)(storedState)
} else {
newValue = value
}

const serializedValue = JSON.stringify(newValue)
localStorage.setItem(key, serializedValue)

// Update cache
lastStringRef.current = serializedValue
lastValueRef.current = newValue

// Notify subscriber
window.dispatchEvent(new Event(key))
} catch (err) {
console.log(err)
}
}
}, [key, storedState])

return [storedState, updateState] as const
}