-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
29a664e
commit 40e4e77
Showing
4 changed files
with
237 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { useLocalStorage } from "./useLocalStorage"; | ||
import { useMediaQuery } from "./useMediaQuery"; | ||
|
||
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"; | ||
const LOCAL_STORAGE_KEY = "usehooks-ts-dark-mode"; | ||
|
||
type DarkModeOptions = { | ||
defaultValue?: boolean; | ||
localStorageKey?: string; | ||
initializeWithValue?: boolean; | ||
}; | ||
|
||
type DarkModeReturn = { | ||
isDarkMode: boolean; | ||
toggle: () => void; | ||
enable: () => void; | ||
disable: () => void; | ||
set: (value: boolean) => void; | ||
}; | ||
|
||
export function useDarkMode(options: DarkModeOptions = { initializeWithValue: true }): DarkModeReturn { | ||
const { defaultValue, localStorageKey = LOCAL_STORAGE_KEY, initializeWithValue = true } = options; | ||
|
||
const isDarkOS = useMediaQuery(COLOR_SCHEME_QUERY, { | ||
initializeWithValue, | ||
defaultValue, | ||
}); | ||
const [isDarkMode, setDarkMode] = useLocalStorage<boolean>(localStorageKey, defaultValue ?? isDarkOS ?? false, { | ||
initializeWithValue, | ||
}); | ||
|
||
return { | ||
isDarkMode, | ||
toggle: () => { | ||
setDarkMode((prev) => !prev); | ||
}, | ||
enable: () => { | ||
setDarkMode(true); | ||
}, | ||
disable: () => { | ||
setDarkMode(false); | ||
}, | ||
set: (value) => { | ||
setDarkMode(value); | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,168 @@ | ||
import { Dispatch, SetStateAction, useEffect, useState } from "react"; | ||
import { useCallback, useEffect, useState } from "react"; | ||
|
||
function getStorageValue<S>(key: string, defaultValue: S): S { | ||
// getting stored value | ||
const saved = localStorage.getItem(key); | ||
const initial = saved && JSON.parse(saved); | ||
return initial || defaultValue; | ||
} | ||
function useLocalStorage<S>(key: string, defaultValue: S): [S, Dispatch<SetStateAction<S>>] { | ||
const [value, setValue] = useState<S>(() => { | ||
return getStorageValue(key, defaultValue); | ||
}); | ||
import type { Dispatch, SetStateAction } from "react"; | ||
|
||
import { useEventCallback } from "usehooks-ts"; | ||
import { useEventListener } from "usehooks-ts"; | ||
|
||
useEffect(() => { | ||
// storing input name | ||
localStorage.setItem(key, JSON.stringify(value)); | ||
}, [key, value]); | ||
declare global { | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions | ||
interface WindowEventMap { | ||
"local-storage": CustomEvent; | ||
} | ||
} | ||
|
||
return [value, setValue]; | ||
/** | ||
* Options for customizing the behavior of serialization and deserialization. | ||
* @template T - The type of the state to be stored in local storage. | ||
*/ | ||
type UseLocalStorageOptions<T> = { | ||
/** A function to serialize the value before storing it. */ | ||
serializer?: (value: T) => string; | ||
/** A function to deserialize the stored value. */ | ||
deserializer?: (value: string) => T; | ||
/** | ||
* If `true` (default), the hook will initialize reading the local storage. In SSR, you should set it to `false`, returning the initial value initially. | ||
* @default true | ||
*/ | ||
initializeWithValue?: boolean; | ||
}; | ||
|
||
export default useLocalStorage | ||
const IS_SERVER = typeof window === "undefined"; | ||
|
||
/** | ||
* Custom hook that uses local storage to persist state across page reloads. | ||
* @template T - The type of the state to be stored in local storage. | ||
* @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. | ||
* @public | ||
* @see [Documentation](https://usehooks-ts.com/react-hook/use-local-storage) | ||
* @see [MDN Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) | ||
* @example | ||
* ```tsx | ||
* const [count, setCount] = useLocalStorage('count', 0); | ||
* // Access the `count` value and the `setCount` function to update it. | ||
* ``` | ||
*/ | ||
export function useLocalStorage<T>( | ||
key: string, | ||
initialValue: T | (() => T), | ||
options: UseLocalStorageOptions<T> = {}, | ||
): [T, Dispatch<SetStateAction<T>>] { | ||
const { initializeWithValue = true } = options; | ||
|
||
const serializer = useCallback<(value: T) => string>( | ||
(value) => { | ||
if (options.serializer) { | ||
return options.serializer(value); | ||
} | ||
|
||
return JSON.stringify(value); | ||
}, | ||
[options], | ||
); | ||
|
||
const deserializer = useCallback<(value: string) => T>( | ||
(value) => { | ||
if (options.deserializer) { | ||
return options.deserializer(value); | ||
} | ||
// Support 'undefined' as a value | ||
if (value === "undefined") { | ||
return undefined as unknown as T; | ||
} | ||
|
||
const defaultValue = initialValue instanceof Function ? initialValue() : initialValue; | ||
|
||
let parsed: unknown; | ||
try { | ||
parsed = JSON.parse(value); | ||
} catch (error) { | ||
console.error("Error parsing JSON:", error); | ||
return defaultValue; // Return initialValue if parsing fails | ||
} | ||
|
||
return parsed as T; | ||
}, | ||
[options, initialValue], | ||
); | ||
|
||
// Get from local storage then | ||
// parse stored json or return initialValue | ||
const readValue = useCallback((): T => { | ||
const initialValueToUse = initialValue instanceof Function ? initialValue() : initialValue; | ||
|
||
// Prevent build error "window is undefined" but keeps working | ||
if (IS_SERVER) { | ||
return initialValueToUse; | ||
} | ||
|
||
try { | ||
const raw = window.localStorage.getItem(key); | ||
return raw ? deserializer(raw) : initialValueToUse; | ||
} catch (error) { | ||
console.warn(`Error reading localStorage key “${key}”:`, error); | ||
return initialValueToUse; | ||
} | ||
}, [initialValue, key, deserializer]); | ||
|
||
const [storedValue, setStoredValue] = useState(() => { | ||
if (initializeWithValue) { | ||
return readValue(); | ||
} | ||
return initialValue instanceof Function ? initialValue() : initialValue; | ||
}); | ||
|
||
// 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`); | ||
} | ||
|
||
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 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); | ||
} | ||
}); | ||
|
||
useEffect(() => { | ||
const value = readValue(); | ||
setValue(value); | ||
setStoredValue(value); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [key]); | ||
|
||
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 writeValueToLocalStorage | ||
// See: useLocalStorage() | ||
useEventListener("local-storage", handleStorageChange); | ||
|
||
return [storedValue, setValue]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters