fix(frontend): Fix Zustand authStore hydration timing issue
Fix race condition where Epic form checked user authentication before Zustand persist middleware completed hydration from localStorage. Root cause: - authStore uses persist middleware to restore from localStorage - Hydration is asynchronous - Epic form checked user state before hydration completed - Result: "User not authenticated" error on page refresh Changes: - Add isHydrated state to authStore interface - Add onRehydrateStorage callback to track hydration completion - Update epic-form to check isHydrated before checking user - Disable submit button until hydration completes - Show "Loading..." button text during hydration - Improve error messages for better UX - Add console logging to track hydration process Testing: - Page refresh should now wait for hydration - Epic form correctly identifies logged-in users - Submit button disabled until auth state ready - Clear user feedback during loading state Fixes: Epic creation "User not authenticated" error on refresh 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,7 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
const createEpic = useCreateEpic();
|
const createEpic = useCreateEpic();
|
||||||
const updateEpic = useUpdateEpic();
|
const updateEpic = useUpdateEpic();
|
||||||
const user = useAuthStore((state) => state.user);
|
const user = useAuthStore((state) => state.user);
|
||||||
|
const isHydrated = useAuthStore((state) => state.isHydrated);
|
||||||
|
|
||||||
const form = useForm<EpicFormValues>({
|
const form = useForm<EpicFormValues>({
|
||||||
resolver: zodResolver(epicSchema),
|
resolver: zodResolver(epicSchema),
|
||||||
@@ -71,12 +72,19 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(data: EpicFormValues) {
|
async function onSubmit(data: EpicFormValues) {
|
||||||
console.log('[EpicForm] onSubmit triggered', { data, user: user?.id, projectId });
|
console.log('[EpicForm] onSubmit triggered', { data, user: user?.id, projectId, isHydrated });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if auth store has completed hydration
|
||||||
|
if (!isHydrated) {
|
||||||
|
console.warn('[EpicForm] Auth store not hydrated yet, waiting...');
|
||||||
|
toast.error('Loading user information, please try again in a moment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
console.error('[EpicForm] User not authenticated');
|
console.error('[EpicForm] User not authenticated');
|
||||||
toast.error('User not authenticated');
|
toast.error('Please log in to create an epic');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,9 +247,9 @@ export function EpicForm({ projectId, epic, onSuccess, onCancel }: EpicFormProps
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="submit" disabled={isLoading}>
|
<Button type="submit" disabled={isLoading || !isHydrated}>
|
||||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{isEditing ? 'Update Epic' : 'Create Epic'}
|
{!isHydrated ? 'Loading...' : isEditing ? 'Update Epic' : 'Create Epic'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface AuthState {
|
|||||||
user: User | null;
|
user: User | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isHydrated: boolean;
|
||||||
|
|
||||||
setUser: (user: User) => void;
|
setUser: (user: User) => void;
|
||||||
clearUser: () => void;
|
clearUser: () => void;
|
||||||
@@ -18,6 +19,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
user: null,
|
user: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
isHydrated: false,
|
||||||
|
|
||||||
setUser: (user) =>
|
setUser: (user) =>
|
||||||
set({ user, isAuthenticated: true, isLoading: false }),
|
set({ user, isAuthenticated: true, isLoading: false }),
|
||||||
@@ -31,6 +33,16 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
user: state.user,
|
user: state.user,
|
||||||
isAuthenticated: state.isAuthenticated,
|
isAuthenticated: state.isAuthenticated,
|
||||||
}),
|
}),
|
||||||
|
onRehydrateStorage: () => (state) => {
|
||||||
|
console.log('[AuthStore] Hydration started');
|
||||||
|
if (state) {
|
||||||
|
state.isHydrated = true;
|
||||||
|
console.log('[AuthStore] Hydration completed', {
|
||||||
|
user: state.user?.id,
|
||||||
|
isAuthenticated: state.isAuthenticated
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user