Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/app/create/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => {
file,
setFile,
blueprint,
hasHydrated: storeHasHydrated,
} = store;

const [errors, setErrors] = useState<string[]>([]);
Expand Down Expand Up @@ -118,7 +119,13 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => {
}, [savedEmls[id]]);

// Load data if an id is provided
// Wait for store hydration before resetting to prevent race condition
// where hydrated state overwrites the reset state
useEffect(() => {
if (!storeHasHydrated) {
return;
}

if (id === 'new') {
reset();
setHasLoadedBlueprint(false);
Expand All @@ -140,7 +147,7 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => {
};
loadBlueprint();
}
}, [id]); // Only run when id changes, not on step changes
}, [id, storeHasHydrated]); // Run when id or hydration state changes

useEffect(() => {
// Load all EMLs from IndexedDB on mount
Expand Down Expand Up @@ -444,8 +451,8 @@ const CreateBlueprint = ({ params }: { params: Promise<{ id: string }> }) => {
return <></>;
};

// Show loader while auth store is hydrating from localStorage
if (!hasHydrated) {
// Show loader while auth store or create blueprint store is hydrating from storage
if (!hasHydrated || !storeHasHydrated) {
return (
<div className="my-16 flex justify-center">
<Loader />
Expand Down
14 changes: 13 additions & 1 deletion src/app/create/[id]/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ interface ExtendedBlueprintProps extends BlueprintProps {
type CreateBlueprintState = ExtendedBlueprintProps & {
blueprint: Blueprint | null;
validationErrors: ValidationErrors;
hasHydrated: boolean;
setHasHydrated: (value: boolean) => void;
setField: (field: keyof ExtendedBlueprintProps, value: any) => void;
validateField: (field: keyof BlueprintProps) => void;
validateAll: () => boolean;
Expand Down Expand Up @@ -76,6 +78,8 @@ export const useCreateBlueprintStore = create<CreateBlueprintState>()(
blueprint: null,
validationErrors: {},
file: null,
hasHydrated: false,
setHasHydrated: (value: boolean) => set({ hasHydrated: value }),

setField: (field: keyof ExtendedBlueprintProps, value: any) => {
set({ [field]: value });
Expand Down Expand Up @@ -335,9 +339,17 @@ export const useCreateBlueprintStore = create<CreateBlueprintState>()(
name: 'create-blueprint',
// Exclude 'file' from persistence as File objects cannot be serialized
partialize: (state) => {
const { file, blueprint, ...rest } = state;
const { file, blueprint, hasHydrated, setHasHydrated, ...rest } = state;
return rest;
},
onRehydrateStorage: () => (state) => {
// This is called after the store has been rehydrated from storage
if (state) {
state.setHasHydrated(true);
} else {
useCreateBlueprintStore.setState({ hasHydrated: true });
}
},
Comment on lines +345 to +352

@coderabbitai coderabbitai Bot Jan 15, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the store file to understand the context
fd "store.ts" | head -20

Repository: zkemail/registry

Length of output: 288


🏁 Script executed:

# Let's examine the file structure
git ls-files "src/app/create" | head -20

Repository: 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.ts

Repository: 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.ts

Repository: 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.ts

Repository: 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 5

Repository: zkemail/registry

Length of output: 958


🏁 Script executed:

# Let's check the useAuthStore implementation for comparison
cat -n src/lib/stores/useAuthStore.ts

Repository: 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 2

Repository: zkemail/registry

Length of output: 87


🏁 Script executed:

# Search for hasHydrated usage without specifying file type
rg "hasHydrated" -B 2 -A 2

Repository: 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.

Suggested change
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@wryonik can you check this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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!

storage: {
getItem: async (name) => {
const value = await get(name);
Expand Down