feat(core): same-tab component sync for storage & cookie hooks (+ useTimeoutFn mount fix)#204
Merged
Merged
Conversation
…sh render When immediate is on, the timer is armed in the mount effect, so pending should already read true on the first render instead of flashing false -> true -> false. Seeding useState(() => immediate) removes the extra mount render; immediate is identical on server and client, so this stays SSR-safe. Spec render-count expectations updated to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tab The native `storage` event only fires in OTHER tabs, so two components bound to the same key in one tab never updated each other. Each write now re-broadcasts a custom `reactuse:storage` / `reactuse:cookie` window event that sibling instances listen for via useEventListener; cross-tab still uses the native `storage` event. - createStorage: a shared core applies the change; the native `storage` listener is gated by `listenToStorageChanges` (cross-tab only), while the same-tab listener is always on. Scoped by storageArea identity; removal resets the cache. - useCookie: same primitive (cookies fire no native event). `refreshCookie` stays for changes made outside the hook. - useColorMode / useDarkMode inherit it for free (built on createStorage). A window event (not a module-level registry) is used so it survives the library being bundled more than once, since `window` is a single per-realm global. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correct the useCookie docs (they stated there was no cross-component sync) and the useLocalStorage / useSessionStorage sync notes, and add clickable two-component `live` demos that show same-tab propagation. en + zh-Hans + zh-Hant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
updateState / updateCookie run only from client handlers, and the mount-write effect is client-only — every dispatch site is already behind `if (storage)` or a real interaction, and Cookies.set is called unguarded too. The isBrowser check on window.dispatchEvent was dead weight. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR bundles two fixes, each closing an open issue.
1. Same-tab component sync — closes #202
The native
storageevent only fires in other tabs, never in the document that made the change. So two components bound to the same key in the same tab never updated each other. That's the root of #202: when a custom hook splitsuseColorMode's mode and toggle across components, the color mode doesn't update live.Each write now re-broadcasts a custom
windowevent (reactuse:storage/reactuse:cookie) that sibling instances listen for viauseEventListener; cross-tab sync still rides the nativestorageevent.createStorage(useLocalStorage+useSessionStorage): a shared core applies the change (key +storageArea-identity filtering; removal resets the cache). Two separate listeners feed it — nativestorage(cross-tab) and the custom event (same-tab).useCookie: same primitive — cookies fire no native event.refreshCookiestays for changes made outside the hook.useColorMode/useDarkMode: inherit it for free (built oncreateStorage) — this is what fixes [Bug] useColorMode 的模式与切换必须在同一组件下 #202.Design notes
windowevent, not a module-level registry —windowis a single per-realm global, so it survives the library being bundled more than once (dual CJS+ESM, micro-frontends); a moduleMapwould split and silently fail. (Grounded in a survey of VueUse / usehooks-ts / Mantine / @uidotdev / jotai+zustand.)'storage'— avoids notifying unrelatedstoragelisteners in the host app.listenToStorageChangesgates cross-tab only. Same-tab sync is core behavior and is always on; the option (defaulttrue) toggles just the nativestoragelistener.2.
useTimeoutFnmount-render fix — closes #203useTimeout(...)[0]flashedfalse → true → falseon mount. SeedinguseState(() => immediate)makespendingread its real (armed) value on the first render, removing the false→true flash. SSR-safe —immediateis identical on server and client.Docs
Corrected the false "no cross-component sync" claims in
useCookieand theuseLocalStorage/useSessionStoragesync notes, and added clickable two-componentlivedemos that show same-tab propagation — en + zh-Hans + zh-Hant.Test plan
pnpm lint— cleanpnpm --filter @reactuses/core test— 305/305 pass, incl. new same-tab sync, removal propagation,localStoragevssessionStoragenon-cross-notify, and "still syncs same-tab whenlistenToStorageChanges: false" tests.🤖 Generated with Claude Code