:fix: prevent form prefilling when creating new Blueprint during acti…#289
:fix: prevent form prefilling when creating new Blueprint during acti…#289
Conversation
…ve generation When navigating to create a new Blueprint while another generation was in progress, the form would be prefilled with data from the previous Blueprint. This was caused by a race condition where the zustand store's reset() was called before the persist middleware finished rehydrating from IndexedDB, causing the hydrated state to overwrite the reset. Added hydration tracking to the create blueprint store: - Added hasHydrated state and onRehydrateStorage callback - Updated useEffect to wait for hydration before calling reset() - Added loader during hydration to prevent flash of stale content Fixes #273
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 📝 WalkthroughWalkthroughIntroduced hydration guards to the create blueprint store and page component to prevent state race conditions. Added Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/app/create/`[id]/store.ts:
- Around line 345-348: onRehydrateStorage currently uses optional chaining and
silently skips setting hydration flag when the persisted state is missing or
malformed; replace that with an explicit guard: if the incoming state is
undefined or falsy, call the store's setter to mark hasHydrated true via the
current store API (so hasHydrated is set even on rehydration failure), otherwise
call state.setHasHydrated(true); follow the defensive pattern used in
useAuthStore and reference onRehydrateStorage and setHasHydrated when making the
change.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/app/create/[id]/page.tsxsrc/app/create/[id]/store.ts
🔇 Additional comments (5)
src/app/create/[id]/store.ts (2)
28-29: LGTM! Hydration state tracking looks correct.The type definitions and initial state setup correctly implement hydration tracking. The
hasHydratedflag starts asfalseand will be set totrueviaonRehydrateStorageafter the store rehydrates from IndexedDB.Also applies to: 81-82
342-344: LGTM! Correctly excludes hydration state from persistence.Excluding
hasHydratedandsetHasHydratedfrom persistence ensures these runtime-only fields don't get stored in IndexedDB, which would defeat the purpose of tracking hydration status.src/app/create/[id]/page.tsx (3)
51-52: LGTM! Good aliasing to avoid confusion.Renaming the store's
hasHydratedtostoreHasHydratedclearly distinguishes it from the auth store'shasHydrated(line 38), improving code readability.
122-150: LGTM! Hydration guard correctly prevents the race condition.The early return when
!storeHasHydratedensures thatreset()is only called after the persist middleware has finished rehydrating from IndexedDB. This directly addresses the root cause: previously,reset()would run immediately, but then the late hydration would overwrite the reset state with stale data.The updated dependency array
[id, storeHasHydrated]ensures the effect re-runs once hydration completes, which will then properly execute the reset or load logic.
454-461: LGTM! Dual hydration check prevents flash of stale content.Checking both
hasHydrated(auth store) andstoreHasHydrated(create blueprint store) before rendering ensures the component doesn't briefly show stale form data while either store is rehydrating.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| onRehydrateStorage: () => (state) => { | ||
| // This is called after the store has been rehydrated from storage | ||
| state?.setHasHydrated(true); | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the store file to understand the context
fd "store.ts" | head -20Repository: zkemail/registry
Length of output: 288
🏁 Script executed:
# Let's examine the file structure
git ls-files "src/app/create" | head -20Repository: zkemail/registry
Length of output: 318
🏁 Script executed:
# Let's read the store.ts file to see the code around lines 345-348
wc -l src/app/create/[id]/store.tsRepository: zkemail/registry
Length of output: 92
🏁 Script executed:
# Read the file content around the specified lines
sed -n '330,360p' src/app/create/[id]/store.tsRepository: zkemail/registry
Length of output: 992
🏁 Script executed:
# Let's see more context about the store initialization and state structure
sed -n '1,100p' src/app/create/[id]/store.tsRepository: zkemail/registry
Length of output: 3240
🏁 Script executed:
# Check if there are other stores using onRehydrateStorage for comparison
rg "onRehydrateStorage" --type ts -B 2 -A 5Repository: zkemail/registry
Length of output: 958
🏁 Script executed:
# Let's check the useAuthStore implementation for comparison
cat -n src/lib/stores/useAuthStore.tsRepository: zkemail/registry
Length of output: 2142
🏁 Script executed:
# Search for how hasHydrated is used in the codebase
rg "hasHydrated" --type tsx --type ts -B 2 -A 2Repository: zkemail/registry
Length of output: 87
🏁 Script executed:
# Search for hasHydrated usage without specifying file type
rg "hasHydrated" -B 2 -A 2Repository: zkemail/registry
Length of output: 2971
🏁 Script executed:
# Check how hasHydrated is actually used in components
fd -e tsx -e ts | xargs rg "hasHydrated"Repository: zkemail/registry
Length of output: 816
🌐 Web query:
Zustand persist middleware onRehydrateStorage state undefined behavior
💡 Result:
Short answer: onRehydrateStorage can receive undefined when there is no saved state (or when stored data fails to parse/version-check/migrate). Also beware that the persisted JSON only contains serializable values — functions/actions are not stored — and the default merge is a shallow merge, so a persisted object can overwrite initial fields (including replacing objects that held methods). Handle both cases by checking for undefined and reattaching any functions or defaults in the after-rehydrate callback or using merge/migrate/versioning.
Key points and examples:
- The persist docs describe onRehydrateStorage's signature and show it may be called with state (or nothing) and that it should return a post‑hydrate function. Use the returned function to update store flags or reattach things. [1][2]
- If nothing was persisted (or version/migrate rejects the stored value), the callback will get undefined — check for that before using state. [1][2]
- Non-serializable values (functions) are not saved; actions must come from the initial creator or be restored/rewired after hydration. Otherwise calling them will be undefined after rehydration. See common reports and notes in community threads. [3]
- If you need to preserve methods or complex objects, either store only plain data and recreate methods after rehydrate, or use persist options like merge/migrate/version/partialize to control what’s saved and how it’s merged. [1][2][4]
References:
- Zustand persist middleware docs (options, onRehydrateStorage). [1]
- Zustand “Persisting store data” examples including onRehydrateStorage usage. [2]
- Community/StackOverflow discussion showing functions/actions omitted on hydration. [3]
- Implementation/merge notes showing shallow-merge behavior and rehydrate flow. [4]
If you want, tell me your store code and storage contents and I’ll point exactly where to add undefined checks / restore logic.
Align with existing codebase pattern and add fallback for rehydration failures.
The state parameter in onRehydrateStorage can be undefined if there's no saved state or if storage parsing fails. The current optional chaining silently fails, leaving hasHydrated as false indefinitely. The sibling useAuthStore in the codebase uses an explicit guard pattern, which is more defensive.
Consider:
onRehydrateStorage: () => (state) => {
// This is called after the store has been rehydrated from storage
- state?.setHasHydrated(true);
+ if (state) {
+ state.setHasHydrated(true);
+ } else {
+ useCreateBlueprintStore.setState({ hasHydrated: true });
+ }
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onRehydrateStorage: () => (state) => { | |
| // This is called after the store has been rehydrated from storage | |
| state?.setHasHydrated(true); | |
| }, | |
| onRehydrateStorage: () => (state) => { | |
| // This is called after the store has been rehydrated from storage | |
| if (state) { | |
| state.setHasHydrated(true); | |
| } else { | |
| useCreateBlueprintStore.setState({ hasHydrated: true }); | |
| } | |
| }, |
🤖 Prompt for AI Agents
In `@src/app/create/`[id]/store.ts around lines 345 - 348, onRehydrateStorage
currently uses optional chaining and silently skips setting hydration flag when
the persisted state is missing or malformed; replace that with an explicit
guard: if the incoming state is undefined or falsy, call the store's setter to
mark hasHydrated true via the current store API (so hasHydrated is set even on
rehydration failure), otherwise call state.setHasHydrated(true); follow the
defensive pattern used in useAuthStore and reference onRehydrateStorage and
setHasHydrated when making the change.
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
|
Fixed |
…ve generation
When navigating to create a new Blueprint while another generation was in progress, the form would be prefilled with data from the previous Blueprint. This was caused by a race condition where the zustand store's reset() was called before the persist middleware finished rehydrating from IndexedDB, causing the hydrated state to overwrite the reset.
Added hydration tracking to the create blueprint store:
Fixes #273
Summary
Type of Change
Target Branch
dev- New feature or non-urgent fixstaging- Ready for QA validationmain- Hotfix for production issueChanges Made
Breaking Changes
Testing
Checklist
Screenshots/Recordings
Related Issues
Additional Notes
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.