refactor(studio): use useSyncExternalStore in useLocalStorage#652
refactor(studio): use useSyncExternalStore in useLocalStorage#652aray12 wants to merge 1 commit into
Conversation
Rewrite useLocalStorage to subscribe via React 18's useSyncExternalStore instead of useState with a lazy initializer. Components are now reactive to storage changes from other tabs, and the getServerSnapshot parameter replaces the manual typeof window guard for SSR. The public API ([value, setValue, deleteValue]) is unchanged, so no callsites need edits. setValue/deleteValue dispatch a custom event so same-tab and multi-hook subscribers stay in sync (the browser storage event only fires in other tabs). Snapshots are cached against the raw string and the default is frozen to first render so useSyncExternalStore's Object.is comparison does not loop on inline defaults. Add unit tests covering cross-tab events, same-tab sync, and stable snapshot references. Signed-off-by: Alex Ray <alray@nvidia.com>
📝 WalkthroughWalkthroughChangesLocal storage synchronization
Sequence Diagram(s)sequenceDiagram
participant Caller
participant useLocalStorage
participant localStorage
participant useSyncExternalStore
Caller->>useLocalStorage: call setValue
useLocalStorage->>localStorage: persist value
useLocalStorage->>useSyncExternalStore: notify key subscribers
useSyncExternalStore->>useLocalStorage: read updated snapshot
useLocalStorage-->>Caller: expose updated value
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
web/packages/studio/src/util/hooks/useLocalStorage.ts (1)
37-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an explicit return type to the exported
useLocalStorage.Per coding guidelines, public APIs should have explicit return types. Currently the tuple type is only inferred via
as const.✏️ Suggested fix
-export const useLocalStorage = <T>(key: string, defaultValue?: T) => { +export const useLocalStorage = <T>( + key: string, + defaultValue?: T +): readonly [T | undefined, (value: T) => void, () => void] => {As per coding guidelines, "Use explicit return types for public APIs and complex functions in TypeScript."
Also applies to: 104-104
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/packages/studio/src/util/hooks/useLocalStorage.ts` at line 37, Update the exported useLocalStorage function to declare an explicit return type, including the tuple’s value and setter types, rather than relying on inference from as const. Apply the same explicit return-type requirement to the related exported symbol at the other indicated location, preserving the existing API behavior.Source: Coding guidelines
web/packages/studio/src/util/hooks/useLocalStorage.test.ts (1)
75-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing test for the
e.key === null(storage cleared) branch.
subscribe'sonStoragehandler explicitly special-casese.key === null(storage cleared) inuseLocalStorage.ts, but no test dispatches aStorageEvent('storage', { key: null })to exercise it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/packages/studio/src/util/hooks/useLocalStorage.test.ts` around lines 75 - 87, Extend the useLocalStorage tests around the “reacts to storage events from other tabs” case to cover a StorageEvent with key set to null, representing cleared storage. Set the stored value, dispatch the event, and assert the hook updates according to the e.key === null branch in subscribe’s onStorage handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@web/packages/studio/src/util/hooks/useLocalStorage.test.ts`:
- Around line 75-87: Extend the useLocalStorage tests around the “reacts to
storage events from other tabs” case to cover a StorageEvent with key set to
null, representing cleared storage. Set the stored value, dispatch the event,
and assert the hook updates according to the e.key === null branch in
subscribe’s onStorage handler.
In `@web/packages/studio/src/util/hooks/useLocalStorage.ts`:
- Line 37: Update the exported useLocalStorage function to declare an explicit
return type, including the tuple’s value and setter types, rather than relying
on inference from as const. Apply the same explicit return-type requirement to
the related exported symbol at the other indicated location, preserving the
existing API behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 2789da4b-8fb2-45e8-b182-8e9eb993a8bb
📒 Files selected for processing (2)
web/packages/studio/src/util/hooks/useLocalStorage.test.tsweb/packages/studio/src/util/hooks/useLocalStorage.ts
|
Summary
Rewrites
useLocalStorage(web/packages/studio/src/util/hooks/useLocalStorage.ts) to use React 18'suseSyncExternalStoreinstead ofuseState+ a lazy initializer. The public API —[value, setValue, deleteValue]— is unchanged, so no callsites needed edits.Resolves ASTD-272.
What changed
storageevent, so a write in another tab now re-renders this tab.storageevent only fires in other tabs, sosetValue/deleteValuealso dispatch a customnemo-studio:local-storageevent. Two hooks on the same key in one tab now stay in sync (previously each had its own isolateduseState).typeof window === 'undefined'guard is replaced by thegetServerSnapshotparameter.Two correctness details
useSyncExternalStoreforcesuseSyncExternalStorecompares snapshots withObject.is:JSON.parseresult each render.ExperimentGroupDataView,useRecentWorkspaces) pass an inline[], a new reference every render — returning that directly would trigger React's "getSnapshot should be cached" infinite loop. Freezing it also matches the old lazy-useState"read the default once" semantics.Minor behavior change
After
deleteValue, the value now falls back to the default rather thanundefined. For callsites without a default this is identical (undefinedeither way); for those with one it's arguably more correct.Testing
useLocalStorage.test.ts(11 tests) — mirrors the siblinguseSessionStorage.test.tsplus new cases for cross-tabstorageevents, same-tab sync, and stable snapshot references.typecheck,lint, the new tests, and theThemeSwitchcallsite test all pass.Summary by CodeRabbit