diff --git a/src/apps/feed/index.tsx b/src/apps/feed/index.tsx index d3d61a9..13d5f39 100644 --- a/src/apps/feed/index.tsx +++ b/src/apps/feed/index.tsx @@ -91,7 +91,7 @@ export default function FeedApp({ setTitle }: AppProps) { variant="ghost" size="sm" className="h-7 gap-1.5 px-2 text-xs" - onClick={() => openApp('notes', {})} + onClick={() => openApp('notes')} > New note diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 73c2959..01966dc 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -1,4 +1,19 @@ -import { useState, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; + +/** + * Fired on `window` whenever `useLocalStorage` writes a key, so every + * component sharing that key *within this document* stays in sync — the + * native `storage` event only fires in *other* tabs/documents, never the + * one that made the write. Without this, e.g. two "New Note" draft windows + * open at once would silently diverge: each holds its own React state, both + * write to the same localStorage entry, and neither sees the other's edits. + */ +const LOCAL_STORAGE_EVENT = 'app:local-storage'; + +interface LocalStorageEventDetail { + key: string; + value: string; +} /** * Generic hook for managing localStorage state @@ -24,17 +39,30 @@ export function useLocalStorage( } }); - const setValue = (value: T | ((prev: T) => T)) => { - try { - const valueToStore = value instanceof Function ? value(state) : value; - setState(valueToStore); - localStorage.setItem(key, serialize(valueToStore)); - } catch (error) { - console.warn(`Failed to save ${key} to localStorage:`, error); - } - }; + const setValue = useCallback( + (value: T | ((prev: T) => T)) => { + setState((prev) => { + try { + const valueToStore = value instanceof Function ? value(prev) : value; + const serialized = serialize(valueToStore); + localStorage.setItem(key, serialized); + window.dispatchEvent( + new CustomEvent(LOCAL_STORAGE_EVENT, { + detail: { key, value: serialized }, + }), + ); + return valueToStore; + } catch (error) { + console.warn(`Failed to save ${key} to localStorage:`, error); + return prev; + } + }); + }, + [key, serialize], + ); - // Sync with localStorage changes from other tabs + // Sync with localStorage changes from other tabs, and from other + // components sharing this key in this tab (see LOCAL_STORAGE_EVENT above). useEffect(() => { const handleStorageChange = (e: StorageEvent) => { if (e.key === key && e.newValue !== null) { @@ -45,10 +73,23 @@ export function useLocalStorage( } } }; + const handleLocalChange = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (detail?.key !== key) return; + try { + setState(deserialize(detail.value)); + } catch (error) { + console.warn(`Failed to sync ${key} from localStorage:`, error); + } + }; window.addEventListener('storage', handleStorageChange); - return () => window.removeEventListener('storage', handleStorageChange); + window.addEventListener(LOCAL_STORAGE_EVENT, handleLocalChange); + return () => { + window.removeEventListener('storage', handleStorageChange); + window.removeEventListener(LOCAL_STORAGE_EVENT, handleLocalChange); + }; }, [key, deserialize]); return [state, setValue] as const; -} \ No newline at end of file +}