diff --git a/packages/trpc-panel/src/react-app/Root.tsx b/packages/trpc-panel/src/react-app/Root.tsx index a2823f2..946e8fe 100644 --- a/packages/trpc-panel/src/react-app/Root.tsx +++ b/packages/trpc-panel/src/react-app/Root.tsx @@ -8,6 +8,7 @@ import { HeadersContextProvider, useHeaders, } from "@src/react-app/components/contexts/HeadersContext"; +import { useLocalStorage } from "@src/react-app/components/hooks/useLocalStorage"; import { HeadersPopup } from "@src/react-app/components/HeadersPopup"; import { Toaster } from "react-hot-toast"; import { SiteNavigationContextProvider } from "@src/react-app/components/contexts/SiteNavigationContext"; @@ -80,7 +81,10 @@ function ClientProviders({ } function AppInnards({ rootRouter }: { rootRouter: ParsedRouter }) { - const [sidebarOpen, setSidebarOpen] = useState(true); + const [sidebarOpen, setSidebarOpen] = useLocalStorage( + "trpc-panel.show-minimap", + true + ); return (
diff --git a/packages/trpc-panel/src/react-app/components/hooks/useLocalStorage.tsx b/packages/trpc-panel/src/react-app/components/hooks/useLocalStorage.tsx new file mode 100644 index 0000000..d84b4c9 --- /dev/null +++ b/packages/trpc-panel/src/react-app/components/hooks/useLocalStorage.tsx @@ -0,0 +1,88 @@ +// a stripped down version of https://usehooks-ts.com/react-hook/use-local-storage +import { + useCallback, + useEffect, + useState, + type Dispatch, + type SetStateAction, +} from "react"; + +type SetValue = Dispatch>; + +export function useLocalStorage( + key: string, + initialValue: T +): [T, SetValue] { + // Get from local storage then + // parse stored json or return initialValue + const readValue = useCallback((): T => { + // Prevent build error "window is undefined" but keeps working + if (typeof window === "undefined") { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? (parseJSON(item) as T) : initialValue; + } catch (error) { + console.warn( + `tRPC-Panel.useLocalStorage: Error reading localStorage key “${key}”:`, + error + ); + return initialValue; + } + }, [initialValue, key]); + + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(readValue); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue: SetValue = useCallback( + (value) => { + // Prevent build error "window is undefined" but keeps working + if (typeof window === "undefined") { + console.warn( + `tRPC-Panel.useLocalStorage: 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(storedValue) : value; + + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(newValue)); + + // Save state + setStoredValue(newValue); + } catch (error) { + console.warn( + `tRPC-Panel.useLocalStorage: Error setting localStorage key “${key}”:`, + error + ); + } + }, + [storedValue] + ); + + useEffect(() => { + setStoredValue(readValue()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [storedValue, setValue]; +} + +// A wrapper for "JSON.parse()"" to support "undefined" value +function parseJSON(value: string | null): T | undefined { + try { + return value === "undefined" ? undefined : JSON.parse(value ?? ""); + } catch { + console.log("tRPC-Panel.useLocalStorage.parseJSON: parsing error on", { + value, + }); + return undefined; + } +}