diff --git a/README.md b/README.md index e8f2c52..d240ff5 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,4 @@ https://www.npmjs.com/package/@risc0/ui | Statements | Branches | Functions | Lines | | --------------------------- | ----------------------- | ------------------------- | ----------------- | -| ![Statements](https://img.shields.io/badge/statements-40.88%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-81.72%25-yellow.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-75.47%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-40.88%25-red.svg?style=flat) | +| ![Statements](https://img.shields.io/badge/statements-41.51%25-red.svg?style=flat) | ![Branches](https://img.shields.io/badge/branches-81.72%25-yellow.svg?style=flat) | ![Functions](https://img.shields.io/badge/functions-75.47%25-red.svg?style=flat) | ![Lines](https://img.shields.io/badge/lines-41.51%25-red.svg?style=flat) | diff --git a/hooks/use-local-storage.ts b/hooks/use-local-storage.ts index 69dd425..d06b675 100644 --- a/hooks/use-local-storage.ts +++ b/hooks/use-local-storage.ts @@ -1,15 +1,12 @@ -"use client"; - -import { isEqual, isFunction } from "radash"; -import { type Dispatch, type SetStateAction, useEffect, useRef, useState } from "react"; +import { isFunction } from "radash"; +import { type SetStateAction, useRef, useSyncExternalStore } from "react"; import { parseJson } from "../utils/parse-json"; -import { useEventListener } from "./use-event-listener"; function isNil(val: unknown) { return val == null; } -export type SetValue = Dispatch>; +export type SetValue = (value: SetStateAction) => void; /** * Creating and read here and using window.setItem for writes. This avoids @@ -42,70 +39,43 @@ function isError(value: StorageError | any): value is StorageError { export function useLocalStorage(key: string, initialValue: T): [T, SetValue] { const previousValueRef = useRef(initialValue); - // Get from local storage then - // parse stored json or return initialValue function readValue(): T { - // Prevent build error "window is undefined" but keep keep working if (typeof window === "undefined") { return initialValue; } const value = readValueFromStorage(key); - return isError(value) || isNil(value) || value === "" ? initialValue : (value as T); } - // State to store our value - // Pass initial state function to useState so logic is only executed once - const [storedValue, setStoredValue] = useState(readValue); + function subscribe(callback: () => void) { + window.addEventListener("storage", callback); + window.addEventListener("local-storage", callback); + return () => { + window.removeEventListener("storage", callback); + window.removeEventListener("local-storage", callback); + }; + } + + const storedValue = useSyncExternalStore(subscribe, readValue, () => initialValue); - // Return a wrapped version of useState's setter function that persists the new value to localStorage. function setValue(value: SetStateAction): void { - // Prevent build error "window is undefined" but keeps working if (typeof window === "undefined") { console.warn(`Tried setting localStorage key "${key}" even though environment is not a client`); + return; } try { - // Allow value to be a function so we have the same API as useState const newValue = isFunction(value) ? (value as (prevState: T) => T)(storedValue) : value; - // Save to local storage window.localStorage.setItem(key, JSON.stringify(newValue)); + previousValueRef.current = newValue; - // Save state - setStoredValue(newValue); - - // We dispatch a custom event so every useLocalStorage hook are notified window.dispatchEvent(new Event("local-storage")); } catch (error) { console.warn(`Error setting localStorage key "${key}":`, error); } } - // biome-ignore lint/correctness/useExhaustiveDependencies: run only once - useEffect(() => { - const newValue = readValue(); - setStoredValue(newValue); - previousValueRef.current = newValue; - }, []); - - function handleStorageChange() { - const newValue = readValue(); - - if (isEqual(newValue, previousValueRef.current)) { - return; - } - setStoredValue(newValue); - previousValueRef.current = newValue; - } - - // 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]; } diff --git a/package.json b/package.json index 58b931a..6cfc47e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@risc0/ui", - "version": "0.0.180", + "version": "0.0.181", "private": false, "sideEffects": false, "type": "module", @@ -15,27 +15,27 @@ "story": "ladle serve" }, "dependencies": { - "@radix-ui/react-avatar": "1.1.0", - "@radix-ui/react-checkbox": "1.1.1", + "@radix-ui/react-avatar": "1.1.1", + "@radix-ui/react-checkbox": "1.1.2", "@radix-ui/react-dialog": "1.0.5", - "@radix-ui/react-dropdown-menu": "2.1.1", + "@radix-ui/react-dropdown-menu": "2.1.2", "@radix-ui/react-label": "2.1.0", - "@radix-ui/react-navigation-menu": "1.2.0", - "@radix-ui/react-popover": "1.1.1", + "@radix-ui/react-navigation-menu": "1.2.1", + "@radix-ui/react-popover": "1.1.2", "@radix-ui/react-progress": "1.1.0", - "@radix-ui/react-radio-group": "1.2.0", - "@radix-ui/react-select": "2.1.1", + "@radix-ui/react-radio-group": "1.2.1", + "@radix-ui/react-select": "2.1.2", "@radix-ui/react-separator": "1.1.0", - "@radix-ui/react-slider": "1.2.0", + "@radix-ui/react-slider": "1.2.1", "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-switch": "1.1.0", - "@radix-ui/react-tabs": "1.1.0", - "@radix-ui/react-tooltip": "1.1.2", + "@radix-ui/react-switch": "1.1.1", + "@radix-ui/react-tabs": "1.1.1", + "@radix-ui/react-tooltip": "1.1.3", "autoprefixer": "10.4.20", "class-variance-authority": "0.7.1-canary.2", "clsx": "2.1.1", "cmdk": "1.0.0", - "lucide-react": "0.446.0", + "lucide-react": "0.447.0", "next-themes": "0.3.0", "radash": "12.1.0", "react-hook-form": "7.52.2",