Skip to content

feat(core): same-tab component sync for storage & cookie hooks (+ useTimeoutFn mount fix)#204

Merged
childrentime merged 4 commits into
mainfrom
feat/same-tab-storage-sync
Jun 25, 2026
Merged

feat(core): same-tab component sync for storage & cookie hooks (+ useTimeoutFn mount fix)#204
childrentime merged 4 commits into
mainfrom
feat/same-tab-storage-sync

Conversation

@childrentime

@childrentime childrentime commented Jun 25, 2026

Copy link
Copy Markdown
Owner

This PR bundles two fixes, each closing an open issue.


1. Same-tab component sync — closes #202

The native storage event 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 splits useColorMode's mode and toggle across components, the color mode doesn't update live.

Each write now re-broadcasts a custom window event (reactuse:storage / reactuse:cookie) that sibling instances listen for via useEventListener; cross-tab sync still rides the native storage event.

  • createStorage (useLocalStorage + useSessionStorage): a shared core applies the change (key + storageArea-identity filtering; removal resets the cache). Two separate listeners feed it — native storage (cross-tab) and the custom event (same-tab).
  • 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) — this is what fixes [Bug] useColorMode 的模式与切换必须在同一组件下 #202.

Design notes

  • window event, not a module-level registrywindow is a single per-realm global, so it survives the library being bundled more than once (dual CJS+ESM, micro-frontends); a module Map would split and silently fail. (Grounded in a survey of VueUse / usehooks-ts / Mantine / @uidotdev / jotai+zustand.)
  • Custom event name, not a faked native 'storage' — avoids notifying unrelated storage listeners in the host app.
  • listenToStorageChanges gates cross-tab only. Same-tab sync is core behavior and is always on; the option (default true) toggles just the native storage listener.

2. useTimeoutFn mount-render fix — closes #203

useTimeout(...)[0] flashed false → true → false on mount. Seeding useState(() => immediate) makes pending read its real (armed) value on the first render, removing the false→true flash. SSR-safe — immediate is identical on server and client.


Docs

Corrected the false "no cross-component sync" claims in useCookie and the useLocalStorage / useSessionStorage sync notes, and added clickable two-component live demos that show same-tab propagation — en + zh-Hans + zh-Hant.

Test plan

  • pnpm lint — clean
  • pnpm --filter @reactuses/core test305/305 pass, incl. new same-tab sync, removal propagation, localStorage vs sessionStorage non-cross-notify, and "still syncs same-tab when listenToStorageChanges: false" tests.

🤖 Generated with Claude Code

childrentime and others added 4 commits June 25, 2026 17:22
…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>
@childrentime childrentime changed the title feat(core): same-tab sync for storage & cookie hooks feat(core): same-tab component sync for storage & cookie hooks (+ useTimeoutFn mount fix) Jun 25, 2026

@childrentime childrentime left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@childrentime childrentime merged commit 07fbe20 into main Jun 25, 2026
4 checks passed
@childrentime childrentime deleted the feat/same-tab-storage-sync branch June 25, 2026 11:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useTimeout 的返回值在组件未加载时是 false [Bug] useColorMode 的模式与切换必须在同一组件下

1 participant